├── .gitignore ├── variables.csv.example ├── environment.conf.example ├── README.md └── tfcli.sh /.gitignore: -------------------------------------------------------------------------------- 1 | logs/ 2 | 3 | environment.conf 4 | variables.csv 5 | 6 | *.bak 7 | -------------------------------------------------------------------------------- /variables.csv.example: -------------------------------------------------------------------------------- 1 | # 2 | # Variables CSV File 3 | # 4 | # ASSIGNING Values TO DECLARED Variables (wiz of oz secret-zero starting-point) 5 | # KEY,VALUE,[terraform|env],HCL[true|false],SENSITIVE[true|false] 6 | # 7 | aws_region,eu-central-2,terraform,false,false 8 | key1,value,env,false,false 9 | key2,sensitive_value,env,false,true -------------------------------------------------------------------------------- /environment.conf.example: -------------------------------------------------------------------------------- 1 | # address of the API endpoint of your Terrform Enterprise/Cloud service. 2 | # app.terraform.io is the default for Terraform Cloud 3 | address="app.terraform.io" 4 | 5 | # The Terraform Oranization we want to work with 6 | organization="joestack" 7 | 8 | # the name of the workspace we are going to create 9 | workspace="tfc-aws-hashistack" 10 | 11 | # HashiCorp Internal: 12 | # as HashiCorp empoyee we use Doormat to inject cloud credentials dynamically 13 | # as non HashiCorp empoyee set it to false and ignore the doormat_arn 14 | inject_cloud_credentials="false" 15 | doormat_arn="arn:aws:iam::1234567890:role/test_demos_dev-tester" 16 | # If varset="" cloud credentials are injected into workspace otherwise into a varset 17 | varset="Provider_Credentials" 18 | 19 | # as good best practices we stick to a specific Terraform version 20 | terraform_version="1.6.6" 21 | 22 | # in case you want a workflow that automatically triggers the apply afer a successful plan 23 | auto_apply="true" 24 | 25 | # When set to false, runs triggered by a VCS change will not be queued until at least one run is manually queued. 26 | # should be false if trigger_run is set to true 27 | queue_all_runs="false" 28 | 29 | # in case you want to share state-file information across worspaces within the Terraform organization 30 | global_remote_state="true" 31 | 32 | # in case you want to assign a VCS repo to the workspace 33 | # If set to "true" we need the vcs_provider_oauth_token_id as well 34 | assign_vcs_to_workspace="true" 35 | 36 | # the oauth_token_id of your specific VCS Provider within your Terraform organization 37 | # can be found in the Terraform UI under Settings > Version Control > Providers 38 | vcs_provider_oauth_token_id="ot-blablabla" 39 | 40 | # the VCS repo that you want to connect to the workspace 41 | vcs_repo="joestack/tfc-aws-hashistack" 42 | 43 | # trigger run immediately (by executing this script) 44 | trigger_run="true" 45 | 46 | # in case you want to directly attach one or more policies to the workspace 47 | # the policies must exist within your Terraform organization upfront 48 | attach_workspace2policyset="false" 49 | policyset_names=(costs production) 50 | 51 | 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tfc-api-bootstrap-script 2 | Using the Terraform Enterprise/Cloud API to generate a Workspace, injecting Variables, connect VCS repository, assigning Policies, and trigger a run. 3 | 4 | This allows a very simple Continuous Infrastructure Automation (CIA) pipeline consisting of only two components - A version control system and Terraform Cloud/Enterprise. 5 | 6 | This enables: 7 | - Programmatic generation of Terraform Workspaces and Landing Zones, respectively. 8 | - The secure injection of secrets like cloud credentials as workspace variables (the secret-zero that enables the workflow). 9 | - The segregation of responsibilities in the form of CIA Pipleline Publisher (admin) and Pipeline Consumer (IaC developer). 10 | 11 | --- 12 | 13 | ## Content of this repository: 14 | 15 | | File | Description | 16 | | - | :- | 17 | | README.md | This README | 18 | | tfcli.sh | The script that contains the TF[C/E] API abstractions | 19 | | environment.conf.example | Briefly documented example of an environment.conf declaration | 20 | | variables.csv.example | Briefly documented example of a variables.csv to be injected as workspace variables | 21 | 22 | 23 | ## environment.conf 24 | to customize your specific needs in terms of workspace to be generated, VCS repo to be assigned, policies to be used, etc... 25 | 26 | ## variables.csv 27 | to assign your specific terraform and environment variables. Mark them as sensitive if needed so that nobody else will ever see them. The format is: Key,Value,terraform or environment variable,HCL true/false, sensitive true/false 28 | 29 | 30 | Finally run the **tfcli.sh -b** script to create or re-create your infrastructure/workload based on the IaC declaration that resides in a version control system. 31 | 32 | --- 33 | 34 | # Quick HowTo 35 | a) Clone the repo on your local machine (Linux/Mac) 36 | ``` 37 | git clone https://github.com/joestack/tfc-api-bootstrap-script.git 38 | ``` 39 | b) Copy the tfcli.sh script into a folder that is part of your $PATH i.e. /usr/local/bin to make the script globally available on your system. You can also skip that step and run the script directly from its local folder. 40 | ``` 41 | cd tfc-api-bootstrap-script 42 | chmod 755 tfcli.sh 43 | sudo mv tfcli.sh /usr/local/bin 44 | ``` 45 | 46 | c) Define your specific **environments.conf** and **variables.csv** within a folder and run the script to bootstrap the environment. 47 | ``` 48 | tfcli.sh -b 49 | ``` 50 | 51 | --- 52 | `````` 53 | $> tfcli.sh -OPTION 54 | 55 | [-h] Print this help message 56 | [-b] Bootstrap the environment based on environment.conf and variables.csv 57 | [-x] Destroy run on Workspace to delete all resources 58 | [-X] Delete Workspace (DANGER!!! Be sure no unmanaged resources are left) 59 | [-d] Print Debug output 60 | [-V] Version Info 61 | HASHICORP INTERNAL (can be ignored): 62 | [-c] Inject AWS cloud credentials to Workspace via Doormat (AWS only) 63 | [-C] Inject AWS cloud credentials as Variable-Set via Doormat (AWS only) 64 | 65 | -------------------------------------------------------------------------------- /tfcli.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | version=240823 3 | 4 | #set -o xtrace 5 | 6 | 7 | workdir=$(pwd) 8 | logdir=$workdir/logs 9 | debug=false 10 | #pit=`date +%s@%N` 11 | #pit=$(`date +%s@%N`) 12 | 13 | [[ -d $logdir ]] || mkdir $logdir 14 | cd $logdir 15 | 16 | # Utility function to log output 17 | log() { 18 | local log_text="$1" 19 | local log_level="$2" 20 | local log_color="$3" 21 | 22 | echo -e "${log_color}[$(date +"%Y-%m-%d %H:%M:%S %Z")] [${log_level}] ${log_text} ${LOG_DEFAULT_COLOR}"; 23 | return 0; 24 | } 25 | 26 | log_info() { log "$1" "INFO" "\033[1m"; } 27 | log_debug() { [[ "${debug}" = "true" ]] && log "$1" "DEBUG" "\033[1;34m"; } 28 | log_success() { log "$1" "SUCCESS" "\033[1;32m"; } 29 | log_error() { log "$1" "ERROR" "\033[1;31m"; } 30 | 31 | # Utility function to simplify curl calls and handle relevant return codes 32 | execute_curl() { 33 | local token="$1" 34 | local http_method="$2" 35 | local url="$3" 36 | local payload="$4" 37 | 38 | case $http_method in 39 | GET | DELETE) 40 | local result=$(curl -Ss \ 41 | --header "Authorization: Bearer ${token}" \ 42 | --header "Content-Type: application/vnd.api+json" \ 43 | --request "${http_method}" \ 44 | "${url}") 45 | ;; 46 | PATCH | POST) 47 | local result=$(curl -Ss \ 48 | --header "Authorization: Bearer ${token}" \ 49 | --header "Content-Type: application/vnd.api+json" \ 50 | --request "${http_method}" \ 51 | --data @${payload} \ 52 | "${url}") 53 | ;; 54 | *) 55 | log_error "invalid tf_curl request" && exit 1 56 | esac 57 | 58 | echo "${result}" 59 | } 60 | 61 | # Utlity function to check if required software is available 62 | is_command_installed() { 63 | local command_to_check="$1" 64 | 65 | if ! command -v ${command_to_check} &> /dev/null 66 | then 67 | log_error "${command_to_check} could not be found. Please install it." 68 | exit 1 69 | else 70 | log_debug "${command_to_check} could be found." 71 | fi 72 | } 73 | 74 | check_environment() { 75 | if [[ ! -e $workdir/environment.conf ]] ; then 76 | log_error "no environment.conf file found in $workdir" && exit 1 77 | else 78 | source $workdir/environment.conf 79 | log_debug "environment.conf successfully sourced." 80 | fi 81 | } 82 | 83 | check_variables() { 84 | if [[ ! -e $workdir/variables.csv ]] ; then 85 | log_error "no variables.csv file found in $workdir" && exit 1 86 | fi 87 | } 88 | 89 | check_tfc_token() { 90 | if [[ ! -e ~/.terraform.d/credentials.tfrc.json ]] ; then 91 | log_error "No TFC/TFE token found. Please execute 'terraform login'" && exit 1 92 | else 93 | tfc_token=$(cat ~/.terraform.d/credentials.tfrc.json | jq -r ".credentials.\"${address}\".token ") 94 | log_debug "Using TFC/TFE token from ~/.terraform.d/credentials.tfrc.json" 95 | fi 96 | } 97 | 98 | check_doormat() { 99 | if [[ $(doormat aws list) ]] ; then 100 | log_success "doormat is initialized." 101 | else 102 | log_error "doormat has not been initialized. Please run 'doormat login'" && exit 1 103 | fi 104 | } 105 | 106 | create_workspace_api() { 107 | local workspace="$1" 108 | pit=`date +%s@%N` 109 | 110 | tee $logdir/workspace-$pit.json > /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null < /dev/null 472 | fi 473 | log_success "Cloud credentials have been injected into the workspace via doormat." 474 | } 475 | 476 | inject_cloud_credentials_varset() { 477 | 478 | # Retrieve varset id as prerequisite 479 | local varset_id=$( 480 | execute_curl $tfc_token "GET" \ 481 | "https://${address}/api/v2/organizations/${organization}/varsets" |\ 482 | jq -r ".data[] | select (.attributes.name == \"$varset\") | .id" 483 | ) 484 | 485 | if [[ "${debug}" = "true" ]]; then 486 | doormat aws -r $doormat_arn tf-push variable-set --id $varset_id 487 | else 488 | doormat aws -r $doormat_arn tf-push variable-set --id $varset_id &> /dev/null 489 | fi 490 | log_success "Cloud credentials have been injected into the variable-set via doormat." 491 | } 492 | 493 | 494 | ######################################################### 495 | # Step 3.1: ATTACH POLICY-SET TO WORKSPACE # 496 | ######################################################### 497 | attach_workspace2policyset() { 498 | # Retrieve workspace ID as prerequisite to attach a policy-set to that workspace 499 | local workspace_id=$( 500 | execute_curl $tfc_token "GET" \ 501 | "https://${address}/api/v2/organizations/${organization}/workspaces" |\ 502 | jq -r ".data[] | select (.attributes.name == \"$workspace\") | .id" 503 | ) 504 | 505 | for i in ${!policyset_names[*]} 506 | do 507 | local policy_set_id=$( 508 | execute_curl $tfc_token "GET" \ 509 | "https://${address}/api/v2/organizations/${organization}/policy-sets" |\ 510 | jq -r ".data[] | select (.attributes.name == \"${policyset_names[$i]// /}\") | .id" 511 | ) 512 | 513 | attach_workspace2policyset_api $workspace_id $policy_set_id ${policyset_names[$i]// /} 514 | done 515 | } 516 | 517 | ######################################## 518 | # Step 4: ASSIGN VCS REPO TO WORKSPACE # 519 | ######################################## 520 | add_vcs_to_workspace() { 521 | if [[ "$vcs_repo" == *\\* ]] 522 | then 523 | vcs_repo=$vcs_repo 524 | else 525 | vcs_repo=$(echo $vcs_repo | sed 's/\//\\\//g') 526 | fi 527 | 528 | add_vcs_to_workspace_api $workspace $vcs_repo $vcs_provider_oauth_token_id 529 | } 530 | 531 | ############################################### 532 | # Step 5: ADDING ALL OTHER WORKSPACE SETTINGS # 533 | ############################################### 534 | add_workspace_settings() { 535 | add_workspace_settings_api $workspace $global_remote_state $auto_apply $queue_all_runs $terraform_version 536 | } 537 | 538 | ####################################### 539 | # Step 6: TRIGGER A RUN ON WORKSPACE # 540 | ####################################### 541 | trigger_run() { 542 | local result_get_workspace_id=$( 543 | execute_curl $tfc_token "GET" \ 544 | "https://${address}/api/v2/organizations/${organization}/workspaces" |\ 545 | jq -r ".data[] | select (.attributes.name == \"$workspace\") | .id" 546 | ) 547 | 548 | log_debug "Workspace ID: ${result_get_workspace_id}" 549 | 550 | trigger_run_api $result_get_workspace_id 551 | } 552 | 553 | usage() { 554 | echo 555 | echo "$(basename "$0") -- programmatically create a Terraform [Cloud|Enterprise] Landing-Zone" 556 | echo 557 | echo "Create a Workspace, inject Variables, connect VCS repository, assign Policies via API" 558 | echo "Publish a VCS-driven pipeline from an administrative perspective" 559 | echo "that can be used by a developer or team of developers in a self service manner" 560 | echo "(separation of duties)". 561 | echo 562 | echo "https://github.com/joestack/tfc-api-bootstrap-script.git for more details" 563 | echo 564 | echo 565 | echo "[-h] Print this help message" 566 | echo "[-b] Bootstrap the environment based on environment.conf and variables.csv" 567 | #echo "[-e] TODO /PATH/TO/environment.conf - override the workdir as location for the environment.conf file" 568 | #echo "[-v] TODO /PATH/TO/variables.csv - override the workdir as location for the variables.csv file" 569 | echo "[-c] Inject AWS cloud credentials to Workspace via Doormat (only AWS is supported by Doormat)" 570 | echo "[-C] Inject AWS cloud credentials as Variable-Set via Doormat (only AWS is supported by Doormat)" 571 | #echo "[-i] Inject AWS cloud credentials via native API calls" 572 | echo "[-x] Destroy run on Workspace to delete all resources" 573 | echo "[-X] Delete Workspace (DANGER!!! Be sure no unmanaged resources are left)" 574 | echo "[-d] Print Debug output" 575 | echo "[-V] Version Info" 576 | echo 577 | } 578 | 579 | log_debug "\nPREREQUISITES:\nPlease make sure that you have a TFC/TFE organization available and configured in the environment.conf. \nIf you are using Sentinel policies, you need to have a TFC organization with Business subscription or TFE with Governance&Policy module enabled. \nThe organization must have a VCS Provider configured as well." 580 | 581 | are_commands_installed() { 582 | is_command_installed "jq" 583 | is_command_installed "sed" 584 | [[ $inject_cloud_credentials = "true" ]] && is_command_installed "doormat" 585 | is_command_installed "grep" 586 | is_command_installed "curl" 587 | is_command_installed "terraform" 588 | } 589 | 590 | while getopts ":hVdcCixXb" opt; do 591 | case ${opt} in 592 | h ) 593 | usage 594 | exit 2 595 | ;; 596 | V ) 597 | echo $version 598 | exit 0 599 | ;; 600 | d ) 601 | debug=true 602 | ;; 603 | e ) 604 | # define path to environment.conf 605 | # to override $workdir as default location 606 | ;; 607 | v ) 608 | # define path to variables.csv 609 | # to override $workdir as default location 610 | ;; 611 | x ) 612 | # Destroy all resouces 613 | # Delete Workspace 614 | # ensure to destroy before delete (destroy without delete ->OK, delete without destroy ->NOT) 615 | are_commands_installed 616 | check_environment 617 | check_tfc_token 618 | destroy_run_api 619 | exit 0 # just to prevent -xX 620 | ;; 621 | X ) 622 | # Destroy all resouces 623 | # Delete Workspace 624 | # ensure to destroy before delete (destroy without delete ->OK, delete without destroy ->NOT) 625 | are_commands_installed 626 | check_environment 627 | check_tfc_token 628 | delete_workspace_api 629 | ;; 630 | c ) 631 | # non generic doormat solution that works for AWS only 'doormat aws tf-push ...' 632 | are_commands_installed 633 | check_environment 634 | check_doormat 635 | check_tfc_token 636 | inject_cloud_credentials_workspace 637 | ;; 638 | C ) 639 | # non generic doormat solution that works for AWS only 'doormat aws tf-push ...' 640 | are_commands_installed 641 | check_environment 642 | check_doormat 643 | check_tfc_token 644 | inject_cloud_credentials_varset 645 | ;; 646 | i ) 647 | # more generic (but doormat seems to be broken when using 'doormat aws -json ...') 648 | are_commands_installed 649 | check_environment 650 | check_doormat 651 | check_tfc_token 652 | delete_ws_variables_aws 653 | get_doormat_aws_credentials 654 | #exit 0 # TO BE REMOVED 655 | ;; 656 | b ) 657 | # bootstrap main 658 | are_commands_installed 659 | check_environment 660 | check_variables 661 | check_tfc_token 662 | [[ $inject_cloud_credentials = "true" ]] && check_doormat 663 | create_workspace 664 | create_variables 665 | [[ $inject_cloud_credentials = "true" && $varset = "" ]] && inject_cloud_credentials_workspace 666 | [[ $inject_cloud_credentials = "true" && $varset != "" ]] && create_varset && inject_cloud_credentials_varset 667 | [[ $attach_workspace2policyset = "true" ]] && attach_workspace2policyset 668 | [[ $assign_vcs_to_workspace = "true" ]] && add_vcs_to_workspace 669 | add_workspace_settings 670 | [[ $trigger_run = "true" ]] && trigger_run 671 | ;; 672 | \? ) 673 | echo "Invalid Option: -$OPTARG" 1>&2 674 | exit 1 675 | ;; 676 | esac 677 | done 678 | shift $((OPTIND -1)) 679 | --------------------------------------------------------------------------------