ECSコンテナが急にEFSへ接続できなくなった件
TL;DR
EFSは各AZアベイラビリティゾーン。リージョン内の独立区画。 に「マウントターゲット(ENIElastic Network Interface。仮想NIC。 )」を持つ。- 例: ap-northeast-1a/1c にそれぞれ固定プライベート IP を持つ
ENIが存在。
- 例: ap-northeast-1a/1c にそれぞれ固定プライベート IP を持つ
ECSタスクは、配置されたAZに近い EFS マウントターゲットへNFSEFS が使うファイル共有プロトコル。標準ポート 2049/TCP。 で接続する。- つまり、1a に居れば 1a の ENI、1c に居れば 1c の ENI へ向かうのが基本動作。
- 今回の原因は「コンテナ側
SGセキュリティグループ。VPC の仮想ファイアウォール(stateful)。 のアウトバウンドが片側の IP/CIDR のみ許可」だったこと。別AZの ENI への2049/TCPがブロックされ、マウント失敗。- デプロイ時に「許可されていない側の AZ/IP」への経路になり、
NFS接続不可 → タスク起動失敗。
- デプロイ時に「許可されていない側の AZ/IP」への経路になり、
- 対策は「
SGのアウトバウンドに“抜けていた側のサブネットCIDR”を追加」し、両AZのマウントターゲット到達を許可。
概要
参画先で、ECS にデプロイしたコンテナが「EFS に接続できない」エラーで起動に失敗しました。
この記事では、
- なぜ発生したのか
- 対策内容
- 自分の AWS アカウントでの再現と検証
をまとめ、個人環境で事象が実現可能な状態にします。
なぜ起きたのか?
EFS の構造
EFSはリージョン冗長だが、VPC内では各AZに 1 つの「マウントターゲット(ENI)」を作成し、固定プライベート IP が割り当てられる。- クライアント(
ECSタスク等)は通常、自分が動作するAZのマウントターゲットに接続(最短経路・低レイテンシ)。
DNS の解決と接続先の選択
fs-XXXX.efs.ap-northeast-1.amazonaws.comのようなEFSのDNS名前解決により接続先 IP を得る仕組み。 を引くと、同一AZの ENI の IP に解決される(同一AZ優先)。- したがって、タスクの配置
AZによって接続先 IP が変わるのは正常挙動。
SG アウトバウンドの制約が“AZ またぎ”を阻害
- 送信側
SGのアウトバウンドを片側のCIDR(または IP)だけに絞ったため、別AZのマウントターゲット IP への2049/TCPが拒否。 ECSは指定サブネット群のいずれかにタスクを起動するため、「許可していない側」に出たタスクのマウント処理が失敗した。
正しい設計/ベストプラクティス
1. SG は stateful を活かす
EFS側SGはインバウンド2049/TCPの送信元に「ECSタスクのSG」を指定する。ECS側SGはアウトバウンドを広めに(例:0.0.0.0/0の2049/TCP)して、AZ/IP 変動に強くする。- 片側 IP のピンポイント許可は避ける。
2. CIDR で絞るなら“両 AZ 分を必ず許可”
- 両方のサブネット
CIDRをアウトバウンドに含め、どちらAZでも到達可能にする。
3. EFS の SG は“参照ベース”で許可
EFS側のインバウンドに「ソースSG(=ECSタスクのSG)」を指定し、IP/CIDR管理を不要化。- 送信側では
SG参照が使えないため、ECS側SGは適切に広げる。
4. NACL を過度に絞りすぎない
NACLNetwork ACL。サブネット境界のステートレスフィルタ。 を厳しくしすぎると、SGが正しくても通信は失敗する。VPCレイヤの整合性を取る。
5. タスク配置方針とネットワーク設計の整合
- 特定
AZ固定は高可用性が下がる。HA を優先するならAZ横断で許可が成立する設計にする。
同様の事象が発生しやすい AWS サービス
「複数 AZ にエンドポイント(ENI/IP)があり、クライアントが同一 AZ を優先して接続する」タイプは同様のハマり方をしがち。片側 AZ や一部 IP のみ許可は危険。
Amazon EFS(今回)
- 各
AZのマウントターゲットENIにNFS(2049/TCP)で接続(AZごとに到達先 IP が異なる)。
Amazon RDS(特に Multi-AZ / フェイルオーバー)
- クライアントは
RDSのエンドポイント(DNS)へ接続するが、裏側 IP はフェイルオーバーや再配置で変わりうる。 - 特定 IP 固定は切替に弱いため、
CIDR/SG参照ベースが望ましい。
Network Load Balancer(NLB)/ Gateway Load Balancer(GWLB)
- 内部
NLBは各AZにENI(IP)を持つ。AZにより指す IP が変わるため、特定 IP のみ許可だと接続不可に。
Interface 型 VPC Endpoint(PrivateLink)
- 各
AZにエンドポイントENIができ、その IP へ接続。片側AZのみ許可だと別AZのタスクは到達不可になり得る。
Amazon OpenSearch Service / ElastiCache / Redshift など
- クラスター/ノードごとに
ENI/IP が存在し、AZ配置で接続先が変わる/増える。IP 固定許可は運用負荷・切替リスクを高めるため、SG参照やCIDRベースが安全。
Application Load Balancer(ALB)
ALBは各AZにロードバランサーノードを持ち、IP が変動。ターゲット側は「ソースをALBのSG参照」で許可するのがベスト。IP 固定許可は非推奨。
実践的な再発防止策(チェックリスト)
1. EFS
EFS側SG: Inbound2049/TCPSource = 「ECSタスクのSG」ECS側SG: Outbound2049/TCPDestination =0.0.0.0/0(または両AZのサブネットCIDR)NACL: 双方向で2049/TCPを許可(過度に絞らない)
2. 共通原則
- アウトバウンド宛先を「特定 IP 固定」にしない(
AZ/フェイルオーバーに弱い) SGの「SG参照(ソースSG指定)」を活用し、IP 管理をなくすDNS名の背後 IP は変わりうる前提で設計(RDS/ALB/NLB/VPC Endpoint等)
3. 運用設計
- 新規
AZ追加やサブネット増設に合わせてSGルールの棚卸しを定期実施 - 監視(CloudWatch Logs / VPC Flow Logs)で拒否イベントを即検知
IaC(Terraform/CloudFormation)でSG設定を標準化し、「片側のみ許可」の事故を防止
今回の止血対応は**「ECS タスクの SG に対し両サブネットを許可」**。
今後はエンドユーザーと設計方針をすり合わせる前提です。
自アカウントでの検証
同様の事象を自分の AWS アカウントで再現・改善しました。
検証リポジトリ:
GitHub - wan0ri/ecs-validate: ECS検証
ECS検証. Contribute to wan0ri/ecs-validate development by creating an account on GitHub.
github.com/wan0ri/ecs-validate
検証の詳細(issue 深掘り)
以下の issue に再現ログと時系列をまとめています:
ecs-validate/issues/2026-01-11-ecs-efs-repro.md at main · wan0ri/ecs-validate
ECS検証. Contribute to wan0ri/ecs-validate development by creating an account on GitHub.
github.com/wan0ri/ecs-validate/blob/main/issues/2026-01-11-ecs-efs-repro.md
ここでは、要点を文章で深掘りします。
- 仮説の立て方: 「EFS の接続先はタスク配置
AZに依存」→「送信側SGのアウトバウンドがAZ片側だけだと、もう一方のマウントターゲットに到達できず失敗する」。 - 失敗の作り込み: サービスを
1cに配置、送信側SGは1aのサブネットCIDRのみ許可。起動時にstoppedReasonにfailed to invoke EFS utils ... timeout ... code: 32が出力。これはefs-utilsEFS のマウントを支援するユーティリティ。DNS 解決や TLS 設定を内部で行う。 が2049/TCPで応答を得られずタイムアウトした典型パターン。 - 切り分け:
EFS側SGは「インバウンド2049/TCPのソースにタスクSG」を設定済みであるため、今回は送信側(タスク)のアウトバウンド制約がボトルネック。authorization_config.iam = "ENABLED"かつAccess Pointを使っており、IAM 権限不足の典型(Permission denied)ではない点も判断材料。 - DNS の挙動確認: タスクのある
AZからfs-xxxx.efs.ap-northeast-1.amazonaws.comを引くと、同一AZのマウントターゲット IP に解決される。よって、タスクの配置が変わると接続先 IP も変わる。 - 修正の最小差分: 送信側
SGのアウトバウンドに「もう片方のAZのサブネットCIDR」を追加。もしくは2049/TCPだけ0.0.0.0/0へ広げる。後者は 設計が単純になり運用耐性が上がる 一方、ポリシーによっては前者(両AZのCIDR明記)を選ぶケースもあり。 - 結果: どちらの
AZにタスクが出てもNFSマウントが成功し、RUNNINGへ遷移。CloudWatch Logs に/dataのls結果が出力されることを確認。 - 補助的な観測ポイント: VPC Flow Logs を有効化している場合、失敗ケースでは
2049/TCPのREJECTを確認可能。成功後はACCEPTに切り替わる。
補助コマンド(環境に合わせて置換)
# EFS マウントターゲットの IP と AZ を把握
aws efs describe-mount-targets \
--file-system-id fs-XXXXXXXX \
--query 'MountTargets[].{AZ:AvailabilityZoneName,Subnet:SubnetId,IP:IpAddress}' --output table
# タスク配置の確認
aws ecs describe-tasks --cluster ecs-validate --tasks <TASK_ARN> \
--query 'tasks[0].[lastStatus,attachments[0].details[?name==`subnetId`].value|[0]]' --output table
# SG アウトバウンドの見直し例(方針A: 0.0.0.0/0:2049 を許可)
# 方針B は両AZのサブネットCIDRを個別に許可
検証構成のポイント(Terraform 抜粋)
module "security" {
source = "./modules/security"
vpc_id = module.network.vpc_id
lockdown_mode = true
allow_both_azs = false
allow_https_egress = true
use_efs_mt_ips = true
efs_mt_ips = module.efs[0].mount_target_ips
}
resource "aws_ecs_task_definition" "this" {
family = var.name
requires_compatibilities = ["FARGATE"]
network_mode = "awsvpc"
cpu = "256"; memory = "512"
execution_role_arn = var.execution_role_arn
task_role_arn = var.task_role_arn
container_definitions = jsonencode([
{
name = "app"
image = "public.ecr.aws/docker/library/busybox:latest"
command = ["sh","-c","echo start && ls -al /data && sleep 3600"]
logConfiguration = {
logDriver = "awslogs"
options = {
awslogs-group = var.log_group_name
awslogs-region = var.region
awslogs-stream-prefix = "ecs"
}
}
mountPoints = [{ containerPath = "/data", sourceVolume = "efs_data" }]
}
])
volume {
name = "efs_data"
efs_volume_configuration {
file_system_id = var.efs_file_system_id
transit_encryption = "ENABLED"
authorization_config {
access_point_id = var.efs_access_point_id
iam = "ENABLED"
}
}
}
}
data "aws_iam_policy_document" "task_efs" {
statement {
actions = ["elasticfilesystem:ClientMount","elasticfilesystem:ClientWrite","elasticfilesystem:ClientRootAccess"]
resources = ["*"]
condition {
test = "StringEquals"
variable = "elasticfilesystem:AccessPointArn"
values = [var.efs_access_point_arn]
}
}
}
resource "aws_iam_role_policy" "task_efs" {
role = aws_iam_role.task.id
policy = data.aws_iam_policy_document.task_efs.json
}
ログ抜粋(失敗→成功)
# 失敗時
stoppedReason: failed to invoke EFS utils ... timeout ... code: 32
# 成功確認
aws ecs describe-tasks --cluster ecs-validate --tasks <TASK_ARN> \
--query 'tasks[0].[lastStatus,stoppedReason]' --output table
# => RUNNING / None
まとめ
原因は、EFS が AZ ごとに異なる接続先 IP(マウントターゲット)を持つ一方、ECS 側 SG のアウトバウンド許可が片側のみで NFS(2049/TCP) が到達不可になったこと。
「IP 固定の許可」ではなく「SG 参照/CIDR ベース/広めのアウトバウンド」を採用すると、AZ 差異・フェイルオーバー・拡張時の変化に強くなる。
同様の事象は RDS(フェイルオーバー時の接続先変動)、NLB(AZ ごとに ENI)、Interface 型 VPC Endpoint などでも起こり得る。