Skip to main content
INS // Insights

Terraform Multi-Account AWS Architecture

Updated February 2026 · 9 min read

Most organizations start with a single AWS account. It works fine — until it doesn't. The moment you need environment isolation, separate billing, least-privilege access across teams, or a compliance boundary between workloads, a single account becomes a liability.

At Rutagon, we architect terraform multi-account AWS environments as a foundational layer for every cloud engagement. Whether we're standing up infrastructure for a production SaaS platform spanning 25+ AWS services or building our own internal systems with zero long-lived credentials, the pattern is the same: account segmentation, identity federation, centralized security, and cost control — all managed as code.

This article walks through the architecture we use in production, with real Terraform patterns you can adapt.

Why Multi-Account? The Case for Account Segmentation

A single AWS account creates blast radius problems. A misconfigured IAM policy in development can expose production data. A runaway Lambda in staging can burn through your budget before CloudWatch alerts fire. And when an auditor asks "show me your environment separation," pointing to naming conventions isn't going to cut it.

AWS Organizations solves this by providing hard boundaries between workloads. Each account is an isolation boundary for IAM, networking, billing, and service quotas.

Here's how we segment:

Account Purpose Key Services
ManagementOrganization root, billing, SCPsAWS Organizations, IAM Identity Center
SecurityCentralized logging, GuardDuty, ConfigCloudTrail, SecurityHub, GuardDuty
Shared ServicesDNS, container registries, shared toolingRoute 53, ECR, Terraform state
DevelopmentDev workloads, experimentationECS, Lambda, DynamoDB
StagingPre-production validationMirrors production stack
ProductionCustomer-facing workloadsEKS, CloudFront, RDS, WAF

This isn't theoretical. Our internal infrastructure runs on this exact pattern. Every Rutagon project — from our content platform to our production SaaS platform — deploys through isolated accounts with Terraform managing the entire lifecycle.

Terraform Organization Bootstrap

The foundation starts with aws_organizations_organization and a set of organizational units (OUs). We use a dedicated bootstrap Terraform workspace that only the management account touches.

resource "aws_organizations_organization" "org" {
  aws_service_access_principals = [
    "cloudtrail.amazonaws.com",
    "config.amazonaws.com",
    "guardduty.amazonaws.com",
    "sso.amazonaws.com",
  ]

  enabled_policy_types = [
    "SERVICE_CONTROL_POLICY",
    "TAG_POLICY",
  ]

  feature_set = "ALL"
}

resource "aws_organizations_organizational_unit" "workloads" {
  name      = "Workloads"
  parent_id = aws_organizations_organization.org.roots[0].id
}

resource "aws_organizations_organizational_unit" "security" {
  name      = "Security"
  parent_id = aws_organizations_organization.org.roots[0].id
}

resource "aws_organizations_account" "production" {
  name      = "rutagon-production"
  email     = "aws+production@rutagon.com"
  parent_id = aws_organizations_organizational_unit.workloads.id

  lifecycle {
    ignore_changes = [name, email]
  }
}

The lifecycle block on accounts is deliberate — AWS doesn't allow renaming accounts after creation, and Terraform will try to recreate them if the name drifts. Ignoring those fields prevents accidental account destruction. This is the kind of production lesson you only learn once.

OIDC-Based CI/CD: Zero Long-Lived Credentials

Long-lived AWS access keys in CI/CD pipelines are a security incident waiting to happen. They get committed to repos, stored in plaintext pipeline variables, and never rotated. We eliminated them entirely.

Every Rutagon CI/CD pipeline authenticates to AWS using OpenID Connect (OIDC) federation. GitLab CI issues a short-lived JWT, AWS validates it against the OIDC provider, and the pipeline assumes a scoped IAM role — no secrets stored anywhere.

resource "aws_iam_openid_connect_provider" "gitlab" {
  url             = "https://gitlab.com"
  client_id_list  = ["https://gitlab.com"]
  thumbprint_list = [data.tls_certificate.gitlab.certificates[0].sha1_fingerprint]
}

