Trying Out ARM64, FARGATE_SPOT, and ECS Exec, Which ECS Express Mode Doesn't Support
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.
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.
- 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).
- 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: 0to prevent tasks from starting with the dummy image. If you setMinTaskCount: 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
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-stackgenerated a new task definition revision. - I confirmed that
cpuArchitectureinruntimePlatformwas maintained asARM64in 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.
