Terraform Task Definitions for AWS ECS Fargate with Secrets, Health Checks and Module Reuse

Creating an Amazon ECS task definition through Terraform is the mechanism by which the desired state of container execution on Fargate is codified. The task definition resource describes the complete execution envelope for one or more containers: the compute and memory allocation, the container image reference, port mappings, logging configuration, environment variables, secret references, health check commands, and IAM roles that govern execution and task permissions. When this definition is expressed as code, changes produce new revisions, services can be pinned to a specific revision or roll to the latest, and the entire lifecycle becomes repeatable across environments. The reference material covers single and multi-container Fargate tasks, secrets management via SSM Parameter Store and Secrets Manager, health checks, resource sizing, parameterized reusable definitions, module-based encapsulation, and migration patterns toward Consul service mesh integration.

The practical consequence for teams is that infrastructure drift is removed from container orchestration. A task definition written in Terraform replaces manual console clicks with version-controlled configuration, allowing peer review, automated testing, and consistent promotion from development to production. The definition also becomes the single source of truth for security posture, because execution and task roles, secret injection, and network mode are all declared explicitly. When combined with parameterized variables, the same definition can be reused for multiple services without duplication, reducing copy-paste errors and accelerating new service onboarding.

Basic Fargate Task Definition Structure

A basic task definition for Fargate begins with the resource type aws_ecs_task_definition and a set of top-level attributes that define the task execution context.

resource "aws_ecs_task_definition" "app" { family = "myapp" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" # Required for Fargate cpu = 512 # 0.5 vCPU (256, 512, 1024, 2048, 4096) memory = 1024 # 1 GB execution_role_arn = aws_iam_role.ecs_execution.arn task_role_arn = aws_iam_role.ecs_task.arn container_definitions = jsonencode([ ... ]) }

The family attribute names the task definition family. The family is used by ECS services to locate task definitions and to group revisions. Changing the family creates a new logical family, while changing attributes within the same family creates a new revision.

requires_compatibilities = ["FARGATE"] restricts the task to the Fargate launch type. This choice removes the need to manage EC2 instances and enforces serverless scheduling. The impact is that the task cannot be run on EC2 launch type and must conform to Fargate networking and resource constraints.

network_mode = "awsvpc" is required for Fargate. It places each task in its own elastic network interface within the selected subnets. This enables security group isolation per task and direct connectivity to AWS resources without a host network.

Task-level resources cpu = 512 and memory = 1024 define the total allocation for the task. The comment indicates 512 is 0.5 vCPU and the valid CPU values are 256, 512, 1024, 2048, 4096. Memory is 1 GB for 1024. The real-world consequence is that containers within the task share this pool. Over allocation increases cost, under allocation causes OOM kills and throttling. The CPU and memory values must be a valid Fargate combination.

IAM roles are supplied via execution_role_arn and task_role_arn. The execution role allows ECS to pull images, write logs, and manage task metadata on behalf of the task. The task role grants permissions the containers themselves may assume. Separating these roles follows least privilege and prevents a compromise in one container from granting broad execution privileges.

Container definitions are supplied as a JSON-encoded list. The example defines a single container named app with an image reference built from variables, essential = true, and a port mapping.

{ name = "app" image = "${var.ecr_repository_url}:${var.image_tag}" essential = true portMappings = [ { containerPort = 8080 hostPort = 8080 protocol = "tcp" } ] }

An essential container determines task health. If the essential container exits, the entire task is stopped. Port mappings expose the container port to the host network namespace. For Fargate, hostPort matches containerPort and the mapping is logical rather than host-bound.

Logging is configured with logConfiguration using the awslogs log driver. Options include log group name, region, and stream prefix. This ensures container stdout and stderr are streamed to CloudWatch Logs for observability and alerting. Centralized logging enables log retention policies and metric filters without modifying container code.

Environment variables are declared explicitly:

environment = [ { name = "PORT", value = "8080" }, { name = "NODE_ENV", value = var.environment }, { name = "LOG_LEVEL", value = "info" } ]

Environment variables are visible to the process and to anyone with task inspect permissions. Sensitive values should not be placed here.

Secrets are injected via the secrets block with valueFrom referencing SSM Parameter Store or Secrets Manager ARNs:

secrets = [ { name = "DATABASE_URL" valueFrom = aws_ssm_parameter.db_url.arn }, { name = "API_KEY" valueFrom = "${aws_secretsmanager_secret.api_key.arn}:api_key::" } ]

Using SSM Parameter Store or Secrets Manager avoids hard-coding credentials in environment variables or image layers. The impact is that secret rotation can be performed centrally without rebuilding the task definition, and access is audited through IAM.

Health checks are defined inside the container definition:

healthCheck = { command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"] interval = 30 timeout = 5 retries =

A health check command runs inside the container at the specified interval. The interval is 30 seconds, timeout 5 seconds. The command uses curl -f to fail on HTTP errors. If the health check fails repeatedly, ECS marks the container unhealthy and may stop the task depending on service settings. This provides early detection of application failures without external monitoring.

Parameterized and Reusable Definitions

Terraform enables reuse by parameterizing the task definition. The reference material shows a parameterized definition using variables for family, container name, image, CPU, memory, and environment.

resource "aws_ecs_task_definition" "app" { family = var.task_family container_definitions = jsonencode([ { name = "${var.container_name}" image = "${var.container_image}" cpu = var.cpu memory = var.memory essential = true environment = var.environment_variables logConfiguration = { logDriver = "awslogs" options = { "awslogs-group" = var.log_group "awslogs-region" = var.region "awslogs-stream-prefix" = var.log_stream_prefix } } } ]) requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" execution_role_arn = aws_iam_role.ecs_task_execution.arn task_role_arn = aws_iam_role.ecs_task.arn cpu = var.task_cpu memory = var.task_memory }

Variables declared include task_family, container_name, container_image, cpu, memory, environment_variables. Parameterization allows the same module to be applied to multiple services by overriding variables at call time. The impact is reduced duplication and consistent defaults across environments.

The reference notes that by utilizing Terraform you can simplify creation of reusable ECS task definitions, pass dynamic variables to tasks and services, and seamlessly integrate with other AWS services like Secrets Manager and CloudWatch. This integration is achieved by referencing data sources and resources from the same state, ensuring IAM roles, log groups, and secret ARNs exist before the task definition is created.

ECS Task Definitions define container settings such as CPU, memory, image, and other attributes. ECS Services manage the number of tasks, scaling policies, and integration with ALBs. The separation allows the task definition to remain immutable while the service controls desired count and load balancing.

Logging, Secrets and Service Integration

AWS Services Integration connects to Secrets Manager for sensitive data and enables CloudWatch logging and metrics. The log configuration options reference aws_cloudwatch_log_group.app.name and var.aws_region. This ties the task definition to a log group resource, ensuring logs are retained and searchable.

Secrets from SSM Parameter Store or Secrets Manager are injected at launch time. The reference emphasizes never putting credentials in environment variables directly. The practical consequence is that credential leakage via process inspection or image history is avoided. Rotation can be performed by updating the parameter or secret version without redeploying the container image.

Multi-container tasks let you run sidecars for monitoring, logging, or proxying alongside the application. The container_definitions list can contain multiple objects, each with its own CPU, memory, ports, and health checks. Essential containers control task lifecycle, while non-essential sidecars can be restarted independently.

Versioning and Outputs

Each time a task definition changes, Terraform creates a new revision. Old revisions are kept. This enables rollback by referencing a previous revision ARN.

output "task_definition_arn" { description = "Full ARN including revision number" value = aws_ecs_task_definition.app.arn } output "task_definition_revision" { description = "Current revision number" value = aws_ecs_task_definition.app.revision }

Outputs provide the family name, ARN, revision, container name, and container port for downstream modules.

output "task_definition_family" { description = "Task definition family name" value = aws_ecs_task_definition.app.family } output "task_definition_arn" { description = "Task definition ARN" value = aws_ecs_task_definition.app.arn } output "container_name" { description = "Main container name" value = "app" } output "container_port" { description = "Main container port" value = 8080 }

Services reference the task definition family and can be configured to use the latest revision or a pinned revision. Pinning provides stability for production deployments, while referencing latest enables automatic adoption of new revisions after a successful Terraform apply.

Module-Based Approaches

The opstimus module creates an Amazon ECS task definition and optionally an IAM role with a policy for the task. It supports configuration for CPU, memory, and container definitions, and integrates with Fargate for serverless container management.

Module requirements:

  • terraform >= 1.3.0
  • aws >= 6.0

Module inputs:

  • project: Project name, string, required
  • environment: Environment name, string, required
  • service: Service name e.g., api, string, required
  • executionrolearn: ARN of the execution role, string, required
  • cpu: CPU size e.g., 256, number, default 256, optional
  • memory: Memory size e.g., 512, number, default 512, optional
  • container_definitions: JSON formatted container definitions, string, required
  • taskrolepolicy: Inline policy for the task role, string, default "", optional

Module outputs:

  • taskdefinitionarn: The ARN of the ECS task definition
  • taskrolearn: ARN of the task IAM role this module created, or null when taskrolepolicy was empty

The module is now located in the monorepo opstimus/terraform-modules at modules/aws-task-definition with source git::https://github.com/opstimus/terraform-modules.git//modules/aws-task-definition?ref=aws-task-definition/v2.1.0. The original repository remains for existing consumers.

The Terraform Foundation module provides a different approach for generating a valid Amazon ECS Task Definition dynamically. The module is not compatible with Terraform versions less than v0.12.x. Its purpose is to generate a valid Amazon ECS Task Definition dynamically. A task definition is required to run Docker containers in Amazon ECS. A task definition contains a list of container definitions received by the Docker daemon to create a container instance.

The module supports having Terraform generate valid task definitions dynamically and update the ECS task definition and trigger new service deployments automatically. It uses the same parameters as the ContainerDefinition object.

Both module approaches centralize common patterns such as IAM role creation, default CPU and memory values, and container definition JSON encoding. This reduces boilerplate and enforces organizational standards.

Migration and Consul Integration

Migrating existing tasks to Consul requires rewriting Terraform code so container definitions include the mesh-task Terraform module. Tasks must already be defined in Terraform using the ecs_task_definition resource so they can be converted to use the mesh-task module.

Example existing task definition:

resource "aws_ecs_task_definition" "my_task" { family = "my_task" requires_compatibilities = ["FARGATE"] network_mode = "awsvpc" cpu = 256 memory = 512 execution_role_arn = "arn:aws:iam::111111111111:role/execution-role" task_role_arn = "arn:aws:iam::111111111111:role/task-role" container_definitions = jsonencode( [{ name = "example-client-app" image = "docker.io/org/my_task:v0.0.1" essential = true portMappings = [ { containerPort = 9090 hostPort = 9090 protocol = "tcp" } ] cpu = 0 mountPoints = [] volumesFrom = [] }] ) }

Corresponding service:

resource "aws_ecs_service" "my_task" { name = "my_task" cluster = "arn:aws:ecs:us-east-1:111111111111:cluster/my-cluster" task_definition = aws_ecs_task_definition.my_task.arn desired_count = 1 network_configuration { subnets = ["subnet-abc123"] } launch_type = "FARGATE" }

Replacing the aws_ecs_task_definition resource with the mesh-task module allows Consul to add necessary dataplane containers that enable the task to join the mesh. The impact is service discovery and mTLS are injected without modifying application code, but the task definition must be authored in Terraform first to allow module substitution.

Best Practices Summary

Creating a robust ECS deployment often involves repetitive configurations and complex setups. Deploying ECS services using Terraform allows you to maintain a modular, reusable, and parameter-driven infrastructure. The reference material emphasizes focusing on creating and reusing ECS task definitions, configuring services, and integrating other AWS components effectively with Terraform.

Key practices derived from the reference:

  • Use network_mode = "awsvpc" for Fargate and choose valid CPU/memory combinations.
  • Use SSM Parameter Store or Secrets Manager for sensitive values, never put credentials in environment variables directly.
  • Define health checks with reasonable interval and timeout to detect failures early.
  • Output the task definition ARN and revision for service references.
  • Parameterize family, image, CPU, memory, and environment to enable reuse.
  • Use modules to encapsulate IAM role creation and default values.
  • Keep task definitions in Terraform before migrating to additional modules like mesh-task for Consul integration.

The cumulative effect is infrastructure that is auditable, repeatable, and safe to change. Task definitions become versioned artifacts, services become declarative controllers, and secrets remain out of source control.

Sources

  1. Create ECS Task Definitions in Terraform
  2. Best Practices for ECS Deployment Using Terraform
  3. Migrate ECS Tasks to Consul
  4. opstimus Terraform AWS Task Definition
  5. Terraform Foundation ECS Task Definition

Related Posts