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:
-
Shared-services account creates an authorization:
-
Dev account creates the association:
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).
Related Pages¶
- AWS Overview — Account IDs, profile mapping, verification
- VPC & Networking — VPC CIDR allocations
- Route53 DNS — Split-horizon DNS and cross-account zone association