ハマログ

株式会社イーツー・インフォの社員ブログ

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でアクセスできる環境を作れました。めでたしめでたし

 


[1] 挨拶、つまり200 OKを返すようにしないとALBのヘルスチェックが通らず、コンテナのデプロイに失敗します。そのためtaskImageOptionsのcommandにNode.jsのワンライナーを記述して、どんなリクエストにも200 OKを返すようにしています。毎リクエスト挨拶大事です
[2] なんでこの余談が突然出てきたのかというと、お察しかもしれませんが最初はAlpineベースのイメージをサンプルに使っていたためです。Alpineのglibc問題は有名ですが、初めてハマりました
AWSAWS CDKCDKcurlFargateS3

  koni   2023年7月25日


関連記事

Laravelでのログイン処理のかきかた

こんにちは、かねこです。 はじめに Laravelでの認証処理の実装方法について…

CodeBuildでEC2とAuroraのバックアップを取得する

はじめに CodePipelineで製品をデプロイする前に、EC2とAurora…

Laravel4.1から4.2へのアップグレード

Laravel4.1から4.2へのアップグレードを行いました。 composer…


← 前の投稿

次の投稿 →