こんにちは。ソーシャル経済メディア「NewsPicks」で検索システムを開発しております崔(ちぇ)です。
弊社の検索システムはAWS EC2(Elastic Compute Cloud、以下、EC2)で動いていました。それを昨年、Amazon ECS(Elastic Container Service、以下、ECS)に移行しました。前回のブログでは、移行のために調べた「アプリケーションをコンテナ化するベストプラクティス」をまとめましたので、ご興味ある方は読んでいただけると嬉しいです。
今日は、ECS on Fargateのタスク起動に手こずった話をしてみようと思います。タイトル通りFargate 1.4.0 で発生しうる ResourceInitializationError
の解決方法について述べるのですが、「まさに今それにハマってた!」という方はぜひ読んでみてください。
検索システムはECS on Fargateを採用していますがNewsPicksではケースによりECS on EC2も採用しております。詳しく比較した記事も公開しておりますので、ご興味ある方はこちらの記事もご確認ください。
ECS on Fargate でシステムを構築する
AWS Fargate(以下、Fargate)はECSで利用可能なコンテナの実行環境で、プロビジョニングやスケール、OSの管理などといったインスタンスの運用が不要です。
現在Amazon Linux 2とMicrosoft Windows 2019 Server Fullをサポートしています。詳しくは以下のドキュメントをご参照ください。
ResourceInitializationError に遭遇してしまった
ResourceInitializationError
はFargate1.4.0からしか発生しないエラーで、エラー名からわかるように、「タスクを起動するために必要なリソースが用意できないので起動ができないよ!」というものです。その他のエラーに関しては、こちらからご確認できます。
ResourceInitializationError
This error occurs when the container agent for your Fargate task fails to create or bootstrap the resources required to start the container or the task it belongs to.
A common cause for this error is using a VPC that doesn't have DNS resolution enabled.
This error only occurs if you use platform version 1.4.0 or later (Linux) or 1.0.0 or later (Windows).
形式は以下の通りです。
Example: ResourceInitializationError: failed to initialize logging driver: <reason>
このエラーがFargate 1.4.0でしか発生しないのは、1.3.0 ~ 1.4.0のチェンジログを辿ればわかります。AWSのブログやAWSのドキュメントによると、いろんな変化がありましたが、その中でも「Task elastic network interface (ENI) now runs additional traffic flows」が影響しています。
FYI
Amazon ENIは、VPC上にある論理ネットワークの一つの要素で、仮想的なNIC(Network Interface Card、あるネットワークに接続するためのカード型の拡張装置)といえます。論理ネットワークというのは、物理的に接続せずIPやMACアドレスなどで通信するものを指します。
軽くまとめると、今までFargate ENIを使ってAWSのVPCエンドポイントにアクセスしていたものを、すべてタスクENIを使うようにしたという話が書かれています。
- Fargate ENI:AWSが管理するVPCを経由し、AWSの特定のサービスのVPCエンドポイントにアクセスする
- タスクENI:ユーザが管理するVPCを経由し、AWSの特定のサービスのVPCエンドポイントにアクセスする
理由としては、ユーザが管理するVPCを経由させ、トラフィックの流れをログから監視できるようにしたかった(そうして欲しいとの要望が多かった)らしいです。
上記に添付したAWSのブログに乗っていますが、変更の対象になったサービスと処理は以下です。
- Amazon ECR(以下、ECR)へのログイン
- AWS Secrets Manager(以下、Secrets Manager)からシークレットを取得
- AWS Systems Manager(以下、SSM)からシークレットを取得
ECSのタスクは、内部で動くアプリケーションの通信以外にも、以下のような処理のためにAWSの各サービスと通信する必要があります。
- Amazon CloudWatch Logs(以下、CloudWatch Logs)にログを出力する
- ECRからイメージをpullする
- Secrets Managerからシークレットを取得する
- SSMからシークレットを取得する
つまり、Fargate 1.4.0から我々の管理するVPCにENIを作成するので、Fargateがこれらの処理を問題なく完了できるように、AWSサービスごとにVPCエンドポイントを用意する必要があります。
Starting with platform version 1.4.0, all Amazon ECS on Fargate tasks receive a single elastic network interface (referred to as the task ENI) and all network traffic flows through that ENI within your VPC and will be visible to you through your VPC flow logs.
それができていない場合に、以下のような ResourceInitializationError
が発生します。
ResourceInitializationError: unable to pull secrets or registry auth: execution resource retrieval failed: unable to retrieve secret from asm: service call has been retried 5 time(s): failed to fetch secret arn:aws:secretsmanager:{region}:{accountId}:secret:{secretName} from secrets manager: RequestCanceled: request context canceled caused by: context deadline exceeded. Please check your task network configuration.
FYI
エラーが発生したらタスクは停止されますが、ECSサービスのDeployments > Eventsからタスクのページを開き、エラー内容を確認することができます。ただ、アクセスできるのは停止されてから最大1時間までなので、注意が必要です。
Fargate 1.4.0 を使う上で満たすべき条件
どういうVPCエンドポイントが必要なのかは、AWSのドキュメントに記載されています。多くの場合は、以下が必要でしょう。
AWSサービス | やりたいこと | VPCエンドポイントタイプ | エンドポイント |
---|---|---|---|
ECR | イメージをpullする | Interface | com.amazonaws.region.ecr.dkr |
com.amazonaws.region.ecr.api | |||
Gateway | com.amazonaws.region.s3 | ||
Secrets Manager | シークレットを取得する | Interface | com.amazonaws.region.secretsmanager |
SSM | シークレットを取得する | Interface | com.amazonaws.region.ssm |
CloudWatch Logs | ログを出力する | Interface | com.amazonaws.region.logs |
これらに加えて、全てのセキュリティグループはタスクに紐づいているものにし、各サービスのVPCエンドポイントとタスク間のアクセスを許可する必要があります。
VPCエンドポイントとは別に、データを取得する必要がある各サービスへのアクセス権限をタスク実行ロールにつける必要があります。
例えば以下のようなシステムがあるとしましょう。
- コンテナイメージをECRで管理する
- 認証情報をSecretsManagerで管理する
- タスク起動時にSecretsManagerのシークレットを環境変数としてうまく埋め込んでおく(アプリケーションの内部にSecretsManagerにアクセスするための処理を書きたくない)
- 全てのログはCloudWatch Logsに出力する
おそらく以下のような権限が必要になります。
アクション | リソース |
---|---|
ecr:GetAuthorizationToken | * |
ecr:BatchCheckLayerAvailability | ECRイメージのARN |
ecr:GetDownloadUrlForLayer | ECRイメージのARN |
ecr:BatchGetImage | ECRイメージのARN |
logs:CreateLogStream | ログを出力するロググループのARN |
logs:PutLogEvents | ログを出力するロググループのARN |
secretsmanager:DescribeSecret | シークレットのARN |
secretsmanager:GetSecretValue | シークレットのARN |
付録
CDKでインフラを構築する
VPCエンドポイントはCDKで以下のように生成できます。
import * as cdk from "aws-cdk-lib"; import * as ec2 from "aws-cdk-lib/aws-ec2"; import { Construct } from "constructs"; interface VpcEndpointTarget { readonly service: ec2.InterfaceVpcEndpointAwsService; readonly idSuffix: string; } export class AppDeployment extends Construct { constructor(scope: Construct, id: string, props: {}) { super(scope, id); const vpc = ec2.Vpc.fromLookup(this, "AppDeploymentVpc", { vpcName: "{vpcName}" }); const subnet = ec2.Subnet.fromSubnetAttributes(this, "AppDeploymentSubnet", { subnetId: "{subnetId}", availabilityZone: "{availabilityZoneId}", routeTableId: "{routeTableId}", }); const securityGroup = ec2.SecurityGroup.fromSecurityGroupId( this, "AppDeploymentSecurityGroup", "{securityGroupId}", ); const necessaryVpcEndpointTargets: VpcEndpointTarget[] = [ { service: ec2.InterfaceVpcEndpointAwsService.ECR, idSuffix: "EcrApi" }, { service: ec2.InterfaceVpcEndpointAwsService.ECR_DOCKER, idSuffix: "EcrDkr" }, { service: ec2.InterfaceVpcEndpointAwsService.CLOUDWATCH_LOGS, idSuffix: "CloudWatchLogs" }, { service: ec2.InterfaceVpcEndpointAwsService.SECRETS_MANAGER, idSuffix: "SecretsManager" }, ]; necessaryVpcEndpointTargets.forEach((target) => { new ec2.InterfaceVpcEndpoint(this, `VpcEncpoint${target.idSuffix}`, { service: target.service as ec2.InterfaceVpcEndpointAwsService, vpc, subnets: { subnets: [subnet] }, securityGroups: [securityGroup], privateDnsEnabled: false, }); }); new ec2.GatewayVpcEndpoint(this, "VpcEncpointS3", { service: ec2.GatewayVpcEndpointAwsService.S3, vpc: props.loadedEcsNetwork.iVpc, subnets: [{ subnets: [subnet] }], });
FYI
同じCDK Stack内でVPCを作るのではなく、既存のものをロードする場合は、
fromAttributes
ではなくfromLookup
を使った方がいいです。インターフェース型VPCエンドポイントを作る際に発生するCannot perform this operation: 'vpcCidrBlock' was not supplied when creating this VPC
エラーが回避できます。
VPCエンドポイントの作成時に注意すべきこと
インターフェース型VPCエンドポイントは固定料金がかかります
インターフェース型VPCエンドポイントは固定料金がかかるので、最低限の数だけ生成できるように、同じVPC内に複数のサブネットでシェアできる設計を考えた方がいいです。
最初作ったVPCエンドポイントにサブネットを追加すればいいのでは?と思うかもしれませんが、現時点ではCDK L1とL2ともにVPCエンドポイントの新規作成時にしかサブネットが設定できないため、後から追加することが難しいです。
一方で、ゲートウェイ型VPCエンドポイントは無料ですが、VPCあたり一つしか作れないので、それも要注意です。ちなみに、今のところゲートウェイ型VPCエンドポイントが必要なのは、Amazon S3とAmazon DynamoDBだけです。
インターフェース型VPCエンドポイントは privateDnsEnabled
の設定に気をつけましょう
すでに同じサービスのVPCエンドポイントが作られている場合は、privateDnsEnabled
を false
にする必要があります。同じ名前のプライベートホストゾーンが作れないからです。詳しくはAWS Knowledge Centerの記事をご参照ください。
If you turn on PrivateDNS when creating interface endpoints, then a private hosted zone is automatically created and associated with your VPC. AWS services and AWS Marketplace partner services have PrivateDNS turned on by default. So, creating a second interface VPC endpoint for the same service with PrivateDNS turned on causes the conflicting DNS domain error. To fix this, turn off the PrivateDNS option when creating the interface endpoint. Use endpoint-specific DNS hostnames for the second VPC interface endpoint for that service.
終わりに
ECS化のプロジェクトは時間がかかった分、はまりポイントも多く、学びも多かったのでまたこうやってブログを書かせていただきました。
「アプリケーションさえ正常に起動できれば、タスクなんて当たり前に起動するっしょ!」と安易に思っていたので、真っ直ぐ ResourceInitializationError
にハマってしまいました。
最初はECSのタスクの停止理由をどこから確認すればいいのかも知らない状態で、色々ググったりAWSのドキュメントを読んだりしました。そうしていたら、「Fargateのデフォルトバージョンって1.4.0なんだ」から始まり「各サービスのVPCエンドポイントをこちらで用意する必要がある」までがやっとわかりました。きっと私と同じくハマっている方がいると思うので、その際にこの記事がお役に立てればと思います。
また、新たに学んだりハマったりチャレンジした記事を書きますので、ご期待ください!