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 theDevenvironment - Once tests have passed, merge the pull request - Deploy Action will run against the
Devenvironment - When happy with what’s deployed in
Dev, a pull request is created fromdevtomaster- Integration tests Action will run against theProdenvironment - Once tests have passed, merge the pull request - Deploy Action will run against the
Prodenvironment
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
.tfvarsfile
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
.tfvarsfile
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 theDevenvironment - Once tests have passed, merge the pull request - Deploy Action will run against the
Devenvironment - When happy with what’s deployed in
Dev, a pull request is created fromdevtomaster- Integration tests Action will run against theProdenvironment - Once tests have passed, merge the pull request - Deploy Action will run against the
Prodenvironment