Automating AWS Infrastructure Provisioning with Terraform and GitHub Actions
Automating the provisioning of AWS infrastructure is essential for ensuring consistency and minimizing human errors during deployments. With Terraform and GitHub Actions, you can implement a Continuous Delivery (CD) pipeline that deploys to multiple environments (like staging and production) across different AWS accounts.
This blog post will walk you through the process of setting up such a pipeline. In this setup, Terraform State files are centrally managed within an AWS Administrative Account, utilizing S3 buckets for state storage and DynamoDB tables for state locking. The necessary AWS infrastructure services are then deployed to the respective AWS Environment Accounts.
Assumptions
- AWS Administrative Account (111111111111)
- AWS Environment Account for Staging (888888888888)
- AWS Environment Account for Production (999999999999)
The workflow is triggered by a push to specific Github branches (stg for staging and main for production). Based on the branch, it dynamically configures the environment variables and backend settings required to deploy the AWS services to the corresponding AWS account.
Create AWS IAM Roles
Administrative Account
IAM Role
terraform-admin-role (Terraform Administrative Account Role)
Trust Relationships
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"token.actions.githubusercontent.com:aud": "sts.amazonaws.com",
"token.actions.githubusercontent.com:sub": "repo:sarubhai/tf-aws-multi-account-deployment-gh-actions:*"
}
}
}
]
}
In order to setup AWS OIDC for Github to access AWS resources refer this article.
Attached IAM Permissions Policy
terraform-admin-policy (Terraform Administrative Account Policy)
- Policy to allow access to S3 and DynamoDB in the Administrative Account
- Policy to allow assuming the role in the Environment Accounts
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AccessTerraformState",
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": [
"arn:aws:s3:::iac-tf-state-gh-actions-dev",
"arn:aws:s3:::iac-tf-state-gh-actions-dev/*",
"arn:aws:s3:::iac-tf-state-gh-actions-pro",
"arn:aws:s3:::iac-tf-state-gh-actions-pro/*"
]
},
{
"Sid": "AccessTerraformStateLock",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:DeleteItem",
"dynamodb:UpdateItem"
],
"Resource": [
"arn:aws:dynamodb:eu-central-1:111111111111:table/iac-tf-state-lock-gh-actions-dev",
"arn:aws:dynamodb:eu-central-1:111111111111:table/iac-tf-state-lock-gh-actions-pro"
]
},
{
"Sid": "AssumeAWSEnvironmentAccountRole",
"Effect": "Allow",
"Action": "sts:AssumeRole",
"Resource": [
"arn:aws:iam::888888888888:role/terraform-env-role",
"arn:aws:iam::999999999999:role/terraform-env-role"
]
}
]
}
Environment Accounts
IAM Role
terraform-env-role (Role assumed by Terraform from Administrative Account Role)
Trust Relationships
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::111111111111:role/terraform-admin-role"
},
"Action": "sts:AssumeRole"
}
]
}
Attached IAM Permissions Policy
- Policy to allow access to relevant AWS Services to be provisioned via Terraform
- E.g. AWS managed policies (AmazonVPCFullAccess, AmazonEC2FullAccess, AmazonS3FullAccess, AmazonRDSFullAccess)
Terraform Configuration
# Provider Configuration
provider "aws" {
region = "eu-central-1"
assume_role {
role_arn = "arn:aws:iam::${var.aws_env_account}:role/terraform-env-role"
}
}
# Terraform Backend
terraform {
backend "s3" {
region = "eu-central-1"
key = "terraform.tfstate"
acl = "private"
encrypt = true
}
}
# Variables
variable "environment" {
description = "This environment tag will be included in the environment of the resources."
default = "dev"
}
variable "aws_env_account" {
description = "The AWS environment account to provision the resources."
default = "777777777777"
}
variable "vpc_cidr_block" {
description = "The address space that is used by the virtual network."
default = "10.0.0.0/16"
}
# VPC
resource "aws_vpc" "vpc" {
cidr_block = var.vpc_cidr_block
enable_dns_hostnames = true
enable_dns_support = true
tags = {
Name = "${var.environment}-vpc"
}
}
For complete terraform configuration file refer the Github repository.
Add Github Actions Secret
- AWS_ACCOUNT_ID_ADM: 111111111111
- AWS_ACCOUNT_ID_STG: 888888888888
- AWS_ACCOUNT_ID_PRO: 999999999999
- TFSTATE_BUCKET_STG: iac-tf-state-gh-actions-stg
- AWS_DYNAMODB_TABLE_STG: iac-tf-state-lock-gh-actions-stg
- TFSTATE_BUCKET_PRO: iac-tf-state-gh-actions-pro
- AWS_DYNAMODB_TABLE_PRO: iac-tf-state-lock-gh-actions-pro
GitHub Actions Workflow
name: Terraform-Deployment-Automation
on:
push:
branches:
- stg
- main
env:
AWS_REGION: "eu-central-1"
DEPLOYMENT_ENV: $\{\{ github.head_ref || github.ref_name }}
AWS_ACCOUNT_ID_ADM: '$\{\{ secrets.AWS_ACCOUNT_ID_ADM }}'
AWS_ACCOUNT_ID_STG: '$\{\{ secrets.AWS_ACCOUNT_ID_STG }}'
TFSTATE_BUCKET_STG: '$\{\{ secrets.TFSTATE_BUCKET_STG }}'
AWS_DYNAMODB_TABLE_STG: '$\{\{ secrets.AWS_DYNAMODB_TABLE_STG }}'
AWS_ACCOUNT_ID_PRO: '$\{\{ secrets.AWS_ACCOUNT_ID_PRO }}'
TFSTATE_BUCKET_PRO: '$\{\{ secrets.TFSTATE_BUCKET_PRO }}'
AWS_DYNAMODB_TABLE_PRO: '$\{\{ secrets.AWS_DYNAMODB_TABLE_PRO }}'
permissions:
id-token: write
contents: read
jobs:
terraform-automation-job:
runs-on: [ubuntu-latest]
steps:
- name: Check branch and corresponding deployment environment
run: |
echo "Branch & Environment To Deploy Artifacts"
echo ${DEPLOYMENT_ENV}
- name: Setup Stage Provider & Backend credentials
if: env.DEPLOYMENT_ENV == 'stg'
run: |
echo "Setup Stage Provider & Backend credentials"
echo "TF_VAR_environment=stg" >> $GITHUB_ENV
echo "TF_VAR_aws_env_account=${AWS_ACCOUNT_ID_STG}" >> $GITHUB_ENV
echo "TFSTATE_BUCKET=${TFSTATE_BUCKET_STG}" >> $GITHUB_ENV
echo "AWS_DYNAMODB_TABLE=${AWS_DYNAMODB_TABLE_STG}" >> $GITHUB_ENV
- name: Setup Production Provider & Backend credentials
if: env.DEPLOYMENT_ENV == 'main'
run: |
echo "Setup Production Provider & Backend credentials"
echo "TF_VAR_environment=pro" >> $GITHUB_ENV
echo "TF_VAR_aws_env_account=${AWS_ACCOUNT_ID_PRO}" >> $GITHUB_ENV
echo "TFSTATE_BUCKET=${TFSTATE_BUCKET_PRO}" >> $GITHUB_ENV
echo "AWS_DYNAMODB_TABLE=${AWS_DYNAMODB_TABLE_PRO}" >> $GITHUB_ENV
- name: Checkout local repo
uses: actions/checkout@v3
- name: configure aws credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::$\{\{ env.AWS_ACCOUNT_ID_ADM }}:role/terraform-admin-role
aws-region: $\{\{ env.AWS_REGION }}
disable-retry: "true"
- name: Setup Terraform
uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.9.5
- name: Terraform Init
run: terraform init -backend-config="bucket=${TFSTATE_BUCKET}" -backend-config="dynamodb_table=${AWS_DYNAMODB_TABLE}"
- name: Terraform Plan
run: terraform plan -no-color
continue-on-error: false
- name: Terraform Apply
run: terraform apply --auto-approve
- The workflow triggers on a push to the stg or main branches.
- Depending on the branch, the environment variables are set for the corresponding AWS Environment account, S3 bucket, and DynamoDB table. This ensures that the right environment is targeted during the Terraform deployment.
- The workflow first assumes the AWS administrative account role to access the Terraform state store by using temporary credentials.
- The workflow then assumes the role in the corresponding AWS environment account to manage infrastructure.
By automating AWS infrastructure provisioning with Terraform and GitHub Actions, you can ensure consistent, reliable, and efficient deployments to multiple environments. The use of centralized state management, environment-specific configuration, and secure handling of secrets further enhances the robustness of this solution.
Implementing this workflow in your CI/CD pipeline will significantly reduce manual errors, streamline deployments, and make your infrastructure as code (IaC) practice more resilient and scalable.