resource "aws_iam_role" "ci_deploy" {
  name = "gitlab-ci-deploy"

  assume_role_policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Effect = "Allow"
        Principal = {
          Federated = aws_iam_openid_connect_provider.gitlab.arn
        }
        Action = "sts:AssumeRoleWithWebIdentity"
        Condition = {
          StringEquals = {
            "gitlab.com:sub" = "project_path:rutagon/infrastructure:ref_type:branch:ref:main"
          }
        }
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "ci_deploy" {
  role       = aws_iam_role.ci_deploy.name
  policy_arn = aws_iam_policy.deploy_permissions.arn
}

The Condition block is critical. It scopes the role assumption to a specific GitLab project and branch. A pipeline running on a feature branch in a different repo cannot assume this role. This is "Security Is Architecture" in practice — the access control is baked into the infrastructure definition, not bolted on as a policy afterthought.

We covered the broader security automation pattern — including vulnerability scanning and compliance gates — in our article on Security Compliance CI/CD Automation.

Centralized Logging and Security

Every account in the organization ships logs to the Security account. CloudTrail captures API activity, AWS Config records resource state, and GuardDuty monitors for threats. Centralizing these into a single account means no workload account can tamper with its own audit trail.

resource "aws_cloudtrail" "org_trail" {
  name                       = "org-trail"
  s3_bucket_name             = aws_s3_bucket.cloudtrail_logs.id
  is_organization_trail      = true
  is_multi_region_trail      = true
  enable_log_file_validation = true
  kms_key_id                 = aws_kms_key.cloudtrail.arn

  event_selector {
    read_write_type           = "All"
    include_management_events = true
  }
}

Setting is_organization_trail = true automatically captures events from every account in the organization. Combined with AWS Security Hub aggregating findings across accounts, the Security account becomes a single pane of glass for the entire organization.

For teams running containerized workloads in regulated environments, this centralized logging architecture integrates directly with the EKS patterns we described in Kubernetes in Regulated Environments.

Service Control Policies: Guardrails as Code

Service Control Policies (SCPs) are the organizational guardrails that prevent accounts from doing things they shouldn't — regardless of what IAM policies exist within those accounts. We apply SCPs at the OU level so they cascade to all member accounts.

resource "aws_organizations_policy" "deny_regions" {
  name        = "deny-unapproved-regions"
  description = "Restrict workloads to approved AWS regions"
  type        = "SERVICE_CONTROL_POLICY"

  content = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "DenyUnapprovedRegions"
        Effect    = "Deny"
        Action    = "*"
        Resource  = "*"
        Condition = {
          StringNotEquals = {
            "aws:RequestedRegion" = ["us-west-2", "us-east-1", "us-gov-west-1"]
          }
        }
      }
    ]
  })
}

resource "aws_organizations_policy_attachment" "workloads_region_lock" {
  policy_id = aws_organizations_policy.deny_regions.id
  target_id = aws_organizations_organizational_unit.workloads.id
}

This SCP ensures no workload account can spin up resources outside of approved regions — a common requirement in government environments where data residency matters. The policy is version-controlled, peer-reviewed, and applied automatically. No one can "just quickly" launch an EC2 instance in ap-southeast-1.

For organizations migrating from other cloud providers into this kind of controlled AWS environment, we documented the migration playbook in GCP to AWS Migration for Government.

Cost Optimization: Visibility Before Optimization

Multi-account architecture gives you cost visibility for free. Each account generates its own billing data, making it trivial to attribute costs to specific environments or projects.

We layer on additional controls:

  • AWS Budgets per account with alerts at 80% and 100% thresholds
  • Cost allocation tags enforced via Tag Policies at the organization level
  • Reserved capacity planning managed from the management account to aggregate savings across all accounts
  • Scheduled scaling — dev and staging environments scale down outside business hours
