Skip to content

Terraform_cicd

This chapter covers integrating Terraform into CI/CD pipelines for automated infrastructure management.

┌─────────────────────────────────────────────────────────────────────────────┐
│ Terraform CI/CD Pipeline │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Pipeline Stages │ │
│ │ │ │
│ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │
│ │ │ Init │──▶│ Plan │──▶│ Plan │──▶│ Apply │ │ │
│ │ │ │ │ │ │ Review │ │ │ │ │
│ │ └─────────┘ └─────────┘ └─────────┘ └─────────┘ │ │
│ │ │ │
│ │ • terraform • terraform • Comment • terraform │ │
│ │ init plan on PR apply │ │
│ │ │ │
│ │ • Backend • Validate • Auto- • Approval │ │
│ │ setup • Format approve (optional) │ │
│ │ • Test │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
.github/workflows/terraform.yml
name: Terraform
on:
push:
branches: [main]
pull_request:
branches: [main]
env:
TF_VERSION: '1.6.0'
AWS_REGION: 'us-east-1'
jobs:
terraform:
name: Terraform
runs-on: ubuntu-latest
permissions:
id-token: write
contents: read
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Configure AWS Credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::${{ secrets.AWS_ACCOUNT_ID }}:role/terraform-ci
aws-region: ${{ env.AWS_REGION }}
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: ${{ env.TF_VERSION }}
cli_config_credentials_token: ${{ secrets.TF_API_TOKEN }}
- name: Terraform Format Check
run: terraform fmt -check -recursive
continue-on-error: true
- name: Terraform Init
run: terraform init
working-directory: ./terraform
- name: Terraform Validate
run: terraform validate
- name: Terraform Plan
id: plan
run: terraform plan -no-color -out=tfplan
working-directory: ./terraform
continue-on-error: true
- name: Post Plan Comment
if: github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const output = `#### Terraform Format and Style 🖌\`${{ steps.plan.outcome }}\`
#### Terraform Initialization ⚙️\`${{ steps.init.outcome }}\`
#### Terraform Validation 🟰\`${{ steps.validation.outcome }}\`
<details><summary>Show Plan</summary>
\`\`\`
${{ steps.plan.outputs.stdout }}
\`\`\`
</details>
*Pushed by: @${{ github.actor }}, Action: \`${{ github.event_name }}\`*`;
github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: output
})
- name: Terraform Apply (on main)
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
run: terraform apply -auto-approve tfplan
working-directory: ./terraform
.gitlab-ci.yml
stages:
- validate
- plan
- apply
variables:
TF_VERSION: "1.6.0"
AWS_DEFAULT_REGION: "us-east-1"
terraform:
image: hashicorp/terraform:${TF_VERSION}
stage: validate
script:
- terraform init
- terraform validate
- terraform fmt -check -recursive
artifacts:
paths:
- .terraform/
expire_in: 1 hour
terraform-plan:
image: hashicorp/terraform:${TF_VERSION}
stage: plan
script:
- terraform init
- terraform plan -out=tfplan
- echo "TFPLAN=$(base64 -w0 tfplan)" > tfplan.env
artifacts:
paths:
- tfplan
- tfplan.env
expire_in: 1 week
only:
- merge_requests
terraform-apply:
image: hashicorp/terraform:${TF_VERSION}
stage: apply
script:
- terraform init
- terraform apply tfplan
environment:
name: production
when: manual
only:
- main
dependencies:
- terraform-plan
// Jenkinsfile
pipeline {
agent any
environment {
AWS_REGION = 'us-east-1'
TERRAFORM_VERSION = '1.6.0'
}
stages {
stage('Checkout') {
steps {
checkout scm
}
}
stage('Terraform Init') {
steps {
dir('terraform') {
sh '''
terraform init
terraform workspace select ${WORKSPACE_ENV} || terraform workspace new ${WORKSPACE_ENV}
'''
}
}
}
stage('Terraform Validate') {
steps {
dir('terraform') {
sh 'terraform validate'
}
}
}
stage('Terraform Plan') {
steps {
dir('terraform') {
sh 'terraform plan -out=tfplan'
}
}
}
stage('Terraform Apply') {
when {
branch 'main'
}
steps {
dir('terraform') {
sh 'terraform apply -auto-approve tfplan'
}
}
}
}
post {
always {
cleanWs()
}
}
}
azure-pipelines.yml
trigger:
- main
pr:
- main
variables:
terraformVersion: '1.6.0'
azureServiceConnection: 'azure-sp'
resourceGroupName: 'rg-terraform'
storageAccountName: 'stterraformstate'
containerName: 'tfstate'
stages:
- stage: Validate
displayName: 'Validate'
jobs:
- job: terraform_validate
displayName: 'Terraform Validate'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: TerraformInstaller@1
inputs:
terraformVersion: $(terraformVersion)
- task: TerraformTaskV4@4
inputs:
provider: 'aws'
command: 'validate'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
- task: TerraformTaskV4@4
inputs:
provider: 'aws'
command: 'init'
backendServiceAWS: '$(awsServiceConnection)'
backendAWSBucketName: '$(storageAccountName)'
backendAWSKey: '$(Build.Repository.Name)/terraform.tfstate'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
- stage: Plan
displayName: 'Plan'
jobs:
- job: terraform_plan
displayName: 'Terraform Plan'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: TerraformInstaller@1
inputs:
terraformVersion: $(terraformVersion)
- task: TerraformTaskV4@4
inputs:
provider: 'aws'
command: 'plan'
commandOptions: '-out=tfplan'
environmentServiceNameAWS: '$(awsServiceConnection)'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
- stage: Apply
displayName: 'Apply'
dependsOn: Plan
condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main'))
jobs:
- job: terraform_apply
displayName: 'Terraform Apply'
pool:
vmImage: 'ubuntu-latest'
steps:
- task: TerraformInstaller@1
inputs:
terraformVersion: $(terraformVersion)
- task: TerraformTaskV4@4
inputs:
provider: 'aws'
command: 'apply'
commandOptions: '-auto-approve tfplan'
environmentServiceNameAWS: '$(awsServiceConnection)'
workingDirectory: '$(System.DefaultWorkingDirectory)/terraform'
.pre-commit-config.yaml
repos:
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.85.0
hooks:
- id: terraform_fmt
- id: terraform_validate
- id: terraform_docs
args: ['--args', '--sort-by-required=false']
- id: terraform_tfsec
args: ['--args', '--exclude-downloaded-modules']
- id: tfupdate
exclude: ^examples/
┌─────────────────────────────────────────────────────────────────────────────┐
│ Drift Detection Workflow │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ Scheduled Job │ │
│ │ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Schedule │ (daily/hourly) │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │terraform │ │ │
│ │ │ plan │ Compare state vs. reality │ │
│ │ └──────┬──────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Drift? │──No──▶ Continue │ │
│ │ └──────┬──────┘ │ │
│ │ │Yes │ │
│ │ ▼ │ │
│ │ ┌─────────────┐ │ │
│ │ │ Alert/ │ (Slack, PagerDuty, Email) │ │
│ │ │ Notify │ │ │
│ │ └─────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────┘
drift-detect.sh
#!/bin/bash
cd terraform
terraform init -backend=false
PLAN_OUTPUT=$(terraform plan -detailed-exitcode 2>&1)
EXIT_CODE=$?
if [ $EXIT_CODE -eq 2 ]; then
# Changes detected - drift exists
echo "DRIFT DETECTED!"
echo "$PLAN_OUTPUT"
# Send alert
curl -X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "{\"text\": \"⚠️ Terraform drift detected in $ENVIRONMENT\n\n$PLAN_OUTPUT\"}"
exit 1
elif [ $EXIT_CODE -eq 0 ]; then
echo "No drift detected"
exit 0
else
echo "Error running terraform plan"
exit 1
fi

In this chapter, you learned:

  • CI/CD Overview: Pipeline stages for Terraform
  • GitHub Actions: Workflow for GitHub-based CI/CD
  • GitLab CI: Pipeline configuration for GitLab
  • Jenkins: Pipeline script for Jenkins
  • Azure DevOps: YAML pipeline for Azure
  • Pre-commit Hooks: Automated validation
  • Drift Detection: Monitoring for infrastructure changes