Continuous Integration and Deployment (CI/CD) for Terraform on AWS

Learn how to implement CI/CD pipelines for Terraform using AWS CodePipeline, GitHub Actions, and best practices for Infrastructure as Code automation

Continuous Integration and Deployment (CI/CD) for Terraform on AWS

Implementing CI/CD for Infrastructure as Code (IaC) is crucial for maintaining consistent and reliable infrastructure deployments. This guide demonstrates how to set up CI/CD pipelines for Terraform using various tools and best practices.

Video Tutorial

Learn more about implementing CI/CD with Terraform in AWS in this comprehensive video tutorial:

Prerequisites

  • AWS CLI configured with appropriate permissions
  • Terraform installed (version 1.0.0 or later)
  • GitHub account
  • Basic understanding of CI/CD concepts

Project Structure

terraform-cicd/
├── .github/
│   └── workflows/
│       ├── terraform-plan.yml
│       └── terraform-apply.yml
├── modules/
│   └── vpc/
│       ├── main.tf
│       ├── variables.tf
│       └── outputs.tf
├── environments/
│   ├── dev/
│   │   ├── main.tf
│   │   ├── variables.tf
│   │   └── terraform.tfvars
│   └── prod/
│       ├── main.tf
│       ├── variables.tf
│       └── terraform.tfvars
├── scripts/
│   ├── terraform-init.sh
│   └── terraform-plan.sh
└── README.md

GitHub Actions Workflow

Create .github/workflows/terraform-plan.yml:

name: 'Terraform Plan'

on:
  pull_request:
    branches:
      - main
    paths:
      - '**.tf'
      - '**.tfvars'
      - '.github/workflows/terraform-*.yml'

jobs:
  terraform-plan:
    name: 'Terraform Plan'
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        environment: [dev, prod]
        
    defaults:
      run:
        working-directory: environments/${{ matrix.environment }}

    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_REGION: us-west-2
      TERRAFORM_VERSION: 1.5.0

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}

      - name: Terraform Format
        run: terraform fmt -check -recursive

      - name: Terraform Init
        run: terraform init

      - name: Terraform Validate
        run: terraform validate

      - name: Terraform Plan
        run: terraform plan -no-color
        continue-on-error: true

      - name: Update Pull Request
        uses: actions/github-script@v6
        if: github.event_name == 'pull_request'
        env:
          PLAN: "terraform\n${{ steps.plan.outputs.stdout }}"
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const output = `#### Terraform Plan 📝
            *Environment: ${{ matrix.environment }}*
            
            <details><summary>Show Plan</summary>
            
            \`\`\`\n
            ${process.env.PLAN}
            \`\`\`
            
            </details>`;
              
            github.rest.issues.createComment({
              issue_number: context.issue.number,
              owner: context.repo.owner,
              repo: context.repo.repo,
              body: output
            })

Create .github/workflows/terraform-apply.yml:

name: 'Terraform Apply'

on:
  push:
    branches:
      - main
    paths:
      - '**.tf'
      - '**.tfvars'
      - '.github/workflows/terraform-*.yml'

jobs:
  terraform-apply:
    name: 'Terraform Apply'
    runs-on: ubuntu-latest
    
    strategy:
      matrix:
        environment: [dev, prod]
        
    defaults:
      run:
        working-directory: environments/${{ matrix.environment }}

    env:
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      AWS_REGION: us-west-2
      TERRAFORM_VERSION: 1.5.0

    steps:
      - name: Checkout
        uses: actions/checkout@v3

      - name: Setup Terraform
        uses: hashicorp/setup-terraform@v2
        with:
          terraform_version: ${{ env.TERRAFORM_VERSION }}

      - name: Terraform Init
        run: terraform init

      - name: Terraform Apply
        if: matrix.environment == 'dev' || github.ref == 'refs/heads/main'
        run: terraform apply -auto-approve

AWS CodePipeline Configuration

Create aws-pipeline.tf:

# S3 bucket for artifacts
resource "aws_s3_bucket" "artifacts" {
  bucket = "${var.project_name}-terraform-artifacts"

  tags = {
    Name        = "${var.project_name}-terraform-artifacts"
    Environment = var.environment
  }
}

# CodeBuild project
resource "aws_codebuild_project" "terraform" {
  name          = "${var.project_name}-terraform-build"
  description   = "Terraform build project"
  build_timeout = "30"
  service_role  = aws_iam_role.codebuild.arn

  artifacts {
    type = "CODEPIPELINE"
  }

  environment {
    compute_type                = "BUILD_GENERAL1_SMALL"
    image                      = "aws/codebuild/amazonlinux2-x86_64-standard:3.0"
    type                       = "LINUX_CONTAINER"
    image_pull_credentials_type = "CODEBUILD"

    environment_variable {
      name  = "TERRAFORM_VERSION"
      value = "1.5.0"
    }
  }

  source {
    type      = "CODEPIPELINE"
    buildspec = "buildspec.yml"
  }

  tags = {
    Name        = "${var.project_name}-terraform-build"
    Environment = var.environment
  }
}

