Deploying Infrastructure to AWS with Terraform and GitHub Actions

In this guide, we’ll walk through the process of securely deploying infrastructure to AWS using Terraform and GitHub Actions. This pipeline demonstrates how to create a reusable template for planning and applying Terraform configurations, while maintaining security and efficiency.

Prerequisites

Before proceeding, ensure you have the following:

  1. AWS Access Key and Secret Key: You’ll need an AWS Access Key and Secret Key to authenticate with AWS services. Follow this guide to create them.
  2. GitHub Secrets: Add your AWS Access Key and Secret Key as secrets in your GitHub repository. These secrets will be used by GitHub Actions to authenticate with AWS.

Pipeline Overview

The pipeline consists of two main stages: planning (terraform-plan.yml) and applying (terraform-apply.yml). Each stage utilizes reusable templates and GitHub Actions to automate the Terraform deployment process.

Code Explanation

terraform-plan.yml

This YAML file defines the workflow for planning Terraform configurations. It accepts input parameters such as the Terraform root path, version, TFVARS file, AWS backend settings, and AWS credentials as secrets.

name: "TF_Plan"

on:
  workflow_call:
    inputs:
      path:
        description: "Terraform Root Path"
        required: true
        type: string
      tf_version:
        description: 'Terraform Version. e.g: 1.3.0 Default=latest.'
        required: false
        type: string
        default: latest
      tf_vars_file:
        description: 'Terraform TFVARS file name.'
        required: true
        type: string
      aws_backend_bucket_name:
        description: 'AWS S3 bucket name to store terraform state file.'
        required: true
        type: string
      aws_backend_bucket_prefix:
        description: 'AWS S3 bucket folder name to store terraform state file.'
        required: true
        type: string
      aws_backend_region:
        description: 'AWS S3 bucket region'
        required: true
        type: string
      aws_backend_dynamodb_table:
        description: 'AWS dynamodb table name'
        required: true
        type: string
      aws_backend_encrypt:
        description: 'AWS storage encryption'
        required: true
        type: boolean
      # environment:
      #   description: 'manual approvals in GitHub Actions with the Environments.'
      #   required: true
      #   type: string
    secrets:
      AWS_ACCESS_KEY_ID:
        description: 'AWS Access key ID'
        required: true
      AWS_SECRET_ACCESS_KEY:
        description: 'AWS Secret Access Key'
        required: true

jobs:
  build-apply:
    runs-on: ubuntu-latest
    # environment: ${{ inputs.environment }}
    
    defaults:
      run:
        shell: bash
        working-directory: ${{ inputs.path }}

    env:
      aws_backend_bucket_name: ${{ inputs.aws_backend_bucket_name }}
      aws_backend_bucket_prefix: ${{ inputs.aws_backend_bucket_prefix }}
      aws_backend_region: ${{ inputs.aws_backend_region }}
      aws_backend_dynamodb_table: ${{ inputs.aws_backend_dynamodb_table }}
      aws_backend_encrypt: ${{ inputs.aws_backend_encrypt }}
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      TF_VARS: ${{ inputs.tf_vars_file }}
    
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: tfsec
      uses: aquasecurity/[email protected]
      with:
        version: latest
        soft_fail: true 

    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ inputs.tf_version }}
     
    - name: Terraform Init
      run: terraform init --backend-config="bucket=$aws_backend_bucket_name" --backend-config="key=$aws_backend_bucket_prefix" -backend-config="region=$aws_backend_region" -backend-config="dynamodb_table=$aws_backend_dynamodb_table" -backend-config="encrypt=$aws_backend_encrypt"
      
    - name: Terraform Validate
      run: terraform validate 
      
    - name: Terraform Plan
      id: plan
      run: terraform plan --var-file=$TF_VARS --out=plan.tfplan
      continue-on-error: true
     
    - name: Terraform Plan Status
      if: steps.plan.outcome == 'failure'
      run: exit 1

    - name: Compress TF Plan artifact
      run: zip -r ${{ inputs.aws_backend_bucket_name }}.zip ./*

    - name: Upload Artifact
      uses: actions/[email protected]
      with:
        name: "${{ inputs.aws_backend_bucket_name }}"
        path: "${{ inputs.path }}/${{ inputs.aws_backend_bucket_name }}.zip"
        retention-days: 5

terraform-apply.yml

Similar to the previous file, this YAML file handles the application of Terraform configurations. It downloads the artifact generated during the planning stage and applies the changes to the infrastructure.

name: "TF_Apply"

on:
  workflow_call:
    inputs:
      path:
        description: "Terraform Root Path"
        required: true
        type: string
      tf_version:
        description: 'Terraform Version. e.g: 1.3.0 Default=latest.'
        required: false
        type: string
        default: latest
      tf_vars_file:
        description: 'Terraform TFVARS file name.'
        required: true
        type: string
      aws_backend_bucket_name:
        description: 'GCP Storage bucket name to store terraform state file.'
        required: true
        type: string
      aws_backend_bucket_prefix:
        description: 'GCP Storage bucket folder name to store terraform state file.'
        required: true
        type: string
      aws_backend_region:
        description: 'GCP Storage bucket folder name to store terraform state file.'
        required: true
        type: string
      aws_backend_dynamodb_table:
        description: 'GCP Storage bucket folder name to store terraform state file.'
        required: true
        type: string
      aws_backend_encrypt:
        description: 'AWS storage encryption'
        required: true
        type: boolean
        default: false
      # environment:
      #   description: 'manual approvals in GitHub Actions with the Environments.'
      #   required: true
      #   type: string
    secrets:
      AWS_ACCESS_KEY_ID:
        description: 'AWS Access key ID'
        required: true
      AWS_SECRET_ACCESS_KEY:
        description: 'AWS Secret Access Key'
        required: true

jobs:
  build-apply:
    runs-on: ubuntu-latest
    # environment: ${{ inputs.environment }}
    
    defaults:
      run:
        shell: bash
        working-directory: ${{ inputs.path }}

    env:
      aws_backend_bucket_name: ${{ inputs.aws_backend_bucket_name }}
      aws_backend_bucket_prefix: ${{ inputs.aws_backend_bucket_prefix }}
      aws_backend_region: ${{ inputs.aws_backend_region }}
      aws_backend_dynamodb_table: ${{ inputs.aws_backend_dynamodb_table }}
      aws_backend_encrypt: ${{ inputs.aws_backend_encrypt }}
      AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
      AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
      TF_VARS: ${{ inputs.tf_vars_file }}
    
    steps:
    - name: Checkout
      uses: actions/checkout@v4

    - name: Download Artifact
      uses: actions/[email protected]
      with:
        name: ${{ inputs.aws_backend_bucket_name }}
        path: ${{ inputs.path }}

    - name: Decompress TF Plan artifact
      run: echo "y" | unzip -o ${{ inputs.aws_backend_bucket_name }}.zip
      
    - name: Setup Terraform
      uses: hashicorp/setup-terraform@v3
      with:
        terraform_version: ${{ inputs.tf_version }}
    
    - name: Terraform Init
      run: terraform init --backend-config="bucket=$aws_backend_bucket_name" --backend-config="key=$aws_backend_bucket_prefix" -backend-config="region=$aws_backend_region" -backend-config="dynamodb_table=$aws_backend_dynamodb_table" -backend-config="encrypt=$aws_backend_encrypt"

    - name: Terraform Apply
      run: terraform apply plan.tfplan 
pipeline.yml

The main pipeline file (pipeline.yml) orchestrates the entire process. It calls the planning and applying stages sequentially, passing the necessary parameters and secrets.

name: 'Infra_build'

on:
  push:
    branches:
      - main
  pull_request:

permissions:
  contents: read

jobs:
    Dev_Plan:
        uses: littleworks-inc/aws_terraform_demo/.github/workflows/terraform-pan.yml@main
        with:
            path: .
            tf_version: latest
            aws_backend_bucket_name: devopsdemobucket01
            aws_backend_bucket_prefix: state/terraform.tfstate
            aws_backend_region: ca-central-1
            aws_backend_dynamodb_table: devtoolhub
            aws_backend_encrypt: true
            tf_vars_file: dev.tfvars 
            # environment: dev
        secrets:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

    Dev_Deploy:
        needs: Dev_Plan
        uses: littleworks-inc/aws_terraform_demo/.github/workflows/terraform-apply.yml@main
        with:
          path: .
          tf_version: latest
          aws_backend_bucket_name: devopsdemobucket01
          aws_backend_bucket_prefix: state/terraform.tfstate
          aws_backend_region: ca-central-1
          aws_backend_dynamodb_table: devtoolhub
          aws_backend_encrypt: true
          tf_vars_file: dev.tfvars 
          # environment: dev
        secrets:
          AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
          AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

Demo

To see this pipeline in action, visit the GitHub repository. Fork the repository and explore the code to understand how Terraform and GitHub Actions are integrated to deploy infrastructure securely on AWS.

Conclusion

Deploying infrastructure to AWS using Terraform and GitHub Actions offers a robust and automated approach. By following the steps outlined in this guide, you can streamline your deployment process and maintain a high level of security.

Start deploying your infrastructure efficiently with Terraform and GitHub Actions today!