Trying Out ARM64, FARGATE_SPOT, and ECS Exec, Which ECS Express Mode Doesn't Support

Trying Out ARM64, FARGATE_SPOT, and ECS Exec, Which ECS Express Mode Doesn't Support

Explains a two-stage deployment process that works around ECS Express Mode limits by enabling ARM64 (Graviton), Fargate Spot, and ECS Exec afterward, with CloudFormation and AWS CLI steps.
2026.02.11

This page has been translated by machine translation. View original

ECS Express Mode is a convenient feature that lets you quickly build an environment with minimal configuration, but features such as ARM64 (Graviton), Fargate Spot, and ECS Exec cannot be configured at creation time.

I recently had an opportunity to try enabling these features in a development environment that uses ECS Express Mode, in order to reduce costs and improve debugging efficiency, so I'll introduce the steps here.

https://dev.classmethod.jp/articles/ecs-express-mode-custom-task-definition/

Prerequisites

  • A production container image built for the ARM64 (aarch64) architecture has been registered in ECR
  • The stack for the initial setup of ECS Express Mode (TaskExecutionRole, InfrastructureRole, etc.) has already been deployed

The Problem: Express Mode Constraints

ECS Express Mode (AWS::ECS::ExpressGatewayService) is a high-level component that automatically creates and manages low-level resources such as task definitions, services, ALBs, and Auto Scaling internally. As a result, properties that can be specified in regular CloudFormation, such as runtimePlatform and capacityProviderStrategy, do not exist in the Express Mode template.

The architecture is fixed to x86_64 and cannot be changed even through the update-express-gateway-service API.

To work around this constraint, I carried out the following two-stage deployment.

  1. Dummy deployment: Create the stack with an x86 dummy image that has the same port and path as production (suppressing startup by setting the task count to 0).
  2. Architecture switch: Edit the automatically generated task definition to ARM64 via the CLI and apply it with update-service.

Step 1: Create the Initial Stack with CloudFormation

First, I used an x86 dummy image to successfully deploy the resources managed by Express Mode.

Verification Code (CloudFormation Template)

  • To use ECS Exec, I configured the IAM for SSM at the time of setting up the initial template.
  • I specified MinTaskCount: 0 to prevent tasks from starting with the dummy image. If you set MinTaskCount: 1, and the dummy image fails the health check, the circuit breaker may trigger and you may fall into a rollback loop.
Full Initial Template
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ECS Express Mode - 初回設置用(x86ダミー)'

Parameters:
  ServiceName:
    Type: String
    Default: 'my-app-dev'
  InitialStackName:
    Type: String
    Default: 'ecs-express-initial'

Resources:
  ECSLogGroup:
    Type: AWS::Logs::LogGroup
    Properties:
      LogGroupName: !Sub '/aws/ecs/default/${ServiceName}-service'
      RetentionInDays: 14

  TaskRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub '${ServiceName}-task-role'
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service: ecs-tasks.amazonaws.com
            Action: sts:AssumeRole
      Policies:
        - PolicyName: ECSExecPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  - ssmmessages:CreateControlChannel
                  - ssmmessages:CreateDataChannel
                  - ssmmessages:OpenControlChannel
                  - ssmmessages:OpenDataChannel
                Resource: '*'

  ExpressModeService:
    Type: AWS::ECS::ExpressGatewayService
    DependsOn: ECSLogGroup
    Properties:
      ServiceName: !Sub '${ServiceName}-service'
      Cluster: 'default'
      ExecutionRoleArn:
        Fn::ImportValue: !Sub '${InitialStackName}-TaskExecutionRoleArn'
      InfrastructureRoleArn:
        Fn::ImportValue: !Sub '${InitialStackName}-InfrastructureRoleArn'
      TaskRoleArn: !GetAtt TaskRole.Arn
      PrimaryContainer:
        Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/my-app:dummy'
        ContainerPort: 8000
        AwsLogsConfiguration:
          LogGroup: !Sub '/aws/ecs/default/${ServiceName}-service'
          LogStreamPrefix: 'ecs'
      NetworkConfiguration:
        Subnets:
          Fn::Split:
            - ','
            - Fn::ImportValue: !Sub '${InitialStackName}-PrivateSubnets'
      Cpu: '256'
      Memory: '512'
      HealthCheckPath: '/health'
      ScalingTarget:
        MinTaskCount: 0
        MaxTaskCount: 1
        AutoScalingMetric: 'AVERAGE_CPU'
        AutoScalingTargetValue: 70

Outputs:
  ServiceEndpoint:
    Value: !GetAtt ExpressModeService.Endpoint

I deployed this template and confirmed that the task definition's runtimePlatform was generated with the default x86_64. Because of MinTaskCount: 0, no tasks start at this point.