# CodePipeline
resource "aws_codepipeline" "terraform" {
  name     = "${var.project_name}-terraform-pipeline"
  role_arn = aws_iam_role.codepipeline.arn

  artifact_store {
    location = aws_s3_bucket.artifacts.bucket
    type     = "S3"
  }

  stage {
    name = "Source"

    action {
      name             = "Source"
      category         = "Source"
      owner            = "AWS"
      provider         = "CodeStarSourceConnection"
      version          = "1"
      output_artifacts = ["source_output"]

      configuration = {
        ConnectionArn    = aws_codestarconnections_connection.github.arn
        FullRepositoryId = "${var.github_owner}/${var.github_repo}"
        BranchName      = "main"
      }
    }
  }

  stage {
    name = "Plan"

    action {
      name            = "Plan"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      input_artifacts = ["source_output"]
      version         = "1"

      configuration = {
        ProjectName = aws_codebuild_project.terraform.name
        EnvironmentVariables = jsonencode([
          {
            name  = "TERRAFORM_COMMAND"
            value = "plan"
            type  = "PLAINTEXT"
          }
        ])
      }
    }
  }

  stage {
    name = "Approve"

    action {
      name     = "Approve"
      category = "Approval"
      owner    = "AWS"
      provider = "Manual"
      version  = "1"
    }
  }

  stage {
    name = "Apply"

    action {
      name            = "Apply"
      category        = "Build"
      owner           = "AWS"
      provider        = "CodeBuild"
      input_artifacts = ["source_output"]
      version         = "1"

      configuration = {
        ProjectName = aws_codebuild_project.terraform.name
        EnvironmentVariables = jsonencode([
          {
            name  = "TERRAFORM_COMMAND"
            value = "apply"
            type  = "PLAINTEXT"
          }
        ])
      }
    }
  }
}

BuildSpec Configuration

Create buildspec.yml:

version: 0.2

phases:
  install:
    runtime-versions:
      python: 3.9
    commands:
      - curl -s -qL -o terraform.zip "https://releases.hashicorp.com/terraform/${TERRAFORM_VERSION}/terraform_${TERRAFORM_VERSION}_linux_amd64.zip"
      - unzip terraform.zip
      - mv terraform /usr/local/bin/
      - chmod +x /usr/local/bin/terraform

  pre_build:
    commands:
      - cd environments/${ENVIRONMENT}
      - terraform init

  build:
    commands:
      - |
        if [ "${TERRAFORM_COMMAND}" = "plan" ]; then
          terraform plan -no-color
        elif [ "${TERRAFORM_COMMAND}" = "apply" ]; then
          terraform apply -auto-approve -no-color
        fi

  post_build:
    commands:
      - echo "Terraform ${TERRAFORM_COMMAND} completed on $(date)"

artifacts:
  files:
    - environments/${ENVIRONMENT}/**/*
    - modules/**/*
  name: terraform-artifacts

Testing and Validation

  1. Pre-Commit Hooks

Create .pre-commit-config.yaml:

repos:
  - repo: https://github.com/antonbabenko/pre-commit-terraform
    rev: v1.77.1
    hooks:
      - id: terraform_fmt
      - id: terraform_docs
      - id: terraform_tflint
      - id: terraform_validate

  - repo: https://github.com/pre-commit/pre-commit-hooks
    rev: v4.4.0
    hooks:
      - id: check-merge-conflict
      - id: end-of-file-fixer
      - id: trailing-whitespace
      - id: check-yaml
  1. Unit Testing

Create test/main_test.go:

package test

import (
    "testing"
    "github.com/gruntwork-io/terratest/modules/terraform"
    "github.com/stretchr/testify/assert"
)

func TestTerraformBasicExample(t *testing.T) {
    terraformOptions := &terraform.Options{
        TerraformDir: "../environments/dev",
        Vars: map[string]interface{}{
            "environment": "test",
        },
    }

    defer terraform.Destroy(t, terraformOptions)
    terraform.InitAndApply(t, terraformOptions)

    output := terraform.Output(t, terraformOptions, "vpc_id")
    assert.NotEmpty(t, output)
}

Best Practices

  1. State Management

    • Use remote state (S3 + DynamoDB)
    • Enable state locking
    • Implement state backups
  2. Security

    • Use IAM roles with least privilege
    • Encrypt sensitive variables
    • Implement proper access controls
  3. Code Organization

    • Use consistent structure
    • Implement proper versioning
    • Document changes

Deployment Strategies

  1. Progressive Deployment

    • Deploy to dev first
    • Automated testing
    • Manual approval for prod
  2. Environment Separation

    • Separate state files
    • Environment-specific variables
    • Isolated IAM roles
  3. Rollback Strategy

    • State backups
    • Version control
    • Automated rollback

Monitoring and Alerting

  1. Pipeline Monitoring

    • CloudWatch metrics
    • SNS notifications
    • Slack integration
  2. Infrastructure Changes

    • Resource tracking
    • Cost monitoring
    • Compliance checks

Conclusion

You’ve learned how to implement CI/CD for Terraform on AWS. This setup provides:

  • Automated infrastructure deployment
  • Consistent testing and validation
  • Secure deployment process
  • Proper change management

Remember to:

  • Follow security best practices
  • Implement proper testing
  • Document your pipeline
  • Monitor deployments