In this blog post, we will look at how to implement a multi-account deployment pipeline on AWS using GitHub Actions and Terraform.
We will assume that you have access to at least two AWS accounts: one to hold pipeline resources and one target account where resources will be deployed.
Architecture
Fig 1. Architecture
We will use two accounts: a pipeline account and a target account. The target account is your dev/staging/prod account. Usually, there is more than one target account in a given pipeline, but we will use one for simplicity. The same approach can be extended to an arbitrary number of target accounts.
To avoid managing AWS credentials in GitHub, we will use the GitHub OIDC provider and a role that is assumed by the workflow.
In the pipeline account, we will deploy:
An S3 bucket to hold Terraform state
A DynamoDB table for state locking
A GitHub OIDC provider
A workflow role with access to Terraform backend and the target account role.
In the target account, we will deploy:
A deployment role with access to deploy resources.
We will use a CloudFormation template to define resources to prevent exposing programmatic access to the pipeline and target accounts. Deployed resources are not expected to change much, but feel free to translate them into Terraform files.
Target Account Deployment Role
The deployment role in the target account grants permissions to deploy resources. In our example, it will have admin permissions, but you can limit permissions based on your workflow requirements. The role principal is the pipeline account, assumed by the workflow role.
Transform:AWS::Serverless-2016-10-31Parameters:PipelinesAccountId:Description:Account id of pipelines accountType:StringResources:AssumeAdminPermissionsRole:Type:AWS::IAM::RoleProperties:Description:Role to be assumed by pipelines account users to deploy resources.RoleName:target-account-deployment-roleAssumeRolePolicyDocument:Version:"2012-10-17"Statement:- Effect:AllowPrincipal:AWS:!Sub "arn:aws:iam::${PipelinesAccountId}:root"Action:"sts:AssumeRole"ManagedPolicyArns:- arn:aws:iam::aws:policy/AdministratorAccessOutputs:TargetAccountDeploymentRoleArn:Description:Role arn that grants access to the pipelinesValue:!GetAtt AssumeAdminPermissionsRole.Arn
Pipelines Account Resources
First, we will define the Terraform S3 bucket, DynamoDB table, and GitHub OIDC provider using the CloudFormation template provided below.
AWSTemplateFormatVersion:"2010-09-09"Resources:######################################### TERRAFORM STATE BUCKET AND LOCK TABLE########################################TerraformStateBucket:Type:AWS::S3::BucketProperties:BucketEncryption:ServerSideEncryptionConfiguration:- ServerSideEncryptionByDefault:SSEAlgorithm:AES256VersioningConfiguration:Status:EnabledTerraformLockTable:Type:AWS::DynamoDB::TableProperties:TableName:"tf-state-table"BillingMode:PAY_PER_REQUESTPointInTimeRecoverySpecification:PointInTimeRecoveryEnabled:trueAttributeDefinitions:- AttributeName:LockIDAttributeType:SKeySchema:- AttributeName:LockIDKeyType:HASHSSESpecification:SSEEnabled:true######################################### Github Oidc Provider########################################GithubOidc:Type:AWS::IAM::OIDCProviderProperties:Url:https://token.actions.githubusercontent.comClientIdList:- sts.amazonaws.comThumbprintList:- ffffffffffffffffffffffffffffffffffffffffOutputs:TerraformStateBucketName:Value:!Ref TerraformStateBucketDescription:Name of terraform state S3 bucketTerraformLockTableName:Value:!Ref TerraformLockTableDescription:Name of the DynamoDB lock tableGithubOidcArn:Value:!Ref GithubOidcDescription:Arn of github oidc provider
To deploy the workflow role, pass the information from the previous stack and GitHub details as parameters. The role is restricted to a single repository and the main branch, but you can customize it according to your setup.
If you have more than one target account, provide the account IDs as a comma-separated list.
name:Example pipelineon:push:branches:[main ]workflow_dispatch:{}env:AWS_REGION :"<REGION>"AWS_PIPELINES_ROLE_ARN:"<WORKFLOW ROLE ARN>"# Permission can be added at job level or workflow levelpermissions:id-token:write # This is required for requesting the JWTcontents:read # This is required for actions/checkoutjobs:AssumeRoleAndCallIdentity:runs-on:ubuntu-lateststeps:- name:clone the repositoryuses:actions/checkout@v4- name:configure aws credentialsuses:aws-actions/configure-aws-credentials@v4with:role-to-assume:${{ env.AWS_PIPELINES_ROLE_ARN }}aws-region:${{ env.AWS_REGION }}- name:setup terraformuses:hashicorp/setup-terraform@v2- name:apply terraform configurationrun:| terraform init
terraform workspace new sandbox || true
terraform workspace select sandbox
terraform plan
terraform apply -auto-approve
# terraform destroy -auto-approve
To execute Terraform commands, we assume the workflow role. This role grants access to the Terraform S3 backend and to the target account deployment role.
The deployment role is used in the AWS provider configuration shown below.
Since GitHub stores workflows in the same directory as the infrastructure, don’t forget to set up appropriate rule sets to improve security posture. If possible, limit file modifications to workflows, especially if you have a production workflow defined.
Conclusion
In this blog post, we have explored how to create a simple multi-account deployment pipeline on AWS using GitHub Actions and Terraform.