Three different applications, three different teams, three different problems to solve, and the same infrastructure decision made three times over, for a global automotive manufacturer’s internal application platforms. Each time, the choice was the same: don’t hand-roll the AWS resources, consume a versioned internal reference-architecture module and keep the application’s own stack as thin as possible. It’s the same discipline applied to a request/order workflow app, an internal data-processing platform, and an import/reporting platform, and the fact that it holds up identically across three unrelated teams is the actual proof it’s the right shape.
Thin stacks, shared modules
None of these three repositories declares a raw ECS service resource or a hand-rolled VPC. Each one is a small set of files (a root module, a secrets file, per-environment tfvars) that composes a pinned, versioned internal container module and drives it entirely through variables. Shared landing-zone facts (the VPC id, the ALB listener ARN, the ALB’s security group, the Route 53 zone, the DB subnet group, the NLB ARN) are resolved at plan time from SSM Parameter Store rather than hardcoded, so the same stack definition is portable across accounts and environments without anyone hand-editing an identifier. Environment separation is directory-based: a nonprod/prod split, each with its own tfvars and its own remote state key in a shared S3 backend.
ECS Fargate and a real database lifecycle
Compute across all three platforms is containerized services behind a shared Application Load Balancer, routed by host or path rules to per-service target groups with their own health checks. The data layer in every case is Amazon RDS for PostgreSQL, fronted by an RDS Proxy for pooled and intranet access. That’s where the work stopped being “write the Terraform once and walk away.” Over the life of these stacks I drove a real PostgreSQL major-version upgrade to 17 across multiple environments, changed storage type and sizing to stop drift caused by manual tuning, enforced SSL on every connection, and tuned backup retention and maintenance windows. None of that is glamorous, but a database that’s been upgraded, resized, and hardened in place is a different asset than one that was provisioned once and left alone.
Secrets by reference, drift by design
Every application secret in all three stacks is provisioned in Secrets Manager and injected into containers by reference. Nothing sensitive lives in tfvars, only identifiers. A recurring piece of the work was closing the gap between that intent and what Terraform actually tracked: duplicated container environment variables and mis-indexed secret references caused plan drift often enough that eliminating them became a standing habit, not a one-time cleanup.
The more interesting drift wasn’t a mistake. It was structural. One of the three platforms deploys through a container pipeline that registers new task-definition revisions outside of Terraform entirely, and production database attributes get touched manually between applies. That means a clean plan on the source-of-truth branch was never actually achievable, because the live state diverges from the code by design, not by accident. Rather than fight that with a targeted apply (which the platform’s CI/CD deliberately doesn’t support) or ignore the plan output altogether, I wrote a drift assessment that triaged every planned change into three buckets: safe to let happen, must be re-asserted immediately after apply, or purely cosmetic. I verified the real IAM boundaries by making the actual API calls rather than reading the policy and hoping, and used that as the basis for applying with confidence instead of applying blind. It’s a small piece of process, but it’s the difference between “the plan output is scary so nobody runs apply” and “we know exactly which lines in this plan are expected and why.”
flowchart TB stack["thin versioned stack: tfvars per env"] deployer["container pipeline: task-def revisions outside terraform"] subgraph aws["aws account: per environment"] alb["shared alb: oidc listener rules"] ecs["ecs services: reference-arch module"] rds["rds postgresql 17 + rds proxy"] sm["secrets manager + kms"] end stack -->|"plan / apply"| alb & ecs & rds & sm alb --> ecs ecs --> rds ecs --> sm deployer -.->|"drift by design"| ecs
Security gates that run before the apply, not after
CI/CD for all three platforms is manual dispatch with a plan/apply choice, gated by a mandatory static-analysis scan that has to pass before the shared reusable Terraform pipeline runs. Production applies are restricted to the main branch by environment protection rules. It’s a deliberately unglamorous pipeline shape, with no auto-apply and no push-triggered production changes, because a manually dispatched run with a human choosing “plan” or “plan and apply” is a much smaller blast radius than a merge queue quietly applying to production.
flowchart LR dev["engineer, dispatch: plan / plan+apply"] --> scan["static-analysis gate"] scan -->|"pass"| pipe["shared terraform pipeline"] pipe --> state["s3 remote state"] pipe --> role["deployment role"] role --> ssm["ssm parameter store: shared landing-zone facts"] pipe --> mod["versioned reference-architecture module"]
What I’d change
The reference-architecture module these three stacks consume doesn’t expose a way to ignore changes on the ECS task definition, which is the actual root cause of the structural drift above. The module could absorb that instead of every consuming stack needing its own drift-assessment workaround. I’d rather fix it once at the module layer than keep writing triage documents for a problem the module could just not have. It’s on the list; it just isn’t mine to ship alone.