🐙

S3イベントをEventBridgeで受けてECSタスクで処理する(環境構築)

2023/01/26に公開

はじめに

S3イベントをトリガーとしてなんらかの処理を行いたい場合に最初に考えるのはLambdaでやる方法かと思います。
しかし、Lambdaだと実行時間15分の制限があり、最初はそれでも良いかもしれないけどデータが増えたら危ないかな?なんてことがあります。

そんな時にふと "s3 event ecs" と検索してみたところLambdaをかませずにEventBridgeでルール設定すればできそうだとわかりました。

https://docs.aws.amazon.com/ja_jp/AmazonCloudWatch/latest/events/CloudWatch-Events-tutorial-ECS.html

それならば割とシンプルなシステム構成で実現できそうなので軽い気持ちでこれでやってみようと思ったら意外と情報が少なくて大変だったのでここに書き残しておこうと思います。

やりたいこと

今回はAWS環境の構築にCDKを利用します。構築する環境は以下の構成です。
images01

S3にファイルがアップロードされたらそれをトリガーとしてECSタスクを起動して、アップロードされたファイルを読み込んでなんらかの処理をしたいです。

S3

まずはS3のバケットを用意します。

export class S3Stack extends cdk.Stack {
    // Create Bucket
    public bulkFilesBucket : s3.Bucket
    constructor(scope: Construct, id: string, props?: cdk.StackProps) {
        super(scope, id, props);

        this.bulkFilesBucket = new s3.Bucket(this, "TestBucket", {
            bucketName: "test-bucket",
            blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
            eventBridgeEnabled: true,
        });
    };
}

ここでのポイントは eventBridgeEnabled です。
trueにするとEventBridgeに各種イベントが飛ぶようになります。

ECS

VPC

ECSタスクを起動するVPCを用意します。
サブネットのところは一般的によくありそうな構成で書いてみましたが必要な環境に合わせて変えてください。

export class Vpcs {
    public vpc: Vpc
    constructor(scope: Construct, id: string) {
      this.vpc = new Vpc(scope, id, {
        ipAddresses: IpAddresses.cidr('10.0.0.0/16'),
        defaultInstanceTenancy: DefaultInstanceTenancy.DEFAULT,
        enableDnsSupport: true,
        enableDnsHostnames: true,
        subnetConfiguration: [
          {
            cidrMask: 24,
            name: "public",
            subnetType: ec2.SubnetType.PUBLIC,
          },
          {
            cidrMask: 24,
            name: "App",
            subnetType: ec2.SubnetType.PRIVATE_WITH_EGRESS,
          },
          {
            cidrMask: 28,
            name: "RDS",
            subnetType: ec2.SubnetType.PRIVATE_ISOLATED,
          },
        ],
        natGateways: 1,
        // maxAzs: 2,
      });
      this.vpc.applyRemovalPolicy(cdk.RemovalPolicy.DESTROY)
    }
}
  

SecurityGroup

ECSタスク用のセキュリティグループを用意します。

interface SecurityGroupProps extends StackProps {
    vpc: Vpc;
}

export class SecurityGroups {
    public ecsSecurityGroup: SecurityGroup;

    constructor(scope: Construct, id: string, props: SecurityGroupProps){
        this.ecsSecurityGroup = new SecurityGroup(scope, 'EcsSg', {
            vpc: props.vpc,
        })
    }
}

ECS & EventBridge

次にECSのタスク定義を書いていきます。
今回はEventBridgeのルールもEcsStack内に一緒に定義します。

export class EcsStack extends cdk.Stack {

