Skip to content

Multi-Account Setup

CWIQ.IO uses an AWS Organizations structure with three accounts: management, shared-services, and dev — each with a dedicated AWS CLI profile and Terraform backend.


Organization Structure

AWS Organization: CodeWilling
├── Management Account (921838417607)
│   └── Owns the Organization, billing, SCPs
├── Shared-Services Account (308188966547)
│   └── VPC: cwiq-shared-vpc (10.0.0.0/16)
│       Central tools: GitLab, Vault, Nexus, Authentik, observability
└── Dev Account (686123185567)
    └── VPC: cwiq-dev-vpc (10.1.0.0/16)
        Application workloads: Orchestrator, EKS, LangFuse, Demo

AWS CLI Profile Configuration

Add the following to ~/.aws/config:

[profile shared-services]
region = us-west-2
# Account: 308188966547

[profile dev]
region = us-west-2
# Account: 686123185567

And ~/.aws/credentials:

[shared-services]
aws_access_key_id     = <key>
aws_secret_access_key = <secret>

[dev]
aws_access_key_id     = <key>
aws_secret_access_key = <secret>

Never use the default profile

The default profile targets the management account (921838417607). Operating against it will either fail (insufficient permissions) or modify stale/duplicate resources. See AWS Overview.


Cross-Account IAM

Some resources require cross-account access. The primary patterns are:

Route53 Cross-Account DNS

The shared.cwiq.io private hosted zone (owned by shared-services) is associated with the dev VPC. This requires a two-step Terraform configuration:

  1. Shared-services account creates an authorization:

    resource "aws_route53_vpc_association_authorization" "shared_internal_to_dev" {
      zone_id = <shared_internal_zone_id>
      vpc_id  = var.dev_vpc_id
    }
    

  2. Dev account creates the association:

    resource "aws_route53_zone_association" "shared_internal" {
      zone_id = var.shared_internal_zone_id
      vpc_id  = var.vpc_id
    }
    

This allows dev VPC instances to resolve *.shared.cwiq.io private records (e.g., Authentik NLB, SonarQube private IP) without crossing the public internet.

S3 Cross-Account Runner Cache

The GitLab runner S3 cache bucket (cwiq-dev-gitlab-runner-cache) lives in the dev account but is accessed by runners that authenticate via the shared-services account. This requires:

  • Dev account: S3 bucket policy granting access to the shared-services IAM role
  • Shared-services account: IAM policy on the runner IAM role permitting S3 operations

EC2 Instance Profiles

Every EC2 instance has an IAM role/instance profile allowing it to: - Read its own Tailscale auth key from Secrets Manager - Access S3 buckets scoped to its application (GitLab: artifacts, registry, runner cache) - Use Vault AppRole auth via Secrets Manager secret references

Source: terraform-plan/organization/environments/<env>/ec2-instances/<app>/iam.tf


Terraform State Backends

Each account uses a separate S3 backend for Terraform state:

Account Bucket Key Pattern Profile
Shared-Services cwiq-terraform-states cwiq-io/shared-services/<module>/terraform.tfstate shared-services
Dev cwiq-terraform-states cwiq-io/dev/<module>/terraform.tfstate shared-services

Backend profile

The Terraform state S3 bucket lives in the shared-services account. Dev environment modules specify profile = "shared-services" in their backend.tf specifically for state access, even though the resources themselves are created in the dev account.


Module-Level Operations

Always operate at the module level

Never run terraform apply at the environment directory level. Each EC2 instance, EKS cluster, and networking component is its own independent root module with its own backend.tf and provider.tf.

# Correct: operate on a specific module
cd terraform-plan/organization/environments/dev/ec2-instances/cwiq-orchestrator
terraform init
terraform plan -var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" -var="created_by=$CREATED_BY"
terraform apply -var="tailscale_auth_key=$TAILSCALE_AUTH_KEY" -var="created_by=$CREATED_BY"

The CREATED_BY variable is mandatory — it tags every AWS resource with the operator's identity (e.g., ateodoro).