CloudFormation で ECS Express Mode のカスタムタスク定義に readonlyRootFilesystem + tmpfs を設定してみた
はじめに
2026年7月、ECS Express Mode がカスタムタスク定義に対応しました。
上記の記事では CLI で Express Mode サービスを作成しました。今回、CloudFormation もカスタムタスク定義をサポートしたことを確認できたので、readonlyRootFilesystem や tmpfs を宣言的に設定する手順を紹介します。
検証内容
検証環境
| 項目 | 値 |
|---|---|
| リージョン | us-west-2 (Oregon) |
| スタック名 | express-nginx-readonly-v2 |
| リソースタイプ | AWS::ECS::ExpressGatewayService |
| タスク定義 | AWS::ECS::TaskDefinition(カスタム) |
| CPU / Memory | 0.25 vCPU / 512 MiB |
| アーキテクチャ | ARM64 |
| コンテナ | nginx:1.27.5-alpine ベースのカスタムイメージ(ECR、8080 で listen・/health 応答) |
CloudFormation テンプレート
readonlyRootFilesystem + tmpfs
コンテナ定義で ReadonlyRootFilesystem: true を設定し、書き込みが必要なパスに tmpfs をマウントします。
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
本検証では ECS Exec を確認用途に使うため、SSM core agent の書き込み先として /var/lib/amazon/ssm と /var/log/amazon/ssm に tmpfs をマウントしました。なお、公式ドキュメントでは ECS Exec と readonlyRootFilesystem の組み合わせはサポート対象外とされています。ECS Exec を使わない場合、これら 2 パスの tmpfs は不要です。Fargate の tmpfs サポートの詳細については以下の記事を参照してください。
ARM64
Apple シリコンでビルドしたイメージをそのまま利用するため、RuntimePlatform で ARM64 を指定しました。
RuntimePlatform:
CpuArchitecture: ARM64
OperatingSystemFamily: LINUX
IAM ロール 3 種(Execution / Task / Infrastructure)はスタック内に定義しています。詳細はテンプレート全文を参照してください。
カスタムタスク定義の紐付け
AWS::ECS::ExpressGatewayService の TaskDefinitionArn に !Ref CustomTaskDefinition を指定します。
ExpressModeService:
Type: AWS::ECS::ExpressGatewayService
Properties:
ServiceName: !Sub '${ServiceName}-service'
Cluster: 'default'
InfrastructureRoleArn: !GetAtt InfrastructureRole.Arn
TaskDefinitionArn: !Ref CustomTaskDefinition
HealthCheckPath: '/health'
テンプレート全文(クリックで展開)
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
デプロイ
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
スタックのステータスが CREATE_COMPLETE になればデプロイ完了です。
設定確認のための準備(CLI)
デプロイされた構成を内部から確認するため、ECS Exec の有効化とベイクタイムの短縮を行いました。いずれもデプロイ済みの設定には影響しません。
ECS Exec の有効化 + ベイクタイムの短縮
コンテナ内部から mount や uname で設定を確認するために ECS Exec を有効化します。あわせて、検証の待機時間を短縮するためベイクタイムも変更しました。Express Mode のデフォルトでは Canary ベイクタイム 3 分 + デプロイベイクタイム 3 分が設定されているため、いずれも 0 に設定しています。本番環境では適切なベイクタイムで運用することをおすすめします。
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
この update-service によりデプロイがトリガーされ、新しいタスクが起動します。ECS Exec は既存タスクには反映されないため、新しいタスクが RUNNING になってから execute-command を実行します。
動作確認(ECS Exec)
ヘルスチェック
$ curl -s "https://ex-xxxxxxxx.ecs.us-west-2.on.aws/health"
ok
ルートファイルシステムの読み取り専用確認
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "touch /test.txt" --interactive
touch: /test.txt: Read-only file system
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 のサイズとマウントオプション
$ 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)
3 箇所の tmpfs がそれぞれ指定したサイズで構成されています。マウントオプションは /tmp が noexec,nosuid、SSM 用の 2 箇所は nosuid です。
アーキテクチャ確認
$ aws ecs execute-command --cluster default \
--task <task-id> --container Main \
--command "uname -m" --interactive
aarch64
uname -m が aarch64 であり、RuntimePlatform で指定したとおり ARM64 環境で動作していることを確認できました。
結果まとめ
| 確認項目 | 結果 |
|---|---|
| ヘルスチェック(/health) | ok |
| ルート書き込み拒否 | Read-only file system |
| tmpfs 書き込み(/tmp) | 成功 |
| tmpfs サイズ | 128MB / 16MB / 16MB |
| tmpfs マウントオプション | /tmp: noexec,nosuid / SSM 用: nosuid |
| アーキテクチャ | aarch64(ARM64) |
SecurityHub 検知への対応
ECS.5(readonlyRootFilesystem)
Security Hub の ECS.5 コントロールは、タスク定義の readonlyRootFilesystem が true に設定されているかを評価します。本構成ではカスタムタスク定義に ReadonlyRootFilesystem: true を宣言しており、タスク定義上は ECS.5 の評価条件を満たしています。
$ aws ecs describe-task-definition \
--task-definition express-nginx-readonly-v2 \
--query 'taskDefinition.containerDefinitions[?name==`Main`].readonlyRootFilesystem' \
--region us-west-2
[true]
readonlyRootFilesystem を有効化する際は、アプリケーションが書き込みを必要とするパスや必要なリソースを事前に評価することをおすすめします。
ACM.1(証明書の有効期限管理)
Express Mode はサービス作成時に ex-XXX.ecs.<region>.on.aws 形式のドメインと ACM 証明書を自動発行します。この証明書は DNS 検証完了前(PENDING_VALIDATION)の段階では有効期限などのデータが取得できません。その結果、Security Hub の ACM.1 が Certificate data could not be retrieved として FAILED を検知することがあります。
検証環境でクラスタの作成・削除を繰り返すと、短命な証明書がこの状態で評価に当たりやすく、検知が発生します。この証明書は AmazonECSManaged: true タグ付きの AWS 管理証明書で、ユーザー側で更新・制御できないため、リソース削除後は NOT_AVAILABLE へ遷移し、Security Hub の対象から外れますが、検知→通知の仕組みがある場合、ACM.1 の抑制もご検討ください。
まとめ
ECS Express Mode のカスタムタスク定義対応により、CloudFormation で定義した AWS::ECS::TaskDefinition を AWS::ECS::ExpressGatewayService から参照できるようになりました。
これにより、readonlyRootFilesystem、tmpfs、ARM64 などのタスク定義に関する設定を IaC として宣言し、サービス作成時点から反映できます。従来のように、サービス作成後にタスク定義を差し替える必要はありません。IaC 管理外で変更する手順も不要です。
なお、ECS Exec の有効化など CloudFormation リソースにプロパティがない項目については、現時点では CLI による設定が必要ですが、今後のサポートを期待したいと思います。