        constructor(scope: Construct, id: string, props?: cdk.StackProps) {
            super(scope, id, props)

        // VPC
        const vpc = new Vpcs(this, id + 'Vpc').vpc
        // SecurityGroup
        const sg = new SecurityGroups(this, id, {
            vpc: vpc
        });
        // ECS Cluseter
        const cluster = new Cluster(this, id + 'Cluster', {
            clusterName: 'EventHandlerCluster',
            vpc: vpc,
        });
        // EventBridgeから起動するタスク定義
        const s3EventHandleTask = new FargateTaskDefinition(this, 's3EventHandleTask', {
            memoryLimitMiB: 512,
        });
        s3EventHandleTask.addContainer('handlerTaskContainer', {
            containerName: "s3EventHandleTaskContainer",
            image: ContainerImage.fromAsset('../../', {
                platform: Platform.LINUX_AMD64,
            }),
            command:[
                "/app",
            ],
            logging: LogDrivers.awsLogs({streamPrefix: 's3EventHandleTask'})
        });
        s3EventHandleTask.addToTaskRolePolicy(
            new PolicyStatement({
                actions: [
                    's3:*',
                ],
                resources: ["*"]
            })
        )
        // EventBrigeのルール定義とターゲットの設定
        const rule = new Rule(this, 's3EventRule', {
            ruleName: 's3EventRule',
            enabled: true,
            eventPattern: {
                "source": ["aws.s3"],
                "detailType": ["Object Created"],
                "detail": {
                        "bucket": {
                        "name": ["test-bucket"]
                    }
                }
            },
            targets: [new EcsTask({
                cluster: cluster,
                taskDefinition: s3EventHandleTask,
                taskCount: 1,
                securityGroups: [sg.ecsSecurityGroup],
                containerOverrides: [{
                    containerName: "s3EventHandleTaskContainer",
                    environment: [
                        {
                            name: "bucketName",
                            value: EventField.fromPath("$.detail.bucket.name")
                        },
                        {
                            name: "objectKey",
                            value: EventField.fromPath("$.detail.object.key")
                        },
                    ]
                }],
            })]
        });
    }
}

ここでのポイントは containerOverrides の部分です。
ここの environment の記述をすることでEventBrigeが受信したイベントの必要な箇所を環境変数に入れてコンテナを起動することができます。
これのやり方があまりサンプルが見つけられなくて試行錯誤しました。

EventField.fromPath のパスの指定はS3からのイベントについては以下のJsonを想定して記載します。
上記の例ではbucket name と object keyしか設定していませんがイベント内の他の項目取得できます。

{
  "version": "0",
  "id": "436cee3f-1218-b9b1-293d-cc0506eeba5f",
  "detail-type": "Object Created",
  "source": "aws.s3",
  "account": "XXXXXXXXXXXX",
  "time": "2021-11-30T03:56:14Z",
  "region": "us-east-1",
  "resources": ["arn:aws:s3:::test-bucket"],
  "detail": {
    "version": "0",
    "bucket": { "name": "test-bucket" },
    "object": {
      "key": "test.png",
      "size": 93617,
      "etag": "035bf1fc4b0a420b23c170769f6b7dfe",
      "sequencer": "0061A5A0DE6C9F3646"
    },
    "request-id": "DMGFBG77TRA4ZWAJ",
    "requester": "XXXXXXXXXXXX",
    "source-ip-address": "14.12.5.225",
    "reason": "PutObject"
  }
}

参考にした記事

イベントを環境変数にセットしてECSタスクを起動するにはinput transformerをすれば良いというのはわかったのですがそれをどう書くのか?も色々探りました。

https://stackoverflow.com/questions/62845436/how-to-use-input-transformer-for-ecs-fargate-launch-type-with-terraform-cloudwat

https://wp-kyoto.net/en/format-lambda-invocation-input-from-eventbridge-using-aws-cdk/

この記事がEventBrigeの先がLambdaだったのが自分のやりたいことに対してはおしかった
https://zenn.dev/nmemoto/articles/s3-eventnotification-with-eventbridge

終わりに

いくらか情報が少ないところがありこんなことできるのかな?それはどうやって書くのかな?と試行錯誤したところがありましたのでまとめてみました。
これから同じようなことをする方に少しでも参考になったら嬉しいです。

動作確認に使った全ソースコードは以下に置きました。
https://github.com/y16ra/aws-eventbridge-ecstask-example

GitHubで編集を提案

Discussion