Step 1.5: Disable the Bake Time

To shorten the working time before the conversion, I set the deployment bake time to 0 in the Management Console.

  • ECS → Clusters → Service → Deployment settings → Bake time: 0 minutes

https://dev.classmethod.jp/articles/ecs-experss-reduce-deployment-time/


Step 2: Convert the Task Definition to ARM64 and Apply It

I retrieved the task definition revision automatically generated by Express Mode, changed cpuArchitecture to ARM64, replaced the image with the production ARM image, and also changed the CPU and memory to the production specs before registering it.

SERVICE_NAME="my-app-dev-service"
TD_FAMILY="default-${SERVICE_NAME}"
REGION="ap-northeast-1"
ARM_IMAGE="123456789012.dkr.ecr.$REGION.amazonaws.com/my-app:latest"

# 1. Get the current task definition
LATEST=$(aws ecs list-task-definitions --family-prefix $TD_FAMILY --sort DESC \
    --region $REGION --query 'taskDefinitionArns[0]' --output text)

aws ecs describe-task-definition --task-definition $LATEST \
    --region $REGION --query 'taskDefinition' > /tmp/td.json

# 2. Change to ARM64 + production image (including removal of metadata)
jq --arg img "$ARM_IMAGE" '
  .runtimePlatform.cpuArchitecture = "ARM64" |
  .containerDefinitions[0].image = $img |
  .cpu = "512" | .memory = "1024" |
  .containerDefinitions[0].cpu = 512 |
  .containerDefinitions[0].memoryReservation = 1024 |
  del(.taskDefinitionArn, .revision, .status, .requiresAttributes,
      .compatibilities, .registeredAt, .registeredBy, .enableFaultInjection)
' /tmp/td.json > /tmp/td_arm.json

# 3. Register the new revision
aws ecs register-task-definition \
    --cli-input-json file:///tmp/td_arm.json \
    --region $REGION

# 4. Update the service
NEW_REV=$(aws ecs list-task-definitions --family-prefix $TD_FAMILY --sort DESC \
    --region $REGION --query 'taskDefinitionArns[0]' --output text)

aws ecs update-service \
    --cluster default \
    --service $SERVICE_NAME \
    --task-definition $NEW_REV \
    --force-new-deployment \
    --region $REGION

Checking Behavior When Running update-stack

After switching to ARM64 via the CLI, I ran CloudFormation's update-stack and changed the image in the template to the production image for ARM64. This way, even if a rollback occurs during later CloudFormation operations, the image referenced by the template will be the ARM64 one, which reduces the risk of an exec format error.

  • Running update-stack generated a new task definition revision.
  • I confirmed that cpuArchitecture in runtimePlatform was maintained as ARM64 in the new revision as well.
  • I confirmed that the update to the image specified in the template was reflected in the new revision.

I was able to confirm that when updating the stack, Express Mode inherits the architecture setting from the currently running task definition.


Step 3: Enable FARGATE_SPOT and ECS Exec

After converting to ARM64, I further enabled cost optimization and debugging features.

aws ecs update-service \
    --cluster default \
    --service my-app-dev-service \
    --capacity-provider-strategy capacityProvider=FARGATE_SPOT,base=0,weight=1 \
    --enable-execute-command \
    --force-new-deployment \
    --region ap-northeast-1

Verification

After the settings were applied, I started a task and verified each setting.

aws ecs update-service \
    --cluster default \
    --service my-app-dev-service \
    --desired-count 1 \
    --region ap-northeast-1

After the task started, I confirmed that it was running on FARGATE_SPOT.

aws ecs describe-tasks \
    --cluster default \
    --tasks <task-id> \
    --query 'tasks[0].capacityProviderName' \
    --region ap-northeast-1
# => "FARGATE_SPOT"

I logged in to the container with ECS Exec and confirmed that it was running on ARM64. In Express Mode the container name is automatically set to Main, so I specified --container Main.

aws ecs execute-command \
    --cluster default \
    --task <task-id> \
    --container Main \
    --interactive \
    --command "/bin/sh"

# コンテナ内で実行
$ uname -m
aarch64

aarch64 was displayed, confirming that it is running on ARM64 (Graviton).


Summary

Even with ECS Express Mode, I was able to use ARM64, Fargate Spot, and ECS Exec by going through a two-stage deployment process. The safe deployment pattern is to set MinTaskCount: 0 in the initial stack to prevent tasks from starting with the dummy image, and then change it to the required number of tasks after the ARM64 conversion.

However, since these steps are not explicitly documented in the official documentation at this time, behavior may change with future updates, and issues such as interference from changes made outside IaC and drift may occur. I recommend limiting use to environments where you can use it at your own risk, such as development and testing environments.

Share this article

AWSのお困り事はクラスメソッドへ