A Terraform module that turns ~50 lines of configuration into a production-grade ECS Fargate platform — load balancing, blue/green deployments, service discovery, and an independent CI/CD pipeline for every microservice.
Every team deploying microservices on AWS was solving the same problems from scratch: how to route traffic, how to deploy without downtime, how to wire a pipeline, how to handle internal service-to-service calls. The result was a dozen slightly-different, slightly-broken platforms — and weeks of lead time for every new service.
One shared ECS cluster, many services — each declared as a few lines in a map. The module classifies every service into one of three shapes and builds the right infrastructure for each:
┌──────────────────────────────────────────────┐
│ AWS Account │
│ │
Internet/CDN ─────►│ ALB (external) │
│ ├── / ──► ui [blue/green] │
│ ├── api.domain ──► api [host routing] │
│ └── /admin/* ──► admin [path routing] │
│ │
│ Service Discovery (*.local) │
│ ├── ds.ns.local ──► ds [rolling + │
│ └── worker.ns.local ──► worker circuit-brk]│
│ │
│ CI-only (task def, no service) │
│ └── data-processor ──► ad-hoc run-task │
│ │
│ Shared ECR ◄── CodePipeline (per service) │
│ └── CodeBuild ─► CodeDeploy │
└──────────────────────────────────────────────┘
External services get ALB listeners with host- or path-based routing and CodeDeploy blue/green — zero-downtime by construction. Internal services get Cloud Map service discovery and rolling deploys with a deployment circuit breaker that auto-rolls-back bad releases. CI-only services get a pipeline and task definition without a running service — for batch and ad-hoc workloads.
One pipeline per microservice. Every service deploys independently — no lock-step releases, no shared blast radius. A shared ECR repository with service-scoped image tags keeps registry sprawl down.
Templated task definitions. Container definitions, appspec, and buildspec render from templates, including a security-scanning variant (Wiz) that inserts image scanning into the build without teams changing anything.
Batteries included, swappable. Sub-modules for CloudFront (with Lambda@Edge), ACM certificates, Secrets Manager wiring, and synthetic monitoring (Uptrends) — each optional.
Roadmapped like a product. Canary traffic shifting, sidecar injection (OTEL/Envoy/Datadog), and map-based secrets are tracked on a public roadmap — because an internal module is a product with users.
The heart of the module is one aws_ecs_service resource that shape-shifts per service via for_each and dynamic blocks — external services get an ALB target group, internal ones get Cloud Map service discovery, from the same resource (naming sanitized):
resource "aws_ecs_service" "service" { for_each = local.deployed_services # one map drives everything name = "aws-${var.account_alias}-…-service-${each.key}" launch_type = "FARGATE" task_definition = aws_ecs_task_definition.task[each.key].arn # ALB wiring only exists for external services dynamic "load_balancer" { for_each = each.value.service_type == "external" ? [1] : [] content { target_group_arn = aws_lb_target_group.blue[each.key].arn container_port = each.value.container_port } } # internal services register in Cloud Map instead dynamic "service_registries" { for_each = each.value.service_type == "internal" ? [1] : [] content { registry_arn = aws_service_discovery_service.sd[each.key].arn } } }
Routing intent lives in data, not resources: routing_type (host vs path), per-service domains, and the default-service fallback all resolve inside locals — so adding a microservice is adding a map entry, never copying a resource block.
New microservice platforms went from weeks of bespoke Terraform to under an hour of writing a service map. Deployments are zero-downtime by default, rollbacks are automatic, and every team runs on the same audited, versioned foundation.