CannotPullContainerError: ECRイメージpull失敗はS3へのegress許可が必要
VPC Endpoint構成(NATなし)かつSGのegressを絞っているECS Fargateで CannotPullContainerError が出たら、SGのegressにS3 prefix listが抜けている可能性があります。
ECRのイメージpullは途中からS3へ通信が切り替わるので、S3 Gateway Endpoint向けのegressを許可しないとレイヤーダウンロードで詰まります。
なお、Fargate Platform Version 1.4.0以降はすべての通信(ECR/S3/CloudWatch Logs)がタスクENIに集約されているため、このSG設定はタスク側のSGに対して行います。
何が起きたか
VPC Endpoint構成でECS Fargateタスクを起動したら、タスクがPENDINGのまま動かなくなったんですよね。サービスイベントにはこんなエラーが出ていました。
CannotPullContainerError: pull image manifest has been retried 7 time(s): failed to resolve ref
「ECRに繋がってないのかな?」と思ってInterface Endpoint(ecr.api, ecr.dkr)を確認したんですけど、問題ない。CloudWatch Logs用のエンドポイントも作ってある。全部揃ってるのに動かない。
で、結論から言うと犯人はSGのegressでした。S3 Gateway Endpoint向けのprefix listが抜けていた。
原因: ECR pullは2段階の通信になっている
これが厄介なところなんですけど、ECRからのイメージpullは途中から通信先がS3に変わります。
ECS Task (Fargate) | | 1. マニフェスト取得(イメージのメタデータ) | +-- 宛先: com.amazonaws.<region>.ecr.dkr | +-- 経由: Interface Endpoint(ENI, プライベートIP) | | 2. レイヤーダウンロード(実際のイメージデータ) | +-- 宛先: com.amazonaws.<region>.s3 | | (prod-<region>-starport-layer-bucket) | +-- 経由: Gateway Endpoint(ルートテーブル経由) | v タスク起動
ECRはコンテナイメージのレイヤーデータをS3に保存しています。docker pull のとき、ECR APIでマニフェスト(どのレイヤーが必要かの目録)を取得した後、実際のレイヤーはS3から直接ダウンロードする仕組みになっています。
この2つが全く別の通信経路を通るのがポイントです。Interface Endpointはプライベートサブネット内のENIを経由しますが、Gateway EndpointはルートテーブルでVPC内部にルーティングされます。
SGの観点では、宛先がVPC CIDRではなくS3のパブリックIPレンジになるので、prefix listで明示的に許可しないと通りません。
解決方法
ECSタスクに割り当てるSGに、S3 prefix list宛のegressルールを追加します。
まずリージョンのS3 prefix list IDを確認します。
aws ec2 describe-prefix-lists \ --filters "Name=prefix-list-name,Values=com.amazonaws.ap-northeast-1.s3" \ --query "PrefixLists[0].PrefixListId" \ --output text # 例: pl-61a54008
SGにegressルールを追加します。
aws ec2 authorize-security-group-egress \
--group-id sg-xxxxxxxxx \
--ip-permissions "IpProtocol=tcp,FromPort=443,ToPort=443,PrefixListIds=[{PrefixListId=pl-61a54008}]"
Terraformの場合はこうなります。
data "aws_prefix_list" "s3" { filter { name = "prefix-list-name" values = ["com.amazonaws.${var.region}.s3"] } } resource "aws_security_group_rule" "ecs_to_s3" { type = "egress" from_port = 443 to_port = 443 protocol = "tcp" prefix_list_ids = [data.aws_prefix_list.s3.id] security_group_id = aws_security_group.ecs_tasks.id }
なぜ気づきにくいか
3つ理由があります。
1つ目は、エラーメッセージが「マニフェスト取得失敗」に見えること。S3へのegressが閉じている場合はレイヤーダウンロード段階で詰まるのに、エラーメッセージからはどの段階で失敗したのか読み取りにくいんですよね。
2つ目は、S3 Gateway Endpointの存在自体は認識していても、SGのegressにprefix listの設定が必要だとは思わないこと。Gateway Endpointはルートテーブルに追加されるので「ルーティングの問題」だと思いがちなんですけど、SGでegressが絞られていると、そもそもパケットがルートテーブルに到達する前にドロップされます。
3つ目は、egressが 0.0.0.0/0 の全開放だと問題が起きないこと。セキュリティを強化しようとegressを絞ったタイミングで初めて踏むので、新規構築時のテンプレートでは気づかないケースがあります。
まとめ
VPC Endpoint構成でECSを動かすなら、以下の4つをセットで用意しておくと踏まずに済みます。
- Interface Endpoint:
com.amazonaws.<region>.ecr.api - Interface Endpoint:
com.amazonaws.<region>.ecr.dkr - Interface Endpoint:
com.amazonaws.<region>.logs(awslogsログドライバを使う場合) - Gateway Endpoint:
com.amazonaws.<region>.s3+ SGにprefix list egressルール
1・2・4がECRイメージpullの必須セット、3はawslogsログドライバ使用時に必要です。特に4番目の「Gateway Endpoint + SGルール」がセットであることを忘れがちです。
Gateway Endpointを作っただけでは不十分で、SGのegressも合わせて開ける必要があります。
CannotPullContainerError が出たらまずSGのS3 prefix list egressを疑う、という引き出しがあると切り分けが速くなります。
参考
- Amazon ECR interface VPC endpoints (AWS PrivateLink) - Amazon ECR
- Resolve the "cannotpullcontainererror" error for Amazon ECS tasks on Fargate | AWS re:Post
- Setting up AWS PrivateLink for Amazon ECS, and Amazon ECR | AWS Blog
- Lock down AWS Fargate networking when using ECR as an image repository | 7th Zero
- Demystifying an interesting relation between ECR and S3 | SkildOps