resource "aws_budgets_budget" "account_monthly" {
  for_each = toset(["development", "staging", "production"])

  name         = "${each.key}-monthly-budget"
  budget_type  = "COST"
  limit_amount = var.budget_limits[each.key]
  limit_unit   = "USD"
  time_unit    = "MONTHLY"

  notification {
    comparison_operator       = "GREATER_THAN"
    threshold                 = 80
    threshold_type            = "PERCENTAGE"
    notification_type         = "ACTUAL"
    subscriber_email_addresses = [var.ops_email]
  }
}

The for_each pattern keeps the configuration DRY while applying appropriate budget thresholds per environment. Production gets a higher limit; development gets a tight leash. Start Small, Think Big — control costs early so you can invest in the infrastructure that matters.

Cross-Account Access with Terraform Providers

One of the more nuanced challenges in multi-account Terraform is managing resources across accounts from a single pipeline. We use provider aliases with assume_role to deploy into target accounts without storing credentials for each one.

provider "aws" {
  alias  = "production"
  region = "us-west-2"

  assume_role {
    role_arn = "arn:aws:iam::${var.production_account_id}:role/terraform-deploy"
  }
}

resource "aws_s3_bucket" "app_assets" {
  provider = aws.production
  bucket   = "rutagon-app-assets-prod"
}

The CI/CD pipeline assumes the initial OIDC role, then cascades into target accounts using sts:AssumeRole. Each target account has a terraform-deploy role with least-privilege permissions scoped to exactly what Terraform needs to manage. No admin access. No wildcard policies. Every permission is justified and auditable.

The Architecture in Context

This multi-account pattern isn't just infrastructure for its own sake. It's the foundation that lets everything else work safely: containerized workloads in regulated environments, automated security compliance in CI/CD, and the AWS cloud infrastructure capabilities we deliver to clients.

When a contracting officer asks, "How do you manage environment isolation?" — the answer isn't a slide deck. It's this architecture, running in production, managed as code, with every change tracked in version control. Ship, Don't Slide.

What is terraform multi-account AWS architecture and why does it matter?

Terraform multi-account AWS architecture uses Infrastructure as Code to manage multiple AWS accounts within an AWS Organization. Each account serves as a hard isolation boundary for IAM, networking, and billing — eliminating blast radius risk between environments. For regulated workloads, this provides the environment segmentation that auditors and contracting officers require without relying on naming conventions or manual configuration.

How does OIDC-based CI/CD eliminate credential risk in AWS deployments?

OIDC federation replaces long-lived AWS access keys with short-lived tokens issued per pipeline run. The CI platform (GitLab, GitHub Actions) issues a signed JWT, AWS validates it against a registered OIDC provider, and the pipeline assumes a scoped IAM role. Credentials exist only for the duration of the deployment, are never stored in pipeline variables, and are automatically scoped to specific projects and branches via IAM conditions.

How do Service Control Policies differ from IAM policies in a multi-account setup?

IAM policies define what a principal can do within an account. Service Control Policies define the maximum permissions available to an entire account — they act as a ceiling that IAM policies cannot exceed. Even if an account has an IAM admin user, an SCP denying access to unapproved regions will override it. SCPs are applied at the Organizational Unit level and cascade to all member accounts, making them the primary guardrail mechanism for multi-account governance.

Can Terraform manage resources across multiple AWS accounts from a single pipeline?

Yes. Using provider aliases with assume_role configuration, a single Terraform workspace can deploy resources into multiple target accounts. The pipeline authenticates once via OIDC, then assumes scoped deployment roles in each target account. Each role follows least-privilege principles — Terraform only gets the permissions required to manage the specific resources defined in the configuration, not blanket admin access.

What cost optimization strategies work best in a multi-account AWS Organization?

Account-level isolation provides natural cost attribution — each account's billing data maps directly to an environment or project. Layer on AWS Budgets with threshold alerts, Tag Policies enforced at the organization level for cost allocation, Reserved Instance purchasing aggregated at the management account for cross-account savings, and scheduled scaling to power down non-production environments outside business hours. The key is visibility first: you can't optimize what you can't attribute.

Discuss your project with Rutagon

Contact Us →

Ready to discuss your project?

We deliver production-grade software for government, defense, and commercial clients. Let's talk about what you need.

Initiate Contact