The Ultimate Multi-Account & Multi-Regional AWS Terraform Landing Zone
Building a secure, scalable, and battle-tested AWS Landing Zone using modern Terraform and GitHub Actions without static credentials.

The Challenge: Scaling Infrastructure Without Compromising Security
As software teams grow, managing cloud infrastructure becomes a delicate balancing act. In the early days, deploying everything to a single AWS account using a local machine and static IAM access keys might work. But as you scale, this approach introduces severe risks:
Massive Blast Radius: A simple mistake in a staging environment could accidentally take down your production database.
Static Key Leaks: Storing long-lived AWS Access Keys in GitHub Secrets or developer laptops is a ticking security time bomb.
Sluggish Deployments: Deploying regional infrastructure sequentially makes your CI/CD pipelines painfully slow.
State File Corruption: Sharing a single Terraform state file across different lifecycle stages eventually leads to state lock collisions and data loss.
To solve this, I built a production-ready, multi-account, multi-region Terraform AWS template that automates the provisioning of decoupled, secure, and parallelized cloud infrastructure. You can find the open-source template here: GitHub - Terraform AWS Template.
In this article, we’ll dive deep into its core architectural pillars and show you how to bootstrap it from scratch.
The Multi-Account Architecture
AWS best practices dictate that workloads should be segregated into distinct, isolated accounts. This template establishes a clean, decoupled boundary by separating responsibilities across four distinct AWS accounts:
1. Dev Account (envs/dev/)
Used exclusively by developers for local testing. It runs isolated, sandbox deployments via CLI commands, preventing experimental changes from impacting official staging or production pipelines.
2. Staging Account (envs/staging/)
Reflects production configuration. Code pushed to the main branch automatically deploys global and regional staging components.
3. Production Account (envs/prod/)
The highly guarded live environment. Deployments to production require a manual approval gate in GitHub Actions.
4. Shared State Account (bootstrap/state/)
A dedicated, minimal-privilege AWS account that hosts the central S3 state bucket.
Security Pillar 1: Passwordless AWS OIDC Federation
Storing static AWS access keys in CI/CD environments is one of the most common vectors for cloud security breaches.
This template completely eliminates persistent keys using GitHub Actions OpenID Connect (OIDC) Integration.
Instead of configuring secrets, the GitHub runner receives a short-lived, signed JSON Web Token (JWT) from GitHub. It presents this token to AWS, which verifies it against a configured IAM Identity Provider (OIDC Provider) and issues temporary, scoped-down IAM session credentials.
Here is what the trust policy looks like underneath:
module "github_actions_role" {
source = "terraform-aws-modules/iam/aws//modules/iam-role"
version = "~> 6.0"
name = "example-app-staging-github-actions"
enable_github_oidc = true
# Scopes trust strictly to your repository and target environment
oidc_subjects = ["your-github-username/your-repo:environment:staging"]
}
By scoping the trust configuration strictly to your-github-username/your-repo:environment:staging, you ensure that other repositories (even those running inside GitHub) cannot assume your deployment role.
Security Pillar 2: Prefix-Based State Isolation
Having all environments share a single state bucket is convenient, but it can lead to disaster if a dev script inadvertently overrides a production state file.
This template implements a highly secure, tenant-like prefix segregation directly on the central S3 bucket policy. The S3 bucket policy enforces constraints based on the IAM Principal's ARN and path:
Developers using the
devprofile can only read/write underexample-app/dev/<dev-account-id>/*.The staging GitHub Action role is locked strictly to
example-app/staging/*.The production GitHub Action role is locked strictly to
example-app/prod/*.
This ensures that even if a developer makes a local backend configuration mistake, the AWS S3 APIs will block them from accessing staging or production environments.
The Bootstrapping Flow (Solving the Chicken-and-Egg Problem)
To set up a remote backend in Terraform, you need an S3 bucket. But to manage that S3 bucket with Terraform, you need a backend! This is the classic chicken-and-egg problem of infrastructure-as-code.
This template solves this by leveraging a clean, 3-phase local-to-remote bootstrapping workflow:
Step 1: OIDC Deployment Roles: Run a local apply with
-backend=falsein thebootstrap/accounts/stagingandbootstrap/accounts/prodroots to provision the AWS OIDC roles first.Step 2: Shared State S3 Bucket: Run a local apply in the
bootstrap/stateroot to create the S3 bucket, passing in the OIDC role ARNs created in Step 1 so they are automatically authorized in the bucket policy.Step 3: Migration: Copy the
backend.tf.exampleconfigurations, insert your real resource IDs, and runterraform init -migrate-state. Terraform will seamlessly copy the local.tfstatefiles into the S3 bucket.
Once migrated, all bootstrap elements are managed in S3, and local .tfstate files can be safely deleted!
Parallel Multi-Region Matrix & Wave Deployments
Deploying regional application stacks (like VPCs, EKS clusters, and RDS) sequentially across multiple regions can drag your deployments from minutes to hours.
To optimize deployment times, this template leverages GitHub Actions Job Matrices.
When you trigger a deploy, the pipeline runs a dynamic resolver to read your target regions from the AWS_REGIONS_JSON variable (e.g., ["us-east-1", "us-west-2", "eu-west-1"]). It then spawns parallel runner environments:
resolve-regions:
runs-on: ubuntu-latest
outputs:
regions: ${{ steps.resolve.outputs.regions }}
# Resolves regions to a JSON array...
deploy-regional:
needs: [deploy-staging-global, resolve-regions]
strategy:
matrix:
region: ${{ fromJson(needs.resolve-regions.outputs.regions) }}
steps:
- name: Deploy Staging Regional App
run: |
terraform init -backend-config="region=${{ matrix.region }}"
terraform apply -auto-approve
Each region runs inside its own execution block with separate state files, enabling parallel applies that complete in record time!
Pro-Tip: Deploying Multi-Regional Infrastructures in Waves
While parallelizing deployments across all regions is extremely fast, production scenarios often require a more controlled blast radius. You can selectively use the AWS_REGIONS_JSON variable to deploy regional updates in progressive waves:
Wave 1 (Highest & Lowest Traffic Canary): Configure
AWS_REGIONS_JSONto target only your primary highest-traffic region and your lowest-traffic region first (e.g.,["us-east-1", "us-west-2"]). This deploys to your critical user base and a canary region concurrently.Verification: Monitor active metrics, logs, and telemetry in these environments to confirm system stability and check for anomalies.
Wave 2 (Staged Rollout): Once you're satisfied with Wave 1's stability, trigger the deployment to remaining regions. You can easily do this manually using the GitHub Actions workflow dispatch interface. Simply choose the
promote-prodaction and use the single-region override parameter (region) to deploy targeted regional updates one by one without needing to push new code tomain.
Hardening IAM to Least-Privilege
During initial development, the template defaults to attaching the AWS managed AdministratorAccess policy to OIDC deployment roles. While this speeds up developer velocity initially, it violates the principle of least privilege in production.
Moving to production is easy:
Review your Terraform resource files to compile a strict list of AWS actions (e.g.,
ec2:*,rds:*,s3:*).Replace
AdministratorAccessinbootstrap/modules/github-actions-role/main.tfwith custom, scoped IAM policy ARNs.Rerun
terraform applylocally in your staging and production bootstrap account directories (bootstrap/accounts/staging/andbootstrap/accounts/prod/) to securely update your deployment roles.
Conclusion
Implementing a multi-account, multi-region AWS Landing Zone doesn't have to be a multi-month engineering effort. By utilizing OIDC federation, tenant-segregated S3 state files, and parallel GitHub Actions matrices, you can deploy a secure, enterprise-grade cloud footprint in a single afternoon.
Ready to build yours?
Head over to the GitHub Repository, click the "Use this template" button to instantiate your own copy, and follow the step-by-step milestones in the README.md to begin your journey to a world-class cloud infrastructure!




