Kaizen

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_target and 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

main.tf
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
}
SettingCommercial defaultFederal settingNote
aws_regionus-east-2us-gov-west-1Feeds the awslogs configuration. Set it to the stack's region.
log_group_kms_key_arnnull (AWS managed)customer managed KMS key ARNFedRAMP SC-13.
log_retention_days3090FedRAMP AU-11, as set in examples/federal.
readonly_root_filesystemnull (AWS default false)true
container_usernull (image default)"65532"The UID that Chainguard distroless images run as.
drop_all_capabilitiesfalsetrue
secrets_kms_key_arns[]the customer managed key that encrypts Secrets Manager values
execution_role_arn, task_role_arnnull (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

NameTypeDefaultRequiredDescription
app_namestringyesApplication name (e.g. 'oak-prototype', 'bidbuddy')
cluster_namestringyesECS cluster name (use module.ecs_cluster.cluster_name)
container_imagestringyesFull container image URI including tag (e.g. ECR or cgr.dev for Chainguard distroless)
private_subnet_idslist(string)yesPrivate subnet IDs for ECS tasks
vpc_idstringyesVPC ID
additional_ingress_security_group_idslist(string)[]noExtra security group IDs allowed to reach container_port (e.g. Cloud Map peers, sidecar SGs).
alb_security_group_idstringnullnoALB security group ID granted ingress on container_port. When null, no ALB ingress rule is created.
autoscaling_enabledbooltruenoCreate CPU target-tracking autoscaling. Disable for single-task services.
aws_regionstring"us-east-2"noAWS region used in CloudWatch log configuration
container_commandlist(string)nullnoOverride the container CMD. Null leaves the image default.
container_entrypointlist(string)nullnoOverride the container ENTRYPOINT. Null leaves the image default.
container_health_checkobject({ command = list(string) interval = number timeout = number retries = number start_period = number })nullnoContainer-level HEALTHCHECK definition. Null leaves the image default.
container_portnumber3000noPort the container listens on
container_secretslist(object({ name = string valueFrom = string }))[]noSecrets injected into the container. valueFrom is an SSM Parameter ARN or a Secrets Manager secret ARN.
container_userstringnullnoLinux user the container runs as. Null omits the field. Chainguard distroless images run as UID 65532; federal posture should set this.
cpu_architecturestring"ARM64"noCPU architecture (ARM64 or X86_64). ARM64 is cheaper for Fargate.
cpu_target_valuenumber70noTarget average CPU utilization for the autoscaling policy (percent). Lower = scale out sooner.
desired_countnumber1noDesired number of running tasks
drop_all_capabilitiesboolfalsenoSet linuxParameters.capabilities.drop = ["ALL"] on the main container. Federal posture should enable this.
enable_execute_commandboolfalsenoEnable ECS Exec for running tasks. Federal posture: leave disabled by default and only flip on temporarily during incident response.
environmentstring""noEnvironment suffix (dev, prod, staging). Leave empty if the account has no env concept.
environment_variablesmap(string){}noPlain-text environment variables to inject into the container
ephemeral_storage_gibnumbernullnoFargate ephemeral storage in GiB (21-200). Null uses the AWS default (20 GiB). Loki write/read benefit from larger scratch space.
execution_role_arnstringnullnoExisting 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_portslist(object({ description = string port = number protocol = string security_groups = list(string) self = bool }))[]noAdditional ingress rules on non-container_port ports (e.g. gRPC port for Loki).
extra_task_role_inline_policiesmap(string){}noInline policies to attach to the task role. Map of policy name → JSON string.
extra_task_role_policy_arnslist(string)[]noManaged/customer policy ARNs to attach to the task IAM role.
firelens_enable_ecs_log_metadataboolfalsenoWhen 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_pathstringnullnoPath 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_imagestring"public.ecr.aws/aws-observability/aws-for-fluent-bit:stable"noFluent Bit image for the firelens log-router sidecar. Only used when main_container_log_configuration.log_driver == "awsfirelens".
health_check_grace_period_secondsnumber60noSeconds ECS waits before starting health checks on new tasks. Only applied when target_group_arn is set.
ingress_selfboolfalsenoAllow ingress on container_port from the task SG itself (for inter-task traffic on the same service).
init_containerslist(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 })) }))[]noInit 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_arnstringnullnoKMS key ARN to encrypt the CloudWatch log group. Null uses AWS-managed encryption. Required for federal posture (FedRAMP SC-13).
log_retention_daysnumber30noCloudWatch log retention in days
main_container_log_configurationobject({ log_driver = string options = optional(map(string), {}) secret_options = optional(list(object({ name = string, valueFrom = string })), []) })nullnoOverride 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_pointslist(object({ sourceVolume = string containerPath = string readOnly = bool }))[]noVolume mounts for the main container.
max_capacitynumber4noMaximum task count for autoscaling (ignored when autoscaling_enabled = false)
min_capacitynumber1noMinimum task count for autoscaling (ignored when autoscaling_enabled = false)
readonly_root_filesystemboolnullnoMount the container root filesystem read-only. Null omits the field (AWS default = false). Federal posture should set this to true.
secrets_kms_key_arnslist(string)[]noKMS 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_arnslist(string)[]noSecrets Manager secret ARNs the execution role must be able to read. Federal customers often standardize on Secrets Manager.
service_namestring""noService suffix for multi-service apps (e.g. 'server', 'web'). Leave empty for single-service apps.
service_registry_arnstringnullnoCloud Map service ARN. Set service_registry_record_type to match the service's DNS records.
service_registry_container_portnumbernullnoContainer port advertised for SRV discovery. Defaults to container_port; ignored for A/AAAA records.
service_registry_record_typestring"SRV"noCloud Map DNS record type. A/AAAA omit container name and port; SRV includes them. Defaults to SRV to preserve existing registrations.
ssm_kms_key_arnstringnullnoKMS 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_arnslist(string)[]noARNs of SSM parameters the execution role must be able to read
tagsmap(string){}noTags to apply to all resources
target_group_arnstringnullnoARN 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_cpustring"256"noTask-level CPU units. Valid: 256, 512, 1024, 2048, 4096
task_memorystring"512"noTask-level memory in MiB. Must be compatible with task_cpu.
task_role_arnstringnullnoExisting 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_descriptionstringnullnoDescription 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.
volumeslist(object({ name = string }))[]noEphemeral shared volumes between init containers and the main container.

Outputs

NameDescription
execution_role_arnValue: local.execution_role_arn
execution_role_nameValue: reverse(split("/", local.execution_role_arn))[0]
log_group_arnARN of the shared CloudWatch log group. Useful for granting the firelens log_router CloudWatch permissions if it also writes locally.
log_group_nameShared CloudWatch log group used by main + init + sidecar containers (when their logConfiguration default is preserved).
security_group_idValue: aws_security_group.tasks.id
service_arnValue: aws_ecs_service.this.id
service_idValue: aws_ecs_service.this.id
service_nameValue: aws_ecs_service.this.name
task_definition_arnValue: aws_ecs_task_definition.this.arn
task_role_arnValue: local.task_role_arn
task_role_nameValue: reverse(split("/", local.task_role_arn))[0]

Used by

AppPinned ref
BidBuddyv1.0.0
castlev1.6.0
dustv1.1.0
evergreenv1.0.0
foragerv1.1.0
ginkgov1.6.0
juniperv1.1.0
magnoliav1.1.0
meridianv1.0.0
oakv1.1.0
poppyv1.1.0
saplingsv1.3.0
skeddyv1.6.0
test-health-dashboardv1.4.0

On this page