Managing CI/CD on Terraform with GitHub Actions workflows.

Cloud Journeys with Anindita
10 min readSep 14, 2021

GitHub actions is one of the most popular CI/CD tool for managing infrastructure as code (IaC) automation while supporting the code quality, code scanner, monitoring, testing, deployment validation in the workflow pipeline. The GitHub actions comes with the features of available set of action workflows like “simple workflow(customize)”, “deploying node.js to Azure Web apps”, “build & deploying Kubernetes clusters manifests to GKE”, “deploying to Amazon ECS” etc.. apart from that the continuous integration workflows are available for most of programming languages like Python, Ruby, Rust, .NET, java with maven/gradle, scala, docker, R , django & many more…

In this post, we’ll deep dive into managing CI/CD workflows with GitHub Actions on Terraform.

  1. GitHub Actions workflow with for Azure Kubernetes Cluster deployment: In this scenario, a simple AKS cluster is deployment with continuous build & continuous integration workflow on terraform. The remote backend is managed via terraform Cloud for configuring the workspace & the API tokens from terraform cloud are stored in GitHub secrets under Settings page of the repo.

In order to create the action workflow for push/pull request, the following terraform workflow is to be selected & the default terraform.yml script is to be configured in the “.github/workflows directory.

Terraform GitHub action workflow

Sample terraform.yml GitHub actions workflow with single working directory of terraform configurations.

name: 'Terraform'on: [push, pull_request]env:TF_LOG: INFOjobs:terraform:name: 'Terraform'runs-on: ubuntu-latestenvironment: dev# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./shell: bashsteps:# Checkout the repository to the GitHub Actions runner- name: Checkoutuses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}terraform_version: 1.0.4# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform init# Checks that all Terraform configuration files adhere to a canonical format# - name: Terraform Format#  run: terraform fmt -check# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approve

On execution of a GitHub push, it triggers the workflow & prepares the build, starts the terraform deployment lifecycle with “terraform init, plan & apply” steps. The CD results appear as followed.

github action CD workflow for AKS terraform module

2. GitHub Actions workflow with multiple terraform modules deployment:

In this scenario, lets look into the terraform github actions workflow yaml manifest to be configured for jobs execution from multiple working directory where multiple Cloud resource terraform modules are required to build.

The following github action terraform workflow builds & deploy multiple terraform modules concurrently with multiple working directory configuration. It triggers the CD of the terraform modules of “Azure Sentinel”, “Azure Log Analytics workspace” & “Security center” on a git push.

name: 'Terraform'on:push:branches:- mainpull_request:jobs:sentinel:name: 'Terraform(sentinel)'runs-on: ubuntu-latestenvironment: dev# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Azure_Sentinelshell: bashsteps:# Checkout the repository to the GitHub Actions runner- name: Checkoutuses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform init# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color- run: echo ${{ steps.plan.outputs.stdout }}- run: echo ${{ steps.plan.outputs.stderr }}- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approvecontinue-on-error: trueloganalytics:name: 'Terraform(loganalytics)'runs-on: ubuntu-latestenvironment: dev# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Log_Analytics_Workspaceshell: bashsteps:- name: 'Checkout'uses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform init# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color#- run: echo ${{ steps.plan.outputs.stdout }}#- run: echo ${{ steps.plan.outputs.stderr }}#- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approvecontinue-on-error: truesecuritycenter:name: 'Terraform(securitycenter)'runs-on: ubuntu-latestenvironment: dev# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Security_Centershell: bashsteps:- name: 'Checkout'uses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform init# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color# - run: echo ${{ steps.plan.outputs.stdout }}# - run: echo ${{ steps.plan.outputs.stderr }}# - run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approvecontinue-on-error: true

The execution of the terraform workflow actions creates the build jobs simultaneously.

github action CDworkflow for multiple terraform modules

3. GitHub action workflow in terraform for resource deployment in multiple environments(with separate .tfvars file)

In this scenario, the terraform configuration is deployed with multiple .tfvars files applicable for each environments — Dev, QA, UAT, Preprod & Prod.

The terraform module structure for these different environments looks like as the following:

Terraform configuration with .tfvars for each env

The github action workflow(“terraform.yml”) for terraform module build/deploy is configured with separate .tfvars file based on each environment. So that, the state can be managed for each environment without affecting others.

name: 'Terraform'on:push:branches:- mainpull_request:jobs:development:name: 'Terraform(development)'runs-on: ubuntu-latestenvironment: dev# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Config/AKSshell: bashsteps:# Checkout the repository to the GitHub Actions runner- name: Checkoutuses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform initwith:tf_actions_working_dir: ./Developmentargs: 'var-file="dev.tfvars"'# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color- run: echo ${{ steps.plan.outputs.stdout }}- run: echo ${{ steps.plan.outputs.stderr }}- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approveqa:name: 'Terraform(qa)'runs-on: ubuntu-latestenvironment: QA# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Config/AKSshell: bashsteps:- name: 'Checkout'uses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform initwith:tf_actions_working_dir: ./QAargs: 'var-file="qa.tfvars"'# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color- run: echo ${{ steps.plan.outputs.stdout }}- run: echo ${{ steps.plan.outputs.stderr }}- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approveuat:name: 'Terraform(uat)'runs-on: ubuntu-latestenvironment: UAT# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Config/AKSshell: bashsteps:- name: 'Checkout'uses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform initwith:tf_actions_working_dir: ./UATargs: 'var-file="uat.tfvars"'# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color- run: echo ${{ steps.plan.outputs.stdout }}- run: echo ${{ steps.plan.outputs.stderr }}- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files# Note: It is recommended to set up a required "strict" status check in your repository for "Terraform Cloud". See the documentation on "strict" required status checks for more information: https://help.github.com/en/github/administering-a-repository/types-of-required-status-checks- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approvepreprod:name: 'Terraform(preprod)'runs-on: ubuntu-latestenvironment: Pre-Production# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Config/AKSshell: bashsteps:- name: 'Checkout'uses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform initwith:tf_actions_working_dir: ./Preprodargs: 'var-file="preprod.tfvars"'# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color- run: echo ${{ steps.plan.outputs.stdout }}- run: echo ${{ steps.plan.outputs.stderr }}- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approveproduction:name: 'Terraform(production)'runs-on: ubuntu-latestenvironment: Production# Use the Bash shell regardless whether the GitHub Actions runner is ubuntu-latest, macos-latest, or windows-latestdefaults:run:working-directory: ./Config/AKSshell: bashsteps:- name: 'Checkout'uses: actions/checkout@v2# Install the latest version of Terraform CLI and configure the Terraform CLI configuration file with a Terraform Cloud user API token- name: Setup Terraformuses: hashicorp/setup-terraform@v1with:cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}# Initialize a new or existing Terraform working directory by creating initial files, loading any remote state, downloading modules, etc.- name: Terraform Initrun: terraform initwith:tf_actions_working_dir: ./Prodargs: 'var-file="prod.tfvars"'# Generates an execution plan for Terraform- name: Terraform Planrun: terraform plan -no-color- run: echo ${{ steps.plan.outputs.stdout }}- run: echo ${{ steps.plan.outputs.stderr }}- run: echo ${{ steps.plan.outputs.exitcode }}# On push to main, build or change infrastructure according to Terraform configuration files- name: Terraform Applyif: github.ref == 'refs/heads/main' && github.event_name == 'push'run: terraform apply -auto-approve

While configuring the terraform.yml file of github action workflow for build, deployment of terraform modules, the code quality, iac scanner tool(e.g. sonarcube, checkov etc.), monitoring, deployment, testing, quality gates tool can be integrated from github markerplace.

Following is an example for Checkov action template for automated iac code scanning while implementing continuous integration & continuous delivery workflow selected from marketplace.

Checkov github action for terraform code analysis & scanning

Following is the sample Checkov github action workflow for executing terraform code analysis & security vulnerability scanning with checking enabled on directory level.

- name: Checkov GitHub Action
# You may pin to the exact commit or the version.
# uses: bridgecrewio/checkov-action@00cc657f4d415593e5c8897bc87f490497ccb5f9
uses: bridgecrewio/checkov-action@v12.641.0
with:
# directory with infrastructure code to scan
directory: # optional, default is .
# Run scan only on a specific check identifier (comma separated)
check: # optional
# Run scan on all checks but a specific check identifier (comma separated)
skip_check: # optional
# display only failed checks
quiet: # optional
# do not return an error code if there are failed checks
soft_fail: # optional
# run only on a specific infrastructure
framework: # optional
# comma separated list of external (custom) checks directories
external_checks_dirs: # optional
# comma separated list of external (custom) checks repositories
external_checks_repos: # optional
# The format of the output. cli, json, junitxml, github_failed_only
output_format: # optional
# download external terraform modules from public git repositories and terraform registry:true, false
download_external_modules: # optional
# log level
log_level: # optional, default is WARNING

# Happy Terraforming

--

--

Cloud Journeys with Anindita

Cloud Architect. Azure, AWS certified. Terraform & K8, Cloud Native expert. Passionate with GenAI. Views are own.