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 theDev
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 fromdev
tomaster
- Integration tests Action will run against theProd
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 theDev
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 fromdev
tomaster
- Integration tests Action will run against theProd
environment - Once tests have passed, merge the pull request - Deploy Action will run against the
Prod
environment