Repository navigation
feat(ecs-fargate-service): long-running container module + example stack - #8
Conversation
One HTTP container on ECS Fargate behind an ALB with ACM TLS and a Route 53 alias. Defaults to the account's default VPC public subnets with a public IP (no NAT gateway, no VPC endpoints). Terraform owns the shape; CI registers task-definition revisions (service ignores task_definition drift). Exposes ci_policy_statements for github-oidc-role, same pattern as preview-substrate. App Runner is closed to new AWS customers since 2026-04-30.
kyleswiger
left a comment
There was a problem hiding this comment.
🤖 Gemini Automated Code Review
Reviewed automatically by Gemini PR Reviewer
📌 Executive Summary
This pull request introduces a highly robust, production-ready, and cost-optimized ECS Fargate module (ecs-fargate-service) alongside a complete example stack (ecs-fargate-stack). The design beautifully implements the "Terraform owns the infrastructure shape, CI/CD owns the application image" paradigm, preventing state conflicts during routine deployments. The default network posture is exceptionally well-thought-out, offering a low-cost entry point (~$27–35/mo) for running persistent VMs (like Elixir/BEAM or JVM) without the heavy overhead of NAT Gateways.
Overall, the code quality is outstanding, featuring excellent documentation, clear variable descriptions, and strict adherence to modern Terraform patterns. The risk assessment is Low, with only
|
Resolve custom-domain validation, task-definition ownership, and example documentation review findings. Co-Authored-By: Warp <agent@warp.dev>
kyleswiger
left a comment
There was a problem hiding this comment.
🤖 Gemini Automated Code Review
Reviewed automatically by Gemini PR Reviewer
what about KMS decryption?
statement {
sid = "DecryptSecrets"
actions = ["kms:Decrypt"]
resources = ["*"]
condition {
test = "StringEquals"
variable = "kms:ViaService"
values = [
"ssm.${local.region}.amazonaws.com",
"secretsmanager.${local.region}.amazonaws.com",
]
}
}This is also very good and secure (using kms:ViaService to restrict decryption).
- Security Group Rules:
The ALB security group allows ingress on 80 and 443 from `0.0.0
| wait_for_steady_state = false | ||
|
|
||
| capacity_provider_strategy { | ||
| capacity_provider = var.capacity_provider | ||
| weight = 1 | ||
| } | ||
|
|
||
| network_configuration { |
There was a problem hiding this comment.
Bug: aws_ecs_cluster.this never gets its Fargate capacity providers associated, so this capacity_provider_strategy will fail terraform apply.
aws_ecs_cluster.this is created with only a containerInsights setting — there's no aws_ecs_cluster_capacity_providers resource anywhere in this module (and the AWS provider ≥5.0 pinned in versions.tf removed the old capacity_providers/default_capacity_provider_strategy arguments from aws_ecs_cluster itself).
Per the ECS CreateCluster API docs: "A capacity provider must be associated with a cluster before it can be included as part of the default capacity provider strategy of the cluster or used in a capacity provider strategy when calling the CreateService or RunTask actions." Fargate/Fargate Spot only auto-associate for clusters created via the AWS Console — not via the API/Terraform.
Since this module always creates its own cluster and var.capacity_provider defaults to FARGATE (with FARGATE_SPOT as the only alternative, as used in examples/ecs-fargate-stack), every default terraform apply of this module will fail creating aws_ecs_service.this with an error that the capacity provider isn't associated with the cluster.
Suggested fix — add near the cluster resource:
resource "aws_ecs_cluster_capacity_providers" "this" {
cluster_name = aws_ecs_cluster.this.name
capacity_providers = ["FARGATE", "FARGATE_SPOT"]
}and add aws_ecs_cluster_capacity_providers.this to aws_ecs_service.this's depends_on (alongside the existing listener dependencies).
| }, | ||
| ], | ||
| var.create_ecr_repository ? [ | ||
| { | ||
| sid = "EcrPush" | ||
| effect = "Allow" | ||
| actions = [ | ||
| "ecr:BatchCheckLayerAvailability", | ||
| "ecr:BatchGetImage", | ||
| "ecr:GetDownloadUrlForLayer", | ||
| "ecr:InitiateLayerUpload", | ||
| "ecr:UploadLayerPart", | ||
| "ecr:CompleteLayerUpload", | ||
| "ecr:PutImage", | ||
| ] | ||
| resources = [aws_ecr_repository.this[0].arn] | ||
| }, | ||
| ] : [], | ||
| [ | ||
| { |
There was a problem hiding this comment.
Bug: ci_policy_statements grants no ECR push permission when create_ecr_repository = false, despite advertising "ECR push" unconditionally.
The EcrPush statement (InitiateLayerUpload, UploadLayerPart, CompleteLayerUpload, PutImage, etc.) is wrapped in var.create_ecr_repository ? [...] : []. The only unconditional ECR statement is EcrAuth (ecr:GetAuthorizationToken, main.tf output line 99). "Bring your own existing ECR repo" is a first-class, documented mode of this module (variables.tf image_repository_url, README input table, and exercised directly by tests/offline/domain_pairing.tftest.hcl's publishes_task_template_for_ci run) — not an edge case.
So in that mode, a CI role built from this output (as wired in examples/ecs-fargate-stack/main.tf straight into github-oidc-role) can docker login but every docker push fails with AccessDenied, with nothing in the output's own description (line 95, "ECR push") or the module README's Outputs section flagging the gap.
Since an external repo's ARN is derivable from var.image_repository_url (<acct>.dkr.ecr.<region>.amazonaws.com/<name> → arn:aws:ecr:<region>:<acct>:repository/<name>), or could be taken as an optional image_repository_arn input, this looks fixable rather than an unavoidable limitation of the "bring your own repo" mode.
Summary
terraform-modules/ecs-fargate-service: cluster, task definition, service (Spot-capable, circuit breaker, ECS Exec), ALB + target group, ACM cert + Route 53 alias when a domain is given, ECR repo triad from lambda-container,ci_policy_statementsoutput for github-oidc-role.examples/ecs-fargate-stackin the numbered-comment style.First consumer: kyleswiger/beam-testbed (
terraform/main.tf). Plan output attached in a comment once run.Test plan
terraform fmt -check -recursiveterraform validateon module and example (aws provider 6.63 under>= 5.0)terraform planfrom beam-testbed against this branch🤖 Generated with Claude Code