CloudFormation lets you describe your ECS services and task definitions in code, but getting them to deploy reliably is trickier than it looks.

Let’s see it in action. Imagine you’re deploying a simple web service. You’ve got an ECRRepository for your image, a TaskDefinition that specifies your container, and an Service that manages the running tasks.

AWSTemplateFormatVersion: '2010-09-09'
Description: Deploy a simple ECS web service

Resources:
  ECRRepository:
    Type: AWS::ECR::Repository
    Properties:
      RepositoryName: my-web-app-repo

  TaskDefinition:
    Type: AWS::ECS::TaskDefinition
    Properties:
      Family: my-web-app-task
      NetworkMode: awsvpc
      RequiresCompatibilities:
        - Fargate
      Cpu: '256'
      Memory: '512'
      ExecutionRoleArn: !GetAtt ECSTaskExecutionRole.Arn
      ContainerDefinitions:
        - Name: web-container
          Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/my-web-app-repo:latest'
          PortMappings:
            - ContainerPort: 80
              Protocol: tcp

  ECSTaskExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ECRReadPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ecr:GetAuthorizationToken
                  - ecr:BatchCheckLayerAvailability
                  - ecr:GetDownloadUrlForLayer
                  - ecr:BatchGetImage
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*'

  Service:
    Type: AWS::ECS::Service
    Properties:
      Cluster: !Ref ECSCluster # Assuming ECSCluster is defined elsewhere or in this template
      DesiredCount: 1
      TaskDefinition: !Ref TaskDefinition
      NetworkConfiguration:
        AwsvpcConfiguration:
          Subnets:
            - subnet-xxxxxxxxxxxxxxxxx # Replace with actual subnet IDs
            - subnet-yyyyyyyyyyyyyyyyy
          SecurityGroups:
            - sg-zzzzzzzzzzzzzzzzz # Replace with actual security group ID
      LaunchType: Fargate

The mental model here is that CloudFormation is your declarative blueprint. You define the desired state of your ECS resources – the container image, the CPU/memory requirements, the networking setup, and how many instances (tasks) should be running. CloudFormation then translates this blueprint into actual AWS API calls to create or update these resources.

The TaskDefinition is the core of your application’s runtime. It specifies what runs: the container image, environment variables, port mappings, logging configuration, and IAM roles for the task itself. The Service then manages the lifecycle of these tasks: ensuring the desired count is maintained, integrating with load balancers, and handling deployments and rollbacks.

When you update the Image in your TaskDefinition, CloudFormation doesn’t automatically update the running Service with the new image. This is a crucial distinction. The Service resource in CloudFormation has a specific property, TaskDefinition, which points to the ARN of the TaskDefinition. If you just update the TaskDefinition resource in your CloudFormation template, CloudFormation will update the TaskDefinition resource, but it won’t trigger a rolling update on the existing Service unless you explicitly tell it to.

To trigger a rolling update of your Service with a new TaskDefinition revision, you need to make sure the TaskDefinition property of your Service resource changes. The simplest way to achieve this is by updating the Image URI within the TaskDefinition resource’s ContainerDefinitions. When the Image URI changes, CloudFormation sees that the TaskDefinition resource has changed, and because the Service resource depends on it (implicitly through the TaskDefinition property), it will initiate a deployment.

The RequiresCompatibilities and LaunchType properties are key for Fargate deployments. RequiresCompatibilities: [Fargate] tells ECS that this task definition is designed to run on Fargate. LaunchType: Fargate on the Service resource specifies that the service itself should use Fargate as the launch type. These must align.

One common pitfall is how CloudFormation handles updates. If you only change the Image tag in your TaskDefinition (e.g., from latest to v1.1) but don’t force a change to the TaskDefinition resource itself, the Service might not pick it up. The Service resource has a TaskDefinition property that references the ARN of the TaskDefinition. CloudFormation only triggers a service update if this referenced ARN changes. A common pattern to ensure this is to use a dynamic value for the image, like a build ID or commit hash, or to use the ImageDigest for an immutable reference. If you’re using latest, you’ll often need to explicitly update the TaskDefinition resource in CloudFormation to force a new revision.

The ECSTaskExecutionRole is essential for your tasks to pull images from ECR and send logs to CloudWatch. Without the correct permissions, your containers will fail to start.

The next thing you’ll likely want to tackle is integrating this service with an Application Load Balancer for external access.

Want structured learning?

Take the full Cloudformation course →