Skip to main content

Command Palette

Search for a command to run...

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.

Updated
7 min read
The Ultimate Multi-Account & Multi-Regional AWS Terraform Landing Zone
H
Full-stack Developer focused on the intersection of high-performance web and scalable AWS infrastructure. I write Well-Architected to share patterns for building resilient, cost-effective, and developer-friendly cloud-native web applications.

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:

  1. Massive Blast Radius: A simple mistake in a staging environment could accidentally take down your production database.

  2. Static Key Leaks: Storing long-lived AWS Access Keys in GitHub Secrets or developer laptops is a ticking security time bomb.

  3. Sluggish Deployments: Deploying regional infrastructure sequentially makes your CI/CD pipelines painfully slow.

  4. 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:

Multi-Account Architecture of the AWS Terraform Template.

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 dev profile can only read/write under example-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:

Workflow Sequence of the AWS Terraform Template.
  1. Step 1: OIDC Deployment Roles: Run a local apply with -backend=false in the bootstrap/accounts/staging and bootstrap/accounts/prod roots to provision the AWS OIDC roles first.

  2. Step 2: Shared State S3 Bucket: Run a local apply in the bootstrap/state root to create the S3 bucket, passing in the OIDC role ARNs created in Step 1 so they are automatically authorized in the bucket policy.

  3. Step 3: Migration: Copy the backend.tf.example configurations, insert your real resource IDs, and run terraform init -migrate-state. Terraform will seamlessly copy the local .tfstate files 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:

  1. Wave 1 (Highest & Lowest Traffic Canary): Configure AWS_REGIONS_JSON to 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.

  2. Verification: Monitor active metrics, logs, and telemetry in these environments to confirm system stability and check for anomalies.

  3. 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-prod action and use the single-region override parameter (region) to deploy targeted regional updates one by one without needing to push new code to main.

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:

  1. Review your Terraform resource files to compile a strict list of AWS actions (e.g., ec2:*, rds:*, s3:*).

  2. Replace AdministratorAccess in bootstrap/modules/github-actions-role/main.tf with custom, scoped IAM policy ARNs.

  3. Rerun terraform apply locally in your staging and production bootstrap account directories (bootstrap/accounts/staging/ and bootstrap/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!