Skip to content

DurianPy-Davao-Python-User-Group/durianpy-root-infra

Repository files navigation

durianpy-root-infra

Multi-root Terraform monorepo for Durianpy infrastructure. Manages AWS resources across four accounts (root, prod, nonprod, sandbox) and GCP production resources, each as an independent Terraform Cloud workspace.


Architectural inspiration

This repo was refactored using elemnta-infra-monorepo as structural inspiration. The following patterns were studied and adapted:

Elemnta pattern What was adopted What was intentionally left out
workspaces/{env}/ multi-root layout Adopted verbatim — one directory per account/workspace Elemnta's tier-based dependency ordering within a single workspace (Durianpy's roots are simpler)
assume_role in every AWS provider Adopted — all four workspaces assume durianpy-terraform-admin in their target account Elemnta's separate elemnta-admin-role vs elemnta-root role split (Durianpy uses a single role name per account)
Shared reusable modules under modules/ Adopted — modules/aws/ and modules/gcp/ Elemnta's large ECS/LGTM/observability modules (Durianpy doesn't run those stacks yet)
Outputs contract between workspaces Adopted lightly — each workspace exports key values via TFC workspace outputs Elemnta's separate outputs/ Terraform root reading from S3 state (overkill for current Durianpy scale)
Root account owns shared services Adopted — DNS, CDN, OIDC, connections stay in root workspace Elemnta's Transit Gateway / VPN / Firewall topology (not needed)
Per-workspace terraform.tfvars Adopted Elemnta's .auto.tfvars pattern (either works; explicit tfvars preferred here)

Repository layout

durianpy-root-infra/
├── workspaces/
│   ├── root/          # AWS root account — shared DNS, CDN, CI/CD identity
│   ├── prod/          # AWS prod account + GCP production project
│   ├── nonprod/       # AWS nonprod account — shared non-production infra
│   └── sandbox/       # AWS sandbox account — isolated/test resources
├── modules/
│   ├── aws/
│   │   ├── cdn/              # S3 origin + CloudFront + ACM + Route53
│   │   ├── github-oidc/      # GitHub Actions OIDC provider + IAM role
│   │   ├── budget/           # Cost budget with forecast alert
│   │   ├── connections/      # AWS CodeConnections for GitHub
│   │   └── terraform-state/  # S3 bucket for Terraform state
│   └── gcp/
│       └── project/          # GCP project API enablement + IAM + state bucket
├── package/           # LEGACY — previous single-root layout; kept for reference
│   ├── root/          #   do not apply from here; use workspaces/root/ instead
│   └── sandbox/       #   do not apply from here; use workspaces/sandbox/ instead
└── README.md

The legacy package/ tree is retained as a reference while moved blocks in workspaces/root/moved.tf are live. It can be removed after the first successful apply from workspaces/root/.


Terraform Cloud workspace mapping

Directory TFC workspace AWS account Purpose
workspaces/root/ durianpy-root Root / management Shared DNS, CDN, CI/CD identity, state bucket
workspaces/prod/ durianpy-prod Prod Production AWS infra + GCP project
workspaces/nonprod/ durianpy-nonprod Nonprod Shared non-production infra
workspaces/sandbox/ durianpy-sandbox Sandbox Isolated testing

All four workspaces live in the durianpy Terraform Cloud organization.


Account responsibilities

Root account (workspaces/root/)

  • durianpy.org public Route53 hosted zone (pre-existing; records managed here)
  • cdn.durianpy.org CloudFront distribution + S3 origin + ACM certificate
  • GitHub Actions OIDC provider and CI/CD IAM role (github_oidc_role)
  • AWS CodeConnections entries for GitHub pipelines
  • Root account monthly cost budget ($20)
  • durianpy-infra-terraform-state S3 bucket (legacy; state now primarily in TFC)

Prod account (workspaces/prod/)

  • Production AWS resources (add modules here as the account grows)
  • GCP production project — API enablement, IAM bindings, optional state bucket
  • Prod account monthly cost budget

Nonprod account (workspaces/nonprod/)

  • Shared non-production AWS infrastructure
  • Nonprod account monthly cost budget

Sandbox account (workspaces/sandbox/)

  • Isolated / ephemeral test resources

Apply order

1. workspaces/root/     — owns shared DNS, OIDC, state bucket
2. workspaces/prod/     — independent; apply after root if consuming root outputs
3. workspaces/nonprod/  — independent; apply after root if consuming root outputs
4. workspaces/sandbox/  — fully independent

Prod, nonprod, and sandbox are independent of root today. If they later consume root outputs (e.g. Route53 zone ID for subdomain delegation), apply root first.


AWS assume-role strategy

Inspired by the Elemnta repo's pattern where every provider block explicitly calls sts:AssumeRole rather than relying on ambient credentials for the target account.

Terraform Cloud identity (base credentials)
    │
    ├─► assume_role → arn:aws:iam::ROOT_ACCOUNT_ID:role/durianpy-terraform-admin
    │       └─► runs workspaces/root/ plan + apply
    │
    ├─► assume_role → arn:aws:iam::PROD_ACCOUNT_ID:role/durianpy-terraform-admin
    │       └─► runs workspaces/prod/ plan + apply
    │
    ├─► assume_role → arn:aws:iam::NONPROD_ACCOUNT_ID:role/durianpy-terraform-admin
    │       └─► runs workspaces/nonprod/ plan + apply
    │
    └─► assume_role → arn:aws:iam::SANDBOX_ACCOUNT_ID:role/durianpy-terraform-admin
            └─► runs workspaces/sandbox/ plan + apply

