Terraform
Terraform Automation with CI/CD Pipelines
Learn how to automate Terraform deployments using CI/CD pipelines.
January 18, 2024
DevHub Team
5 min read
Terraform Automation with CI/CD Pipelines
Learn how to automate your infrastructure deployments by integrating Terraform with popular CI/CD platforms.
Why Automate Terraform?
- Consistent deployments
- Reduced human error
- Automated testing
- Version control integration
- Audit trail
CI/CD Pipeline Components
- Version Control
- Testing
- Plan Generation
- Approval Gates
- Deployment
- Validation
GitHub Actions Pipeline
# .github/workflows/terraform.yml name: Terraform CI/CD on: push: branches: [ main ] pull_request: branches: [ main ] jobs: terraform: runs-on: ubuntu-latest env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} TF_VAR_environment: ${{ github.ref == 'refs/heads/main' && 'prod' || 'dev' }} steps: - uses: actions/checkout@v3 - name: Setup Terraform uses: hashicorp/setup-terraform@v2 with: terraform_version: 1.0.0 - name: Terraform Format run: terraform fmt -check - name: Terraform Init run: terraform init - name: Terraform Validate run: terraform validate - name: Terraform Plan run: terraform plan -out=tfplan - name: Upload Plan uses: actions/upload-artifact@v3 with: name: tfplan path: tfplan - name: Terraform Apply if: github.ref == 'refs/heads/main' && github.event_name == 'push' run: terraform apply -auto-approve tfplan
GitLab CI Pipeline
# .gitlab-ci.yml image: hashicorp/terraform:1.0.0 variables: TF_VAR_environment: ${CI_COMMIT_REF_NAME} cache: paths: - .terraform stages: - validate - plan - apply before_script: - terraform init validate: stage: validate script: - terraform fmt -check - terraform validate plan: stage: plan script: - terraform plan -out=tfplan artifacts: paths: - tfplan apply: stage: apply script: - terraform apply -auto-approve tfplan only: - main when: manual
Azure DevOps Pipeline
# azure-pipelines.yml trigger: - main pool: vmImage: 'ubuntu-latest' variables: - group: terraform-secrets stages: - stage: Validate jobs: - job: ValidateAndPlan steps: - task: TerraformInstaller@0 inputs: terraformVersion: '1.0.0' - task: TerraformTaskV3@3 inputs: provider: 'aws' command: 'init' - task: TerraformTaskV3@3 inputs: provider: 'aws' command: 'plan' environmentServiceNameAWS: 'AWS-Connection' - stage: Deploy condition: and(succeeded(), eq(variables['Build.SourceBranch'], 'refs/heads/main')) jobs: - deployment: ApplyTerraform environment: 'production' strategy: runOnce: deploy: steps: - task: TerraformTaskV3@3 inputs: provider: 'aws' command: 'apply' environmentServiceNameAWS: 'AWS-Connection'
Jenkins Pipeline
// Jenkinsfile pipeline { agent any environment { TF_VAR_environment = "${BRANCH_NAME}" AWS_CREDENTIALS = credentials('aws-credentials') } stages { stage('Checkout') { steps { checkout scm } } stage('Terraform Init') { steps { sh 'terraform init' } } stage('Terraform Plan') { steps { sh 'terraform plan -out=tfplan' } } stage('Approval') { when { branch 'main' } steps { input message: 'Apply Terraform changes?' } } stage('Terraform Apply') { when { branch 'main' } steps { sh 'terraform apply -auto-approve tfplan' } } } post { always { cleanWs() } } }
CircleCI Pipeline
# .circleci/config.yml version: 2.1 orbs: terraform: circleci/terraform@3.0.0 workflows: version: 2 terraform: jobs: - terraform/fmt: checkout: true context: terraform - terraform/validate: checkout: true context: terraform requires: - terraform/fmt - terraform/plan: checkout: true context: terraform requires: - terraform/validate - terraform/apply: checkout: true context: terraform filters: branches: only: main requires: - terraform/plan
Best Practices
1. State Management
# backend.tf terraform { backend "s3" { bucket = "terraform-state" key = "env/${var.environment}/terraform.tfstate" region = "us-west-2" encrypt = true dynamodb_table = "terraform-locks" } }
2. Variable Management
# variables.tf variable "environment" { type = string description = "Environment name (dev/staging/prod)" } # terraform.tfvars.example environment = "dev" region = "us-west-2"
3. Workspace Separation
# Use workspaces for environment isolation terraform workspace new dev terraform workspace new prod terraform workspace select dev
Security Considerations
1. Secrets Management
# GitHub Actions secrets env: AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} TF_VAR_db_password: ${{ secrets.DB_PASSWORD }}
2. Access Control
# IAM policy for CI/CD resource "aws_iam_policy" "terraform_ci" { name = "terraform-ci-policy" policy = jsonencode({ Version = "2012-10-17" Statement = [ { Effect = "Allow" Action = [ "s3:GetObject", "s3:PutObject" ] Resource = "${aws_s3_bucket.terraform_state.arn}/*" } ] }) }
Testing in CI/CD
1. Unit Tests
# test/main_test.go package test import ( "testing" "github.com/gruntwork-io/terratest/modules/terraform" ) func TestTerraformBasicExample(t *testing.T) { terraformOptions := &terraform.Options{ TerraformDir: "../examples/basic", Vars: map[string]interface{}{ "environment": "test", }, } defer terraform.Destroy(t, terraformOptions) terraform.InitAndApply(t, terraformOptions) }
2. Policy Compliance
# policy/naming.rego package terraform deny[msg] { resource := input.planned_values.root_module.resources[_] not startswith(resource.values.tags.Name, "company-") msg = sprintf("Resource %v must have company prefix in Name tag", [resource.address]) }
Monitoring and Notifications
1. Slack Notifications
# GitHub Actions - name: Notify Slack uses: 8398a7/action-slack@v3 with: status: ${{ job.status }} fields: repo,message,commit,author,action,eventName,ref,workflow env: SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
2. Email Notifications
// Jenkins post { success { emailext subject: "Pipeline Successful", body: "Terraform changes applied successfully", to: "team@company.com" } failure { emailext subject: "Pipeline Failed", body: "Terraform pipeline failed. Please check logs.", to: "team@company.com" } }
Drift Detection
# Scheduled drift detection name: Terraform Drift Detection on: schedule: - cron: '0 0 * * *' jobs: detect-drift: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - name: Setup Terraform uses: hashicorp/setup-terraform@v2 - name: Terraform Plan run: | terraform init terraform plan -detailed-exitcode continue-on-error: true id: plan - name: Notify on Drift if: steps.plan.outcome == 'failure' run: | echo "Infrastructure drift detected!" # Add notification logic
Conclusion
Automating Terraform with CI/CD:
- Ensures consistent deployments
- Reduces human error
- Provides audit trail
- Enables automated testing
- Improves collaboration
Remember to:
- Use version control
- Implement proper testing
- Secure sensitive data
- Monitor deployments
- Handle state properly
IaC
DevOps
CI/CD
Automation