AWS CDK で S3 の PUT をトリガーに Step Functions 起動する構成を作成してみた

2020.06.04

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部の佐藤です。

S3 の PUT をトリガーに Step Functions を呼び出す構成を作成していたところ、ちょっとハマってしまったので知見として残します。今回はAWS CDKを使ってリソースを構成していきます。

環境

  • AWS CDK 1.42.1
  • TypeScript 3.9.2
  • Node.js 12.16.0

AWS アーキテクチャ

以下のようなアーキテクチャを作成します。S3 に何らかのファイルが PUT されたのをトリガーに EventBridge にイベントを流し、ターゲットとして Step Functions のステートマシンを起動します。

やってみた

まずは、AWS CDK のプロジェクトを作成します。プロジェクト名は適宜変えます。

mkdir hoge
cdk init app --language=typescript
cd hoge

必要なライブラリをインストールします。

npm install --dev @aws-cdk/aws-stepfunctions @aws-cdk/aws-stepfunctions-tasks @aws-cdk/aws-lambda @aws-cdk/aws-s3 @aws-cdk/aws-events @aws-cdk/aws-events-targets

AWS CDKで実装

lib/*-stack.tsを以下のように実装します。Step Functions のステートマシンは受けたイベントをパススルーするだけの簡易的なものです。EventBridge ルールは S3 に PUT されたイベントをステートマシンに流す設定を行います。

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as sfn from '@aws-cdk/aws-stepfunctions';
import * as events from '@aws-cdk/aws-events';
import * as targets from '@aws-cdk/aws-events-targets';

export class HogeStepfunctionsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    // ファイルアップロード用バケット
    const fileUploadBucket = new s3.Bucket(this, 'fileUploadBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // StepFunctionsの定義
        const definition = new sfn.Pass(this, 'pass');
    const stateMachine = new sfn.StateMachine(this, 'StateMachine', {
        definition,
    });

    // EventBridge ルールのターゲットにStepFunctionsを設定する
    const sfnTarget = new targets.SfnStateMachine(stateMachine);

    // ファイルが PUT されたら StepFuntions を起動する EventBridge Rule を作成する
    const rule = new events.Rule(this, 'put event rule', {
        description:
            '特定のバケットにファイルがPUTされたら動作するルールです',
        eventPattern: {
            source: ['aws.s3'],
            detailType: ['AWS API Call via CloudTrail'],
            detail: {
                eventSource: ['s3.amazonaws.com'],
                eventName: [
                    'PutObject',
                    'CopyObject',
                    'CompleteMultipartUpload',
                ],
                requestParameters: {
                    bucketName: [fileUploadBucket.bucketName],
                },
            },
        },
    });
    rule.addTarget(sfnTarget);
  }
}

デプロイ

以下でデプロイを行います。

cdk deploy

以下のように単純にパススルーするだけのステートマシンがデプロイされています。

また、EventBridge のルールも以下のようにデプロイされています。

イベントパターンとは、イベントをフィルタする役割で、以下のように定義すると バケット名が hogeputObject, CopyObject, CompleteMultipartUpload が行われたら、そのイベントをターゲットに流すという処理になります。今回はターゲットが Step Functions なので、該当のステートマシンが起動します。

{
  "detail-type": [
    "AWS API Call via CloudTrail"
  ],
  "source": [
    "aws.s3"
  ],
  "detail": {
    "eventSource": [
      "s3.amazonaws.com"
    ],
    "requestParameters": {
      "bucketName": [
        "hoge"
      ]
    },
    "eventName": [
      "PutObject",
      "CopyObject",
      "CompleteMultipartUpload"
    ]
  }
}

動作確認

デプロイできたので、動作確認をしてみます。デプロイされたS3バケットに適当なファイルをPUTしてStep Functionsのステートマシンが起動するかを確認します。test.txt のような適当なファイルを作成し、AWSコンソールからS3バケットに移動し ドラッグ&ドロップでアップロードしてみます。

アップロードができたら、StepFunctionsのステートマシンが起動しているかを確認します。デプロイされたステートマシンに移動します。

なぜか実行されていなさそうです。

原因を探る

Step Functionsドキュメントを確認したところ、以下の情報にたどり着きました。

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-cloudwatch-events-s3.html

これによると、

ステップ 2: AWS CloudTrail で証跡を作成する

とありました。また、

Amazon S3​ の API イベントが CloudWatch イベント​ のルールを満たすには、これらのイベントを受信するように CloudTrail​ の証跡を設定する必要があります。

とあります。S3バケットのAPI 呼び出しの証跡がないと、PutObject などのイベントは EventBridge には流れないようです。なので、CloudTrailのS3証跡を作成する必要がありそうです。CloudTrailの設定を以下のハイライトの箇所に追加しました。

import * as cdk from '@aws-cdk/core';
import * as s3 from '@aws-cdk/aws-s3';
import * as sfn from '@aws-cdk/aws-stepfunctions';
import * as events from '@aws-cdk/aws-events';
import * as targets from '@aws-cdk/aws-events-targets';
import * as cloudtrail from '@aws-cdk/aws-cloudtrail';

export class HogeStepfunctionsStack extends cdk.Stack {
  constructor(scope: cdk.Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // The code that defines your stack goes here
    // ファイルアップロード用バケット
    const fileUploadBucket = new s3.Bucket(this, 'fileUploadBucket', {
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // StepFunctionsの定義
		const definition = new sfn.Pass(this, 'pass');
    const stateMachine = new sfn.StateMachine(this, 'StateMachine', {
        definition,
    });

		// PutObject ➾ EventBridge ➾ StepFunctionsを実現するためにはCloudTrailの設定が必要
    const trail = new cloudtrail.Trail(this, 'trail');
    trail.addS3EventSelector([{
        bucket: fileUploadBucket,
    }], {
        readWriteType: cloudtrail.ReadWriteType.ALL,
    });

    // EventBridge ルールのターゲットにStepFunctionsを設定する
    const sfnTarget = new targets.SfnStateMachine(stateMachine);

    // ファイルが PUT されたら StepFuntions を起動する EventBridge Rule を作成する
    const rule = new events.Rule(this, 'CSV put event rule', {
        description:
            '特定のバケットにファイルがPUTされたら動作するルールです',
        eventPattern: {
            source: ['aws.s3'],
            detailType: ['AWS API Call via CloudTrail'],
            detail: {
                eventSource: ['s3.amazonaws.com'],
                eventName: [
                    'PutObject',
                    'CopyObject',
                    'CompleteMultipartUpload',
                ],
                requestParameters: {
                    bucketName: [fileUploadBucket.bucketName],
                },
            },
        },
    });
    rule.addTarget(sfnTarget);
  }
}

再度デプロイします。

cdk deploy

以下のように、CloudTrailにアップロード対象S3の証跡が設定されていることを確認します。

再度動作確認

再度、S3バケットに対して適当なファイルをアップロードします。アップロード後、StepFunctionsの実行履歴を確認します。

今度は実行されていそうです。うまくいきました。

まとめ

最初に実装してみたときは、なんでステートマシンが起動しないんだ!?という感じで、結構ハマりました。まさかCloudTrailの設定が必要だとは思いもしませんでした。誰かの参考になれば幸いです。

参考

https://docs.aws.amazon.com/ja_jp/step-functions/latest/dg/tutorial-cloudwatch-events-s3.html

https://docs.aws.amazon.com/cdk/api/latest/docs/aws-construct-library.html