I tried configuring readonlyRootFilesystem + tmpfs in a custom task definition for ECS Express Mode with CloudFormation
This page has been translated by machine translation. View original
Introduction
In July 2026, ECS Express Mode added support for custom task definitions.
In the article above, we created an Express Mode service using the CLI. This time, having confirmed that CloudFormation also supports custom task definitions, we will walk through the steps to declaratively configure readonlyRootFilesystem and tmpfs.
What We Verified
Verification Environment
| Item | Value |
|---|---|
| Region | us-west-2 (Oregon) |
| Stack Name | express-nginx-readonly-v2 |
| Resource Type | AWS::ECS::ExpressGatewayService |
| Task Definition | AWS::ECS::TaskDefinition (custom) |
| CPU / Memory | 0.25 vCPU / 512 MiB |
| Architecture | ARM64 |
| Container | Custom image based on nginx:1.27.5-alpine (ECR, listening on port 8080, responds to /health) |
CloudFormation Template
readonlyRootFilesystem + tmpfs
Set ReadonlyRootFilesystem: true in the container definition and mount tmpfs at paths that require write access.
ContainerDefinitions:
- Name: Main
ReadonlyRootFilesystem: true
LinuxParameters:
Tmpfs:
- ContainerPath: /tmp
Size: 128
MountOptions:
- noexec
- nosuid
- ContainerPath: /var/lib/amazon/ssm
Size: 16
MountOptions:
- nosuid
- ContainerPath: /var/log/amazon/ssm
Size: 16
MountOptions:
- nosuid
In this verification, since we use ECS Exec for confirmation purposes, we mounted tmpfs at /var/lib/amazon/ssm and /var/log/amazon/ssm as write destinations for the SSM core agent. Note that the official documentation states that combining ECS Exec with readonlyRootFilesystem is not supported. If you are not using ECS Exec, the tmpfs mounts for these two paths are unnecessary. For more details on Fargate's tmpfs support, please refer to the following article.
ARM64
To use images built on Apple Silicon as-is, we specified ARM64 in RuntimePlatform.
RuntimePlatform:
CpuArchitecture: ARM64
OperatingSystemFamily: LINUX
The three IAM roles (Execution / Task / Infrastructure) are defined within the stack. For details, please refer to the full template.
Linking the Custom Task Definition
Specify !Ref CustomTaskDefinition in the TaskDefinitionArn of AWS::ECS::ExpressGatewayService.
ExpressModeService:
Type: AWS::ECS::ExpressGatewayService
Properties:
ServiceName: !Sub '${ServiceName}-service'
Cluster: 'default'
InfrastructureRoleArn: !GetAtt InfrastructureRole.Arn
TaskDefinitionArn: !Ref CustomTaskDefinition
HealthCheckPath: '/health'
Full Template (click to expand)
AWSTemplateFormatVersion: '2010-09-09'
Description: 'ECS Express Mode + readonlyRootFilesystem + tmpfs - 完全自己完結型 ARM64 版'
Parameters:
ServiceName:
Type: String
Default: 'express-nginx-readonly'
Resources:
# ===== IAM Roles =====
# ExecutionRole: ECSがイメージpull・ログ送信に使用
ExecutionRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-execution-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: ecs-tasks.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
# TaskRole: コンテナ内アプリが使用(ECS Exec用)
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: '*'
# InfrastructureRole: ECS Express Gatewayサービス管理用
InfrastructureRole:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub '${ServiceName}-infra-role'
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Sid: AllowAccessInfrastructureForECSExpressServices
Effect: Allow
Principal:
Service: ecs.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AmazonECSInfrastructureRoleforExpressGatewayServices
# ===== ECS Resources =====
ECSLogGroup:
Type: AWS::Logs::LogGroup
Properties:
LogGroupName: !Sub '/aws/ecs/default/${ServiceName}-service'
RetentionInDays: 7
CustomTaskDefinition:
Type: AWS::ECS::TaskDefinition
Properties:
Family: !Sub '${ServiceName}'
RequiresCompatibilities:
- FARGATE
NetworkMode: awsvpc
Cpu: '256'
Memory: '512'
ExecutionRoleArn: !GetAtt ExecutionRole.Arn
TaskRoleArn: !GetAtt TaskRole.Arn
RuntimePlatform:
CpuArchitecture: ARM64
OperatingSystemFamily: LINUX
ContainerDefinitions:
- Name: Main
Image: !Sub '${AWS::AccountId}.dkr.ecr.${AWS::Region}.amazonaws.com/express-mode-nginx-tmpfs:latest'
Essential: true
ReadonlyRootFilesystem: true
PortMappings:
- ContainerPort: 8080
Protocol: tcp
Name: http
LogConfiguration:
LogDriver: awslogs
Options:
awslogs-group: !Sub '/aws/ecs/default/${ServiceName}-service'
awslogs-region: !Ref 'AWS::Region'
awslogs-stream-prefix: ecs
HealthCheck:
Command:
- CMD-SHELL
- 'wget -qO- http://localhost:8080/health || exit 1'
Interval: 15
Timeout: 5
Retries: 3
StartPeriod: 10
LinuxParameters:
Tmpfs:
- ContainerPath: /tmp
Size: 128
MountOptions:
- noexec
- nosuid
- ContainerPath: /var/lib/amazon/ssm
Size: 16
MountOptions:
- nosuid
- ContainerPath: /var/log/amazon/ssm
Size: 16
MountOptions:
- nosuid
ExpressModeService:
Type: AWS::ECS::ExpressGatewayService
DependsOn: ECSLogGroup
Properties:
ServiceName: !Sub '${ServiceName}-service'
Cluster: 'default'
InfrastructureRoleArn: !GetAtt InfrastructureRole.Arn
TaskDefinitionArn: !Ref CustomTaskDefinition
HealthCheckPath: '/health'
NetworkConfiguration:
Subnets:
- subnet-xxxxxxxxxxxxxxxxx
- subnet-xxxxxxxxxxxxxxxxx
- subnet-xxxxxxxxxxxxxxxxx
ScalingTarget:
MinTaskCount: 1
MaxTaskCount: 1
AutoScalingMetric: 'AVERAGE_CPU'
AutoScalingTargetValue: 70
Outputs:
ServiceEndpoint:
Value: !GetAtt ExpressModeService.Endpoint
TaskDefinitionArn:
Value: !Ref CustomTaskDefinition
Deployment
aws cloudformation deploy \
--stack-name express-nginx-readonly-v2 \
--template-file express-nginx-readonly-standalone.yaml \
--parameter-overrides ServiceName=express-nginx-readonly-v2 \
--capabilities CAPABILITY_NAMED_IAM \
--region us-west-2
Deployment is complete once the stack status becomes CREATE_COMPLETE.
Preparation for Configuration Verification (CLI)
To verify the deployed configuration from inside the container, we enabled ECS Exec and shortened the bake time. Neither of these affects the already deployed configuration.
Enabling ECS Exec + Reducing Bake Time
We enable ECS Exec to verify configuration from inside the container using mount and uname. We also changed the bake time to reduce waiting time during verification. Since Express Mode defaults to a 3-minute canary bake time + 3-minute deployment bake time, we set both to 0. We recommend operating with appropriate bake times in production environments.
aws ecs update-service \
--cluster default \
--service express-nginx-readonly-v2-service \
--enable-execute-command \
--deployment-configuration '{
"strategy": "CANARY",
"bakeTimeInMinutes": 0,
"canaryConfiguration": {
"canaryPercent": 5.0,
"canaryBakeTimeInMinutes": 0
}
}' \
--region us-west-2
This update-service triggers a deployment and starts a new task. Since ECS Exec is not applied to existing tasks, run execute-command after the new task becomes RUNNING.
Verification (ECS Exec)
Health Check
$ curl -s "https://ex-xxxxxxxx.ecs.us-west-2.on.aws/health"
ok
Verifying Root Filesystem is Read-Only
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "touch /test.txt" --interactive
touch: /test.txt: Read-only file system
Verifying Write Access to tmpfs
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "sh -c 'echo test > /tmp/test.txt && cat /tmp/test.txt'" --interactive
test
tmpfs Size and Mount Options
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "df -h /tmp" --interactive
Filesystem Size Used Available Use% Mounted on
tmpfs 128.0M 280.0K 127.7M 0% /tmp
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "mount | grep tmpfs" --interactive
tmpfs on /tmp type tmpfs (rw,nosuid,noexec,relatime,size=131072k)
tmpfs on /var/lib/amazon/ssm type tmpfs (rw,nosuid,relatime,size=16384k)
tmpfs on /var/log/amazon/ssm type tmpfs (rw,nosuid,relatime,size=16384k)
The three tmpfs mounts are each configured with the specified sizes. The mount options are noexec,nosuid for /tmp, and nosuid for the two SSM paths.
Architecture Verification
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "uname -m" --interactive
aarch64
With uname -m returning aarch64, we confirmed that the environment is running on ARM64 as specified in RuntimePlatform.
Summary of Results
| Verification Item | Result |
|---|---|
| Health check (/health) | ok |
| Root write rejection | Read-only file system |
| tmpfs write (/tmp) | Successful |
| tmpfs size | 128MB / 16MB / 16MB |
| tmpfs mount options | /tmp: noexec,nosuid / SSM: nosuid |
| Architecture | aarch64 (ARM64) |
Responding to Security Hub Findings
ECS.5 (readonlyRootFilesystem)
The Security Hub ECS.5 control evaluates whether readonlyRootFilesystem is set to true in a task definition. In this configuration, we declare ReadonlyRootFilesystem: true in the custom task definition, satisfying the evaluation criteria for ECS.5 at the task definition level.
$ aws ecs describe-task-definition \
--task-definition express-nginx-readonly-v2 \
--query 'taskDefinition.containerDefinitions[?name==`Main`].readonlyRootFilesystem' \
--region us-west-2
[true]
When enabling readonlyRootFilesystem, we recommend evaluating in advance which paths your application requires write access to and what resources are needed.
ACM.1 (Certificate Expiration Management)
When a service is created, Express Mode automatically issues a domain in the format ex-XXX.ecs.<region>.on.aws along with an ACM certificate. Before DNS validation is complete (PENDING_VALIDATION), data such as the certificate's expiration date cannot be retrieved. As a result, Security Hub's ACM.1 may trigger a FAILED finding reported as Certificate data could not be retrieved.
In a verification environment where the cluster is repeatedly created and deleted, short-lived certificates are more likely to be evaluated in this state, causing findings to occur. These certificates are AWS-managed certificates tagged with AmazonECSManaged: true and cannot be renewed or controlled by the user. After the resource is deleted, they transition to NOT_AVAILABLE and fall out of Security Hub's scope. However, if you have a finding-and-notification mechanism in place, consider suppressing ACM.1.
Summary
With ECS Express Mode's support for custom task definitions, it is now possible to reference an AWS::ECS::TaskDefinition defined in CloudFormation from AWS::ECS::ExpressGatewayService.
This allows task definition settings such as readonlyRootFilesystem, tmpfs, and ARM64 to be declared as IaC and applied from the time the service is created. There is no longer a need to swap out the task definition after service creation, as was previously necessary. Steps to make changes outside of IaC management are also no longer required.
Note that for items without a corresponding property in CloudFormation resources, such as enabling ECS Exec, CLI-based configuration is still required at this time, but we look forward to future support.