The TFC base identity only needs sts:AssumeRole permission for the four admin role ARNs. All infra permissions are granted via the assumed role.

Cross-account provider aliases: the root workspace also declares an aws.us_east_1 provider alias that assumes the same root role but targets us-east-1. This is required because CloudFront mandates ACM certificates in us-east-1 regardless of where other resources are deployed.

Role naming convention

Role name: durianpy-terraform-admin Trust policy: must trust the TFC base identity (OIDC or IAM user ARN). Permissions: AdministratorAccess (or a tighter custom policy per account).


Bootstrap procedure

The assume-role pattern requires durianpy-terraform-admin to exist before Terraform can run. First-time setup:

  1. Create durianpy-terraform-admin in each target account manually (or via AWS CloudFormation StackSets from the org management account):

    {
      "RoleName": "durianpy-terraform-admin",
      "AssumeRolePolicyDocument": {
        "Statement": [{
          "Effect": "Allow",
          "Principal": { "AWS": "arn:aws:iam::TFC_BASE_IDENTITY_ACCOUNT_ID:root" },
          "Action": "sts:AssumeRole"
        }]
      },
      "ManagedPolicies": ["arn:aws:iam::aws:policy/AdministratorAccess"]
    }

    Replace TFC_BASE_IDENTITY_ACCOUNT_ID with the account ID of the IAM user or role used by Terraform Cloud.

  2. Configure TFC workspace variables (sensitive):

    • TF_VAR_root_account_id (root workspace)
    • TF_VAR_prod_account_id (prod workspace)
    • TF_VAR_nonprod_account_id (nonprod workspace)
    • TF_VAR_sandbox_account_id (sandbox workspace)
    • AWS credentials: AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY for the TFC base identity, or configure TFC's native dynamic AWS credentials.
  3. Apply workspaces/root/ first — it processes the moved blocks that migrate existing resources from the old module.package.module.root.* address hierarchy.


Migrating the root workspace state

workspaces/root/ points to the same durianpy-root TFC workspace as the old main.tf. The moved.tf file remaps all live resource addresses:

module.package.module.root.module.budget.aws_budgets_budget.cost
  → module.budget.aws_budgets_budget.cost

module.package.module.root.module.iam.aws_iam_role.github_oidc
  → module.github_oidc.aws_iam_role.github_oidc

module.package.module.root.module.s3.aws_s3_bucket.cdn        \
module.package.module.root.module.cloudfront.*                  ├─ → module.cdn.*
module.package.module.root.module.route53.*                    /

...and so on (see workspaces/root/moved.tf for the full list)

On the first terraform plan, Terraform reads the existing TFC state and applies the moved blocks — no resources are destroyed or re-created. After a successful terraform apply the moved blocks become permanent no-ops.


Sandbox state migration

The previous package/sandbox/ used a local state file. To migrate to the new durianpy-sandbox TFC workspace without recreating 150 IAM resources:

cd workspaces/sandbox
terraform login
terraform init   # initialises the TFC backend

# Push the old local state to TFC (Option A — recommended)
terraform state push ../../package/sandbox/terraform.tfstate

terraform plan   # should show 0 changes if resource addresses are identical

Resource addresses in workspaces/sandbox/main.tf are kept flat (identical to the old workspace) so no terraform state mv operations are needed.


GCP credential strategy

GCP credentials for the durianpy-prod workspace are sourced in priority order:

  1. Workload Identity Federation (recommended) — configure TFC's OIDC provider in GCP IAM. Set GOOGLE_APPLICATION_CREDENTIALS in the TFC workspace to a generated impersonation credential file. This avoids static keys entirely.

  2. Service account key — store the raw JSON key (or base64-encoded) in the TFC workspace variable GOOGLE_CREDENTIALS. Simpler to bootstrap but less secure.

  3. Application Default Credentials — for local development only (gcloud auth application-default login).

No service account JSON files should ever be committed to this repository.


Module reference

modules/aws/cdn

Creates an end-to-end CDN stack: S3 origin bucket, CloudFront distribution, ACM certificate (us-east-1), and Route53 DNS records. Requires two provider configurations: default (primary region) and aws.us_east_1 (for ACM).

modules/aws/github-oidc

Creates a GitHub Actions OIDC provider and IAM role with controlled IAM + Terraform state + STS permissions. Dangerous operations (user creation, access key management, OIDC provider modification) are explicitly denied.

modules/aws/budget

AWS Budgets cost budget with a forecasted threshold notification.

modules/aws/connections

AWS CodeConnections (GitHub) for pipeline integrations. Connections require manual activation in the AWS Console after apply.

modules/aws/terraform-state

S3 bucket with versioning and AES256 encryption for storing Terraform state files.

modules/gcp/project

GCP project API enablement, project-level IAM bindings, and optional GCS state bucket. See the module variables for credential configuration options.

About

Root Terraform Infra of DurianPy Projects

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors