Deploy AWS Dev/Prod via Terraform & Github Actions

The Plan

In this post we will be going through the process of setting up automated AWS infrastructure deployments using Terraform and GitHub Actions. In this case we have both a Dev and a Prod environment in respective AWS accounts. We will be looking at how to deploy to different accounts and manage state across the accounts. To have separation of Dev and Prod environments there will be a long lived dev branch that will reflect the deployed state for the Dev environment, while the master branch reflects the deployed state for the Prod environment. The deployment model that will be used is:

  • Create a branch from dev
  • Make changes in new branch
  • Create a pull request to dev - Integration tests Action will run against the Dev environment
  • Once tests have passed, merge the pull request - Deploy Action will run against the Dev environment
  • When happy with what’s deployed in Dev, a pull request is created from dev to master - Integration tests Action will run against the Prod environment
  • Once tests have passed, merge the pull request - Deploy Action will run against the Prod environment

Setting up GitHub Project

There are a few things we need to do to get our GitHub project setup for GitHub actions to be able to authenticate with our AWS accounts.

Setup Access Keys for AWS Accounts

For each AWS account, in this case the Prod and Dev account, we will need to create an IAM user with programmatic access. I have simply named the user terraform. Take note of the Access key and Secret key for the IAM user as this is what the GitHub Actions will use to authenticate Terraform with AWS.

The other thing we should do while signed into the AWS console is manually create an S3 bucket in each account that will be used to store AWS state. Take note of the bucket name as well.

GitHub Secrets

Once you have the Access Key, Secret Key and S3 Bucket name for each AWS account. You will need to add these values as Secrets in your GitHub project. I have suffixed each secret with the environment they belong to:

Setting up Terraform

There are a few things we will need to do here to allow our deployments to work across multiple AWS account/environments

Setting up Terraform State in AWS

In our main Terraform file we will need to define both the provider and backend to be AWS and s3 respectively. Of particular note is that the Bucket property for the backend definition is not provided. We will be passing this in via out GitHub actions. This is because buckets need to have globally unique names, so the bucket in each account will have a different name.

provider "aws" {
  version = "~> 2.0"
  region  = "ap-southeast-2"
}

terraform {
  backend "s3" {
    key    = "terraform-state-key"
    region = "ap-southeast-2"
  }
}

Configuring Terraform Variables per Environment

Every Terraform variable needs to be defined in a variables.tf in the same directory as your main Terraform file. You can set this up with default values that you can then replace with environment specific values. My variables.tf looks something like this:

variable "environment" {
    type = string
    default = "dev"
}

variable "variable123" {
    type = string
    default = "placeholder"
}

To replace these variable with environment specific values we need to create some .tfvars files, one for each environment. In this case I have both a env/dev.tfvars and a env/prod.tfvars. We will configure the GitHub action to pass Terraform the respective .tfvars file for each environment to replace the default values set in the variables.tf file. These .tfvars files look something like this:

environment = "prod"
variable123 = "variable123-prod-value"

Creating GitHub Actions for Integration Testing

For the integration tests we will need a separate action for dev and prod respectively. The actions will be almost identical and run on a pull request to their respective branch. The action will also pass in the environment specific value for:

  • AWS Access and Secret key
  • S3 State bucket for Terraform
  • Environment specific .tfvars file

The action for dev looks something like the below, where prod would be the same just with the references to Dev updated for Prod.

name: Pull Request

on:
  pull_request:
    branches:
      - dev

jobs:
  Terraform:
    name: Terraform Plan
    runs-on: ubuntu-latest
    steps:

    - name: Checkout Repo
      uses: actions/checkout@v2
    
    - name: Terraform Setup
      uses: hashicorp/setup-terraform@v1

    - name: Terraform Init
      run: terraform init
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: '.'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
        TF_CLI_ARGS: '-var-file="env/dev.tfvars" -backend-config="bucket=${{ secrets.STATE_BUCKET_DEV }}" '

    - name: Terraform Validate
      run: terraform validate 

    - name: Terraform Plan
      run: terraform plan
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: '.'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
        TF_CLI_ARGS: '-var-file="env/dev.tfvars"'

Creating GitHub Actions for the Deployment

For the Deployment, much like the integration tests, we will need a separate action for dev and prod respectively. Again, the actions will be almost identical and run on a push to their respective branch. The action will also pass in the environment specific value for:

  • AWS Access and Secret key
  • S3 State bucket for Terraform
  • Environment specific .tfvars file

The action for dev looks something like the below, where prod would be the same just with the references to Dev updated for Prod.

name: Deploy

on:
  push:
    branches:
      - dev

jobs:
  Terraform:
    name: Terraform Apply
    runs-on: ubuntu-latest
    steps:

    - name: Checkout Repo
      uses: actions/checkout@v2
    
    - name: Terraform Setup
      uses: hashicorp/setup-terraform@v1

    - name: Terraform Init
      run: terraform init
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: '.'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
        TF_CLI_ARGS: '-var-file="env/dev.tfvars" -backend-config="bucket=${{ secrets.STATE_BUCKET_DEV }}" '

    - name: Terraform Validate
      run: terraform validate 

    - name: Terraform Apply
      run: terraform apply -auto-approve
      env:
        GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        TF_ACTION_WORKING_DIR: '.'
        AWS_ACCESS_KEY_ID:  ${{ secrets.AWS_ACCESS_KEY_ID_DEV }}
        AWS_SECRET_ACCESS_KEY:  ${{ secrets.AWS_SECRET_ACCESS_KEY_DEV }}
        TF_CLI_ARGS: '-var-file="env/dev.tfvars"'

Putting it all Together

Now that we have some GitHub action created and some Terraform to deploy. The whole thing can be put together by:

  • Create a branch from dev
  • Make changes in new branch
  • Create a pull request to dev - Integration tests Action will run against the Dev environment
  • Once tests have passed, merge the pull request - Deploy Action will run against the Dev environment
  • When happy with what’s deployed in Dev, a pull request is created from dev to master - Integration tests Action will run against the Prod environment
  • Once tests have passed, merge the pull request - Deploy Action will run against the Prod environment