ECS Fargate Service
Runs the app container on Fargate in private subnets with IAM, logs, and secrets.
GovCloudCMKView source
- Module
- ecs-service
- Layer
- Compute
- Interface
- 55 inputs, 11 outputs
- Used by
- 14 Kaizen apps
Why it matters
This module runs the app itself. It starts containers in private subnets with no public IP, injects secrets from SSM or Secrets Manager, and rolls back a bad deploy on its own. The same module call works in commercial AWS and GovCloud, and federal stacks turn on a read only filesystem, a non root user, and customer managed log encryption with a few settings.
Use it when
- You deploy a containerized web app or API behind the alb module.
- You run an internal service with no load balancer. Leave target_group_arn null and use Cloud Map with service_registry_arn.
- The customer account forbids creating IAM roles. Pass execution_role_arn and task_role_arn.
Reach for something else when
- You need a one off job such as a database migration. Write a separate aws_ecs_task_definition and run it with aws ecs run-task.
- You need a sidecar other than Fluent Bit, such as Envoy or an OpenTelemetry agent. The module supports only the Fluent Bit log router.
What it creates
aws_iam_role(task execution role, AmazonECSTaskExecutionRolePolicy with a partition aware ARN)aws_iam_role_policy(SSM, Secrets Manager, and KMS reads for the execution role)aws_iam_role(task role for app permissions)aws_security_group(ingress on container_port from the load balancer only)aws_cloudwatch_log_group(/ecs/<name>, optional CMK)aws_ecs_task_definition(Fargate, ARM64 by default, optional init and Fluent Bit sidecar containers)aws_ecs_service(platform 1.4.0, circuit breaker with rollback)aws_appautoscaling_targetand aws_appautoscaling_policy (CPU target tracking)
Secure by default
- Tasks never get a public IP (assign_public_ip = false, not configurable).
- The task security group accepts traffic only from the load balancer security group on container_port.
- Failed deploys roll back on their own (deployment circuit breaker with rollback).
- ECS Exec is off (enable_execute_command = false).
- Secrets reach the container from SSM or Secrets Manager, never as plain environment variables.
Commercial and GovCloud
module "ecs_service" {
source = "git::ssh://git@github.com/the-kaizen-labs/terraform-modules.git//ecs-service?ref=v1.7.0"
app_name = local.app_name
cluster_name = module.ecs_cluster.cluster_name
vpc_id = module.networking.vpc_id
private_subnet_ids = module.networking.private_subnet_ids
container_image = "${module.ecr.repository_url}:latest"
target_group_arn = module.alb.target_group_arn
alb_security_group_id = module.alb.alb_security_group_id
container_secrets = [{ name = "DATABASE_URL", valueFrom = aws_ssm_parameter.database_url.arn }]
ssm_parameter_arns = [aws_ssm_parameter.database_url.arn]
tags = local.tags
}| Setting | Commercial default | Federal setting | Note |
|---|---|---|---|
aws_region | us-east-2 | us-gov-west-1 | Feeds the awslogs configuration. Set it to the stack's region. |
log_group_kms_key_arn | null (AWS managed) | customer managed KMS key ARN | FedRAMP SC-13. |
log_retention_days | 30 | 90 | FedRAMP AU-11, as set in examples/federal. |
readonly_root_filesystem | null (AWS default false) | true | |
container_user | null (image default) | "65532" | The UID that Chainguard distroless images run as. |
drop_all_capabilities | false | true | |
secrets_kms_key_arns | [] | the customer managed key that encrypts Secrets Manager values | |
execution_role_arn, task_role_arn | null (module creates the roles) | customer provided role ARNs, when Kaizen cannot create IAM roles |
How it connects
- From networking: vpc_id, private_subnet_ids
- From ecs-cluster: cluster_name
- From ecr: repository_url (as container_image)
- From alb: target_group_arn, alb_security_group_id
- From rds-postgres: db_host, in an SSM DATABASE_URL passed through container_secrets
- From redis: endpoint_address, in an SSM REDIS_URL passed through container_secrets
- From s3-bucket: bucket_arn, in a policy on task_role_name
- From rum: app_monitor_id, identity_pool_id (as environment_variables)
- From openobserve: ingest_host, in main_container_log_configuration for Fluent Bit
- To rds-postgres: security_group_id (as allowed_security_group_ids)
- To redis: security_group_id (as allowed_security_group_ids)
- To openobserve: security_group_id (as caller_ingress_security_group_ids)
Inputs
| Name | Type | Default | Required | Description |
|---|---|---|---|---|
app_name | string | yes | Application name (e.g. 'oak-prototype', 'bidbuddy') | |
cluster_name | string | yes | ECS cluster name (use module.ecs_cluster.cluster_name) | |
container_image | string | yes | Full container image URI including tag (e.g. ECR or cgr.dev for Chainguard distroless) | |
private_subnet_ids | list(string) | yes | Private subnet IDs for ECS tasks | |
vpc_id | string | yes | VPC ID | |
additional_ingress_security_group_ids | list(string) | [] | no | Extra security group IDs allowed to reach container_port (e.g. Cloud Map peers, sidecar SGs). |
alb_security_group_id | string | null | no | ALB security group ID granted ingress on container_port. When null, no ALB ingress rule is created. |
autoscaling_enabled | bool | true | no | Create CPU target-tracking autoscaling. Disable for single-task services. |
aws_region | string | "us-east-2" | no | AWS region used in CloudWatch log configuration |
container_command | list(string) | null | no | Override the container CMD. Null leaves the image default. |
container_entrypoint | list(string) | null | no | Override the container ENTRYPOINT. Null leaves the image default. |
container_health_check | object({ command = list(string) interval = number timeout = number retries = number start_period = number }) | null | no | Container-level HEALTHCHECK definition. Null leaves the image default. |
container_port | number | 3000 | no | Port the container listens on |
container_secrets | list(object({ name = string valueFrom = string })) | [] | no | Secrets injected into the container. valueFrom is an SSM Parameter ARN or a Secrets Manager secret ARN. |
container_user | string | null | no | Linux user the container runs as. Null omits the field. Chainguard distroless images run as UID 65532; federal posture should set this. |
cpu_architecture | string | "ARM64" | no | CPU architecture (ARM64 or X86_64). ARM64 is cheaper for Fargate. |
cpu_target_value | number | 70 | no | Target average CPU utilization for the autoscaling policy (percent). Lower = scale out sooner. |
desired_count | number | 1 | no | Desired number of running tasks |
drop_all_capabilities | bool | false | no | Set linuxParameters.capabilities.drop = ["ALL"] on the main container. Federal posture should enable this. |
enable_execute_command | bool | false | no | Enable ECS Exec for running tasks. Federal posture: leave disabled by default and only flip on temporarily during incident response. |
environment | string | "" | no | Environment suffix (dev, prod, staging). Leave empty if the account has no env concept. |
environment_variables | map(string) | {} | no | Plain-text environment variables to inject into the container |
ephemeral_storage_gib | number | null | no | Fargate ephemeral storage in GiB (21-200). Null uses the AWS default (20 GiB). Loki write/read benefit from larger scratch space. |
execution_role_arn | string | null | no | Existing task execution role ARN, for accounts where the caller cannot create IAM roles. When set, the module creates no execution role or policies, so that role must already allow image pull, logging, and secret reads. |
extra_ingress_ports | list(object({ description = string port = number protocol = string security_groups = list(string) self = bool })) | [] | no | Additional ingress rules on non-container_port ports (e.g. gRPC port for Loki). |
extra_task_role_inline_policies | map(string) | {} | no | Inline policies to attach to the task role. Map of policy name → JSON string. |
extra_task_role_policy_arns | list(string) | [] | no | Managed/customer policy ARNs to attach to the task IAM role. |
firelens_enable_ecs_log_metadata | bool | false | no | When true, Fluent Bit injects ECS metadata fields (container_id, container_name, ecs_cluster, ecs_task_arn, ecs_task_definition) into every log record. Set to false for cleaner records when the stream name already identifies the source. |
firelens_extra_config_path | string | null | no | Path inside firelens_log_router_image to an extra Fluent Bit config file (parsers, filters). When set, the sidecar's firelensConfiguration receives config-file-type = file + this path. Leave null when using the stock aws-for-fluent-bit image. |
firelens_log_router_image | string | "public.ecr.aws/aws-observability/aws-for-fluent-bit:stable" | no | Fluent Bit image for the firelens log-router sidecar. Only used when main_container_log_configuration.log_driver == "awsfirelens". |
health_check_grace_period_seconds | number | 60 | no | Seconds ECS waits before starting health checks on new tasks. Only applied when target_group_arn is set. |
ingress_self | bool | false | no | Allow ingress on container_port from the task SG itself (for inter-task traffic on the same service). |
init_containers | list(object({ name = string image = string entrypoint = list(string) command = list(string) environment = list(object({ name = string, value = string })) secrets = list(object({ name = string, valueFrom = string })) mount_points = list(object({ sourceVolume = string, containerPath = string, readOnly = bool })) })) | [] | no | Init containers run to completion (essential = false) before the main container starts. The main container declares dependsOn each init container with condition SUCCESS. Used for distroless images that need a config writer step. |
log_group_kms_key_arn | string | null | no | KMS key ARN to encrypt the CloudWatch log group. Null uses AWS-managed encryption. Required for federal posture (FedRAMP SC-13). |
log_retention_days | number | 30 | no | CloudWatch log retention in days |
main_container_log_configuration | object({ log_driver = string options = optional(map(string), {}) secret_options = optional(list(object({ name = string, valueFrom = string })), []) }) | null | no | Override the main container's logConfiguration. When set, replaces the module's default awslogs driver. Used to enable awsfirelens routing via a sidecar log_router. |
main_container_mount_points | list(object({ sourceVolume = string containerPath = string readOnly = bool })) | [] | no | Volume mounts for the main container. |
max_capacity | number | 4 | no | Maximum task count for autoscaling (ignored when autoscaling_enabled = false) |
min_capacity | number | 1 | no | Minimum task count for autoscaling (ignored when autoscaling_enabled = false) |
readonly_root_filesystem | bool | null | no | Mount the container root filesystem read-only. Null omits the field (AWS default = false). Federal posture should set this to true. |
secrets_kms_key_arns | list(string) | [] | no | KMS key ARNs used to encrypt Secrets Manager values. The execution role is granted kms:Decrypt on these. Federal posture should set the customer-managed key here. |
secrets_manager_secret_arns | list(string) | [] | no | Secrets Manager secret ARNs the execution role must be able to read. Federal customers often standardize on Secrets Manager. |
service_name | string | "" | no | Service suffix for multi-service apps (e.g. 'server', 'web'). Leave empty for single-service apps. |
service_registry_arn | string | null | no | Cloud Map service ARN. Set service_registry_record_type to match the service's DNS records. |
service_registry_container_port | number | null | no | Container port advertised for SRV discovery. Defaults to container_port; ignored for A/AAAA records. |
service_registry_record_type | string | "SRV" | no | Cloud Map DNS record type. A/AAAA omit container name and port; SRV includes them. Defaults to SRV to preserve existing registrations. |
ssm_kms_key_arn | string | null | no | KMS key ARN used to encrypt SSM SecureString parameters. When set, the execution role is granted kms:Decrypt on this key. Required for SecureString secrets - usually data.aws_kms_key.ssm.arn. |
ssm_parameter_arns | list(string) | [] | no | ARNs of SSM parameters the execution role must be able to read |
tags | map(string) | {} | no | Tags to apply to all resources |
target_group_arn | string | null | no | ARN of the ALB target group. When null, the service is not registered with any load balancer (Cloud Map only - used for internal/federal services). |
task_cpu | string | "256" | no | Task-level CPU units. Valid: 256, 512, 1024, 2048, 4096 |
task_memory | string | "512" | no | Task-level memory in MiB. Must be compatible with task_cpu. |
task_role_arn | string | null | no | Existing task role ARN, for accounts where the caller cannot create IAM roles. When set, the module creates no task role, so that role must already grant the application's permissions. |
tasks_security_group_description | string | null | no | Description for the tasks security group. Set this when adopting an existing SG - AWS does not allow editing SG descriptions in place, so a mismatch forces replacement. |
volumes | list(object({ name = string })) | [] | no | Ephemeral shared volumes between init containers and the main container. |
Outputs
| Name | Description |
|---|---|
execution_role_arn | Value: local.execution_role_arn |
execution_role_name | Value: reverse(split("/", local.execution_role_arn))[0] |
log_group_arn | ARN of the shared CloudWatch log group. Useful for granting the firelens log_router CloudWatch permissions if it also writes locally. |
log_group_name | Shared CloudWatch log group used by main + init + sidecar containers (when their logConfiguration default is preserved). |
security_group_id | Value: aws_security_group.tasks.id |
service_arn | Value: aws_ecs_service.this.id |
service_id | Value: aws_ecs_service.this.id |
service_name | Value: aws_ecs_service.this.name |
task_definition_arn | Value: aws_ecs_task_definition.this.arn |
task_role_arn | Value: local.task_role_arn |
task_role_name | Value: reverse(split("/", local.task_role_arn))[0] |
Used by
| App | Pinned ref |
|---|---|
| BidBuddy | v1.0.0 |
| castle | v1.6.0 |
| dust | v1.1.0 |
| evergreen | v1.0.0 |
| forager | v1.1.0 |
| ginkgo | v1.6.0 |
| juniper | v1.1.0 |
| magnolia | v1.1.0 |
| meridian | v1.0.0 |
| oak | v1.1.0 |
| poppy | v1.1.0 |
| saplings | v1.3.0 |
| skeddy | v1.6.0 |
| test-health-dashboard | v1.4.0 |