AWS CDKを使ってcurlでS3のファイルにアクセスできるFargateをつくる
最近は、ネットでにわかに流行しているOnly Up!という登山系ゲームがマイブームです。手持ち無沙汰なときについやりたくなります。たまにはそんなカジュアルなネタでも…と思いましたが、実案件でちょっとしたネタが出てきたので今回もAWSのこと書きます
TL;DR
・AWS CDKで、S3 バケットとVPCエンドポイントゲートウェイを作成する
・タスクロールのIAM設定だと、AWS CLIでの操作はできるがHTTPでのアクセスは不可
・バケットポリシーで許可して、HTTPSでアクセスできるようにする
背景とやったこと
最近コンテナ化したある案件で、サーバ上でコマンドを実行するタスクが発生しました。以前はSCPでサーバにファイルを転送し、SSHでシェルにアクセスしていたようです。SSHの代わりにECS Execでシェル操作は可能ですが、SCPのファイル転送は使えません。ECS ExecにSCPみたいな機能があれば嬉しいのですが無いものは仕方ありません。
ということで、S3を経由してファイル転送を試みます。今回のファイルは数KB程度だったので、ターミナルでコピペでも作業は可能でしたが、今後のためにもコンテナ内からS3バケットを使えるようにしました。
サンプル用に簡単化した環境を作ってたのが上の図です。プライベートサブネットにALBやECSがある構成です。実際の案件ではDBなど他リソースもありますが、今回は関係ないので省略します。
まずは愚直に構築
次のAWS CDKのコードで、上に示したコンテナ環境の基本部分(VPC, NATGateway, ALB, ECS on Fargate)を作ります。今回の目的は、コンテナ内のシェルからS3のファイルをダウンロードできるようにすることです。実際の案件ではECRのコンテナイメージを使っている部分は、サンプルとしてNode.jsの公式イメージで代用しました。Webサーバとしての役割はどうでもよいのですが、最低限の挨拶として200 OKを返すようにしています。挨拶は大事なので [1]
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ecsp from 'aws-cdk-lib/aws-ecs-patterns'; export class S3AccessFromFargateWithCurlStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, 'vpc', { maxAzs: 2, natGateways: 1, // コスト削減 }); new ecsp.ApplicationLoadBalancedFargateService(this, 'cluster', { vpc, taskImageOptions: { image: ecs.ContainerImage.fromRegistry('node:20.4-bookworm'), containerPort: 8080, command: [ 'node', '-e', 'require("http").createServer((_, res) => { res.end("Hello") }).listen(8080)' ], }, taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, enableExecuteCommand: true, }); const bucket = new s3.Bucket(this, 'bucket', { bucketName: 's3-access-from-fargate-with-curl', removalPolicy: cdk.RemovalPolicy.DESTROY, // テスト用なので気軽に削除 }); bucket.grantReadWrite(cluster.taskDefinition.taskRole); } }
ポイントは bucket.grantReadWrite(cluster.taskDefinition.taskRole);
の部分です。文字通り、ここでタスクロール(FargateのコンテナにつくIAMロール)に対してS3バケットの読み書き権限を付与しています
AWS CDKでデプロイしたら、ECS Execでシェルに入ってS3のファイルをダウンロードしてみます。この状態ではcurlでダウンロードできませんでした。XML形式のエラーが返ってきます。
root@ip-10-0-168-184:/# curl https://s3-access-from-fargate-with-curl.s3.ap-northeast-1.amazonaws.com/test.txt <?xml version="1.0" encoding="UTF-8"?> <Error><Code>AccessDenied</Code><Message>Access Denied</Message><RequestId>WJMPZZ9APP15B0ED</RequestId><HostId>GEIdZwwVy/7RSMLOAGn0cdODlhLILadK+2XjCeKDCaU9v+rhecUX1bWr/s8IaZYFfP2LTt0e78Q=</HostId></Error>
しかし、AWS CLIをインストール(公式ドキュメントのLinuxの手順そのままでOKです)して
root@ip-10-0-168-184:/# aws s3 cp s3://s3-access-from-fargate-with-curl/test.txt test.copy download: s3://s3-access-from-fargate-with-curl/test.txt to ./test.copy root@ip-10-0-168-184:/# root@ip-10-0-168-184:/# cat test.copy Hello World!
としてみると、ファイルがダウンロードできます。IAMは正常に設定できているのでAWS CLIだとファイルを取得できますが、curlのHTTPでは駄目という状況でした。調べたところ、どうやらS3バケットポリシーで許可しないといけないようです。
「SSHを入れるよりはマシ」と割り切ってAWS CLIを入れてもよいのですが、本番のコンテナに不要なパッケージを入れるのはやはり抵抗があります。そのため、curlだけでS3にアクセスできるようにしましょう。
ちなみに余談[2]ですが、上の例で使ったのはDebianベースのイメージですが、Aplineベースのイメージにはglibcが不足しているためAWS CLIの導入が意外と大変です。さらにコンテナの実行ユーザがrootでない場合、AWS認証情報を環境変数で取得できずDockerfileで工夫する必要があるそう(参考)です。そういった点からも、あまりコンテナ内でAWS CLIを使いたくはありません。
VPC-Eの追加とバケットポリシーの設定
そこでCDKのコードを次のように追記してみます。ポイントは、VPCエンドポイントゲートウェイの作成と、バケットポリシーの設定です。これによって、FargateからS3へのアクセスがNATゲートウェイを経由せず、VPCエンドポイントを経由するようになりました。この経路のアクセスに対して、バケットポリシーで許可を与えています。わざわざ消す必要はないのですが、なくても良いみたいなので bucket.grantReadWrite(...
を削除しています。
import * as cdk from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as s3 from 'aws-cdk-lib/aws-s3'; import * as iam from 'aws-cdk-lib/aws-iam'; import * as ec2 from 'aws-cdk-lib/aws-ec2'; import * as ecs from 'aws-cdk-lib/aws-ecs'; import * as ecsp from 'aws-cdk-lib/aws-ecs-patterns'; export class S3AccessFromFargateWithCurlStack extends cdk.Stack { constructor(scope: Construct, id: string, props?: cdk.StackProps) { super(scope, id, props); const vpc = new ec2.Vpc(this, 'vpc', { maxAzs: 2, natGateways: 1, // コスト削減 }); new ecsp.ApplicationLoadBalancedFargateService(this, 'cluster', { vpc, taskImageOptions: { image: ecs.ContainerImage.fromRegistry('node:20.4-bookworm'), containerPort: 8080, command: [ 'node', '-e', 'require("http").createServer((_, res) => { res.end("Hello") }).listen(8080)' ], }, taskSubnets: { subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS, }, enableExecuteCommand: true, }); const bucket = new s3.Bucket(this, 'bucket', { bucketName: 's3-access-from-fargate-with-curl', removalPolicy: cdk.RemovalPolicy.DESTROY, // テスト用なので気軽に削除 }); // VPCエンドポイントを作成 const s3Endpoint = vpc.addGatewayEndpoint(`vpce-gw-s3`, { service: ec2.GatewayVpcEndpointAwsService.S3, }); // バケットポリシーを作成 bucket.addToResourcePolicy(new iam.PolicyStatement({ actions: ["s3:*"], resources: [`${bucket.bucketArn}/*`], principals: [new iam.AnyPrincipal()], conditions: { "StringEquals": { "aws:SourceVpce": s3Endpoint.vpcEndpointId, }, }, })); } }
これをデプロイして、ECS Execを使ってシェルに入ります。そして実際にcurlしてみると、ダウンロードできるようになりました。
root@ip-10-0-168-184:/# curl https://s3-access-from-fargate-with-curl.s3.ap-northeast-1.amazonaws.com/test.txt Hello World!
また、この件で初めて知ったのですが、curlでPUTすればS3にアップロードすることも可能でした。
root@ip-10-0-168-184:/# echo "Hi! This is fargate container! $(date)" > fargate.txt root@ip-10-0-168-184:/# curl -XPUT -T fargate.txt https://s3-access-from-fargate-with-curl.s3.ap-northeast-1.amazonaws.com/fargate.txt
これで無事、FargateからS3にcurlでアクセスできる環境を作れました。めでたしめでたし