Managing containerized workloads on Amazon Elastic Container Service (Amazon ECS) requires precise orchestration of compute resources, networking, security, and application configuration. While the ECS console provides a graphical interface for creating task definitions, managing these resources at scale necessitates Infrastructure as Code (IaC). Terraform has emerged as the industry standard for provisioning AWS resources, offering idempotency, state management, and version control. The aws_ecs_task_definition resource is the cornerstone of any ECS deployment in Terraform, defining the exact specifications for how containers are instantiated, what resources they consume, and how they interact with the surrounding AWS ecosystem. This guide provides a deep technical analysis of creating, structuring, and managing ECS task definitions using Terraform, covering Fargate compatibility, multi-container orchestration, secrets management, and dynamic module generation.
Core Architecture and Fargate Compatibility
An ECS task definition is a logical object that describes one or more containers, their resource requirements, and their networking mode. In Terraform, this is managed via the aws_ecs_task_definition resource. A critical distinction in modern AWS architecture is the separation between task-level resources and container-level resources. When deploying on Amazon ECS Fargate, the task definition must explicitly declare the total CPU and memory allocation for the entire task. These values must correspond to specific, valid combinations approved by AWS.
For Fargate, the network_mode must be set to "awsvpc". This mode assigns an Elastic Network Interface (ENI) to each task, allowing each task to have its own IP address and security group. This is a mandatory requirement for Fargate and is strongly recommended for EC2-based tasks to simplify security management and enable better isolation.
The following table outlines the valid CPU and memory combinations for Fargate task definitions. Terraform will reject configurations that do not strictly match one of these pairs.
| CPU (vCPUs) | Memory (MiB) |
|---|---|
| 0.25 | 512 |
| 0.5 | 1024, 2048 |
| 1.0 | 2048, 4096 |
| 2.0 | 4096, 8192 |
| 4.0 | 8192, 16384, 30720 |
Below is a foundational Terraform configuration for a single-container application deployed on Fargate. This example demonstrates the integration of IAM roles, logging configuration, and environment variable injection.
```hcl
resource "awsecstaskdefinition" "app" {
family = "myapp"
requirescompatibilities = ["FARGATE"]
network_mode = "awsvpc"
# Task-level resources
cpu = 512
memory = 1024
# IAM Roles
executionrolearn = awsiamrole.ecsexecution.arn
taskrolearn = awsiamrole.ecstask.arn
# Container Definitions
containerdefinitions = jsonencode([
{
name = "app"
image = "${var.ecrrepositoryurl}:${var.imagetag}"
essential = true
portMappings = [
{
containerPort = 8080
hostPort = 8080
protocol = "tcp"
}
]
# CloudWatch Logs Integration
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "app"
}
}
# Non-sensitive Environment Variables
environment = [
{ name = "PORT", value = "8080" },
{ name = "NODE_ENV", value = var.environment },
{ name = "LOG_LEVEL", value = "info" }
]
# Health Check
healthCheck = {
command = ["CMD-SHELL", "curl -f http://localhost:8080/health || exit 1"]
interval = 30
timeout = 5
retries = 5
startPeriod = 60
}
}
])
}
```
The execution_role_arn is critical for the ECS agent to pull images from Amazon ECR and write logs to CloudWatch. The task_role_arn provides the identity for the container itself to access other AWS services, such as DynamoDB or S3. Confusing these two roles is a common source of permission errors.
Managing Secrets and Sensitive Data
Storing credentials directly in the environment block is a severe security anti-pattern. Environment variables are often visible in process listings and can be leaked through log misconfigurations. Terraform enables secure secret management by integrating with AWS Secrets Manager and AWS Systems Manager (SSM) Parameter Store.
The secrets block in the container definition allows you to reference these external secrets. The key constraint is that the ECS task execution role must have the kms:Decrypt permission for Secrets Manager or the ssm:GetParameter permission for SSM Parameter Store.
The following configuration demonstrates how to inject a database URL from SSM and an API key from Secrets Manager into the application container.
hcl
# Secrets from SSM Parameter Store or Secrets Manager
secrets = [
{
name = "DATABASE_URL"
valueFrom = aws_ssm_parameter.db_url.arn
},
{
name = "API_KEY"
valueFrom = "${aws_secretsmanager_secret.api_key.arn}:api_key::"
}
]
In the second example, the syntax ${aws_secretsmanager_secret.api_key.arn}:api_key:: indicates that the secret is a JSON document. Terraform and the ECS agent parse the JSON and extract the value associated with the key api_key. This mechanism allows a single secret to contain multiple values, which are then mapped to distinct environment variables within the container.
Multi-Container Tasks and Sidecar Patterns
Modern microservices architectures often require more than a single container per task. Common patterns include running a main application container alongside a sidecar container for log aggregation, metrics collection (e.g., Datadog or Prometheus), or reverse proxying (e.g., NGINX or Envoy).
Terraform supports multi-container tasks by defining an array of container objects within container_definitions. Resource allocation can be fine-tuned per container. The sum of container CPU and memory allocations should not exceed the task-level limits, although they are optional for individual containers. If not specified, containers share the task's allocated resources.
A robust pattern involves defining dependencies between containers. The dependsOn block allows you to ensure that a main application container starts only after a supporting sidecar has successfully started.
The following example illustrates a multi-container task with a main application and a Datadog agent sidecar.
```hcl
resource "awsecstaskdefinition" "multicontainer" {
family = "myapp-multi"
requirescompatibilities = ["FARGATE"]
networkmode = "awsvpc"
cpu = 1024
memory = 2048
executionrolearn = awsiamrole.ecsexecution.arn
taskrolearn = awsiamrole.ecstask.arn
containerdefinitions = jsonencode([
# Main Application Container
{
name = "app"
image = "${var.ecrrepositoryurl}:${var.imagetag}"
essential = true
cpu = 768
memory = 1536
portMappings = [{
containerPort = 8080
hostPort = 8080
protocol = "tcp"
}]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "app"
}
}
environment = [
{ name = "PORT", value = "8080" }
]
# Dependency on the sidecar
dependsOn = [{
containerName = "datadog-agent"
condition = "START"
}]
},
# Datadog Agent Sidecar
{
name = "datadog-agent"
image = "public.ecr.aws/datadog/agent:latest"
essential = false
cpu = 256
memory = 512
environment = [
{ name = "ECS_FARGATE", value = "true" },
{ name = "DD_APM_ENABLED", value = "true" }
]
secrets = [
{
name = "DD_API_KEY"
valueFrom = aws_ssm_parameter.dd_api_key.arn
}
]
logConfiguration = {
logDriver = "awslogs"
options = {
"awslogs-group" = aws_cloudwatch_log_group.app.name
"awslogs-region" = var.aws_region
"awslogs-stream-prefix" = "datadog"
}
}
}
])
}
```
In this configuration, the datadog-agent is marked as essential = false. This means that if the agent crashes, the ECS task will not be terminated, ensuring the application remains available. Conversely, if the application crashes, the entire task is stopped. The dependsOn condition ensures the Datadog agent starts before the application, which is useful if the agent needs to establish a connection to the collector before the application generates logs or metrics.
Versioning and State Management
One of the most significant advantages of using Terraform for ECS task definitions is the immutable nature of task definition revisions. Each time you modify a task definition, ECS creates a new revision. The original revisions are preserved in the AWS cloud, allowing for instant rollbacks if a new configuration causes failures.
Terraform manages this versioning automatically. When you update the resource, Terraform detects the change and applies a new revision. You can reference the current revision number and ARN in your outputs to monitor the deployment state.
```hcl
output "taskdefinitionarn" {
description = "Full ARN including revision number"
value = awsecstask_definition.app.arn
}
output "taskdefinitionrevision" {
description = "Current revision number"
value = awsecstask_definition.app.revision
}
```
When defining an ECS Service, you can choose to reference the task definition by family name (which always points to the latest revision) or by a specific ARN. Referencing the family name is simpler for continuous deployment pipelines but offers less control over which revision is running. Referencing a specific ARN allows for precise pinning of the running version, which is often preferred in strict compliance environments.
Reading Existing Task Definitions with Data Sources
While creating new task definitions is common, many workflows require interacting with existing task definitions defined outside of Terraform or managed by another team. Terraform provides the aws_ecs_task_definition data source to retrieve details about a specific task definition.
This is particularly useful when you need to reference the container name or port mappings of an existing task in a load balancer or service definition without hardcoding those values.
```hcl
data "awsecstaskdefinition" "existingapp" {
family_name = "my-existing-app"
}
resource "awsecsservice" "existingappservice" {
name = "my-existing-app-service"
cluster = awsecscluster.main.id
taskdefinition = data.awsecstaskdefinition.existing_app.arn
# Use data source values for load balancer target group
# ...
}
```
Dynamic Generation with Terraform Modules
For organizations managing hundreds of task definitions, writing raw aws_ecs_task_definition resources for each container can lead to code duplication and maintenance overhead. The terraform-aws-ecs-task-definition module provides a standardized way to generate valid task definitions dynamically. This module is compatible with Terraform v0.12.x and later.
The module accepts parameters that mirror the ContainerDefinition object. It is designed to make it easy to define container-specific parameters and output a valid task definition.
```hcl
provider "aws" {}
module "mongo-task-definition" {
source = "github.com/mongodb/terraform-aws-ecs-task-definition"
family = "mongo"
image = "mongo:3.6"
memory = 512
name = "mongo"
portMappings = [
{
containerPort = 27017
}
]
}
```
When applied, this module generates a task definition with the following container definition structure. Note that many fields are null by default, which is valid in the ECS API schema.
json
[
{
"command": null,
"cpu": null,
"disableNetworking": false,
"dnsSearchDomains": null,
"dnsServers": null,
"dockerLabels": null,
"dockerSecurityOptions": null,
"entryPoint": null,
"environment": null,
"essential": true,
"extraHosts": null,
"healthCheck": null,
"hostname": null,
"image": "mongo:3.6",
"interactive": false,
"links": null,
"linuxParameters": null,
"logConfiguration": null,
"memory": 512,
"memoryReservation": null,
"mountPoints": null,
"name": "mongo",
"portMappings": [
{
"containerPort": 27017
}
],
"privileged": false,
"pseudoTerminal": false,
"readonlyRootFilesystem": false,
"repositoryCredentials": null,
"resourceRequirements": null,
"secrets": null,
"systemControls": null,
"ulimits": null,
"user": null,
"volumesFrom": null,
"workingDirectory": null
}
]
Using modules ensures consistency across your infrastructure. It also simplifies the process of updating ECS task definitions and triggering new service deployments automatically. By encapsulating the logic for task definition creation, teams can focus on the application-specific parameters while the module handles the AWS-specific schema requirements.
Conclusion
The aws_ecs_task_definition resource in Terraform is a powerful abstraction that bridges the gap between container orchestration and cloud infrastructure management. By leveraging Terraform, teams can achieve full reproducibility of their ECS environments. Key takeaways include the strict adherence to Fargate CPU/memory combinations, the critical use of awsvpc networking mode, and the mandatory separation of execution and task IAM roles.
Security best practices dictate that all sensitive data must be managed via the secrets block, referencing AWS Secrets Manager or SSM Parameter Store, rather than using plain environment variables. For complex applications, multi-container tasks with sidecars and defined dependencies (dependsOn) allow for sophisticated observability and proxying setups. Furthermore, the immutable nature of task definition revisions provides a safety net for rollbacks, while Terraform modules offer a scalable path for managing large fleets of tasks.
Mastering these configurations allows DevOps and engineering teams to deploy resilient, secure, and observable containerized applications on AWS. The depth of control offered by Terraform—from granular health check configurations to detailed log streaming options—ensures that every aspect of the container lifecycle is governed by code, audit, and version control.