/var/www/yatta47.log

/var/www/yatta47.log

やったのログ置場です。スクラップみたいな短編が多いかと。

CannotPullContainerError: ECRイメージpull失敗はS3へのegress許可が必要

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つをセットで用意しておくと踏まずに済みます。

  1. Interface Endpoint: com.amazonaws.<region>.ecr.api
  2. Interface Endpoint: com.amazonaws.<region>.ecr.dkr
  3. Interface Endpoint: com.amazonaws.<region>.logs(awslogsログドライバを使う場合)
  4. 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を疑う、という引き出しがあると切り分けが速くなります。

参考