Technote

by sizuhiko

AWS 環境の Next.js アプリから Lambda 呼び出しトレースを X-Rayで追跡できるようにする

Next.js アプリを AWS にデプロイしたとき、SSR などでサーバーサイドで実行した処理から Lambda など別の AWS サービスを呼び出すことがあると思います。 アプリケーションのオブザーバビリティを確保するため、AWS 上のアプリケーションでは X-RAY を使ったログ集積をやることが多いと思います。

ここでは Lambda など別のAWSサービスを呼び出したとき、トレースを追跡できるようにする方法について記録していきます。

また本記事での Next.js のバージョンは v15 を想定しています。それ以前のバージョンでは instrumentationexperimental になっていますのでご注意ください。

Next.js で X-RAY に出力できるように設定する

instrumentation.ts で以下のようにします。

export async function register() {
  if (process.env.NEXT_RUNTIME === 'nodejs') {
    await require('pino')
    await require('next-logger')
    await import('./instrumentation.node')
  }
}

ログが JSON で保持されていると X-RAY と相性が良いので、ロガーを pino にしておくと吉でお勧めです。

重要なのは NEXT_RUNTIMEnodejs の場合に instrumentation.node というファイルをダイナミックインポートすることです。 つまりサーバーサイドの処理のときの計測コードを使うようにします。

instrumentation.node.ts の内容は以下のようになります。

import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'
import { AWSXRayIdGenerator } from '@opentelemetry/id-generator-aws-xray'
import { AwsInstrumentation } from '@opentelemetry/instrumentation-aws-sdk'
import { AWSXRayPropagator } from '@opentelemetry/propagator-aws-xray'
import { defaultResource, resourceFromAttributes } from '@opentelemetry/resources'
import { NodeSDK } from '@opentelemetry/sdk-node'
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base'
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions'

const sdk = new NodeSDK({
  idGenerator: new AWSXRayIdGenerator(),
  textMapPropagator: new AWSXRayPropagator(),
  resource: defaultResource().merge(resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'your-app-service' })),
  instrumentations: [
    new AwsInstrumentation({ suppressInternalInstrumentation: true }),
  ],
  spanProcessors: [new BatchSpanProcessor(new OTLPTraceExporter())],
})

sdk.start()

OTEL 系のライブラリを使って計測の設定を行いますが、X-RAY を使うときは @opentelemetry/id-generator-aws-xray@opentelemetry/propagator-aws-xray が重要になります。 あと、aws-sdk を使った呼び出しを計測できるようにラッパーである @opentelemetry/instrumentation-aws-sdk を入れておくと楽になります。

この例では Next.js を App Runner(新規申込が終了してしまいました。残念ですね)にデプロイしているのでリソース定義も defaultResource().merge(resourceFromAttributes({ [ATTR_SERVICE_NAME]: 'your-app-service' })) のようにしています。ここは Next.js をデプロイしたリソースによって書き方が変わります。

あとはいい感じに AWS のサービスを SDK で呼び出すと、Next.js(App Runner)から DynamoDB へのリクエストとか、S3 へのアクセスとか全部 X-RAY 上で線がつながって表示されるので、システムトレースの追跡がしやすくなります。

なぜか Lambda だけ線がつながらない?

さて、DynamoDBとかS3などのマネージドサービスは線が接続されていて問題がなさそうに思うのですが、Lambdaの呼び出し(InvokeFunction)だけは、うまく線が接続されません。Lambdaから外部のAPIサーバーを実行していたり、Aurora DBを実行するトレースなどの情報が AppRunner とは結合されておらず、App Runner から Lambda の呼び出し、Lambda から HTTP(API) 呼び出しみたいな別の線が出来上がっていて、システムトレースの追跡ができません。

調べてみた

そうすると X-Amzn-Trace-Id がセグメントドキュメントの http.request に記録されておらず、Lambdaへ正しく伝播されてないことがわかりました。 X-Amzn-Trace-Id が X-RAY で線を結びつけるための値です。

OpenTelemetry の仕様を調べてみる

公式ドキュメント 手動コンテキスト伝搬 などが参考になります。

propagation.inject(context.active(), output);

上記のようにアクティブコンテキストに追加すれば良さそうです。 さらに AWS-SDK の Lambda クライアントでコマンドが送信されたときにフックするにはミドルウェアを使います。

公式ドキュメント ミドルウェアを使用してリクエストをログに記録する

上記サイトの例は DynamoDB クライアントですが、他のクライアントでも同様に実装できます。

メモ

通常、AwsInstrumentation は多くの AWS サービスをカバーしていますが、Lambda の Invoke においては、ペイロードや呼び出し形式の都合上、自動でのコンテキスト注入が期待通りに動かないケースがあります。 これを解決するために、AWS SDK v3 の Middleware Stack を利用して、リクエストが送出される直前に OpenTelemetry のアクティブコンテキストからトレース情報を抽出し、HTTP ヘッダーに明示的にねじ込む処理を追加します。

やってみた

手動伝搬するコードの追加

まず x-ray.ts を作ります。

import { FinalizeHandlerArguments, HttpRequest, MiddlewareStack } from '@aws-sdk/types'
import { context, propagation } from '@opentelemetry/api'

interface HasMiddlewareStack {
  middlewareStack: MiddlewareStack<any, any>
}

const headerSetter = {
  set: (carrier: Record<string, unknown>, key: string, value: string) => {
    carrier[key] = value
  },
}

export const addXRayInjector = <T extends HasMiddlewareStack>(client: T): void => {
  client.middlewareStack.add(
    (next) => async (args) => {
      const finalizeArgs = args as FinalizeHandlerArguments<object>
      const request = finalizeArgs.request as HttpRequest

      if (request?.headers) {
        const activeContext = context.active()
        const carrier = request.headers as Record<string, string>
        propagation.inject(activeContext, carrier, headerSetter)
      }

      return next(args)
    },
    {
      step: 'serialize',
      name: 'xRayContextInjector',
      override: true,
    }
  )
}

このコードでは AWS-SDK のクライアントから X-RAY のコンテキストにリクエストヘッダーを手動伝搬しています。

トレースコンテキスト $C$ は、スパン ID $S$ とトレース ID $T$、およびフラグ $F$ の集合として定義されます。

$$ C = \{T, S, F\} $$

この $C$ を X-Amzn-Trace-Id ヘッダーとして $C_{AppRunner} \rightarrow C_{Lambda}$ へ手動で注入(Inject)することが、今回のコードのキモです。

Lambda クライアントのインスタンスに設定する

そして LambdaClient インスタンスを生成したら、 addXRayInjector で手動伝搬するためのミドルウェアを設定するようにします。

  const client = new LambdaClient()
  addXRayInjector(client)

最後に

今回は Lambda クライアントについて手動伝搬できるような記事となっていますが、S3やDynamoDBのようにさらにその先がない場合は伝搬を気にしなくて良いのですが、Lambdaと同様に以下のサービスについても手動伝搬は必要になります。

  • StepFunctions
  • SQS

つまりリクエストが伝搬して、さらに別のサービスまでトレースを伝搬するようなサービスを利用するときは、リクエストを受け取ったサービス(今回の例でいうと Next.js)から、さきほどの addXRayInjector を設定して X-Amzn-Trace-Id を伝搬できるようにしましょう。

これで良きオブザーバビリティが実現できますように!

PHPカンファレンス福岡2025に参加した

PHPカンファレンス10周年おめでとうございます。

過去の参加ブログのリンクから:

ほとんど参加していると思っていたら、そうでもなかった。

前日入り

今回も前日入りしましたが、いきなり飛行機が遅延するというトラブルに。

こちらは昨年と同じ書き出しを1行コピペしてきましたが、今年も遅延から始まりました。 お昼ぐらいの来福者は時間どおりだったそうなので、朝だけだったようです。

福岡に到着したら WeWork 天神ブリッククロスへ。永和システムマネジメントでは東京支社がWeWork京橋内になったため、今回のように福岡に前日入りしてもWeWork拠点があれば作業できるのでとても便利。

こちらも昨年からのコピペに見えますが、WeWork の場所が変更となっています。昨年訪問した大名は新しい天神ブリッククロスに引越したようです。こちらは8月にオープンしたということで、まだ3ヶ月ぐらいの新しい拠点で、すごい新築の香りがしていました。

プロジェクトが多忙なこともあり、始発に近い時間に東京を出て、普通の勤務時間に仕事を開始してました。

お昼はWeWork近くにあった能古うどんへ。

夕方からは昨年と同じくWeWorkに知っている人たちが続々来て【非公式】PHPカンファレンス福岡2025・前日Meetup が行われました。昨年は袋詰めに参加していたのですが、今年はこちらに参加(偶然居合わせ?w)ていました。

そのあとは懇親会/2次会にも混ざって、そのあとは Rummy 行って美味しいラムをいただいて、締めにラーメンを食べて3時ちょいに寝ました。なんと気づくとほぼ昨年と同じパターン。

当日

ちゃんと朝起きれて今年は無事会場へ到着。

今回は多くの時間セッションに参加してました。PHP養分がたくさん補充されました。アンカンもなかったので、大量のインプットです。

技術面もエモさもたくさんあって、とても充実した1日となりました。来週からの仕事のモチベにもなります(PHP書いてないけど)。

AIというキーワードが多くあって、やっぱりそういう時流というか避けては通れない何かみたいなのは感じますね。とはいえ、その中で僕ら技術者がやるべきことというか、やることが変わっていく中での模索が続いているというのは、こういうリアルなコミュニティという現場で得られるなぁと強く感じました。

ちなみにお昼は、隣のウェストで。店中が同じようなイベントTシャツを着た人で溢れていて、お店の人も「何?」って後で話したりしてるんでしょうねぇw(そういえばランチの写真はFacebookにしかupしてなかった

そのあとはそのあとは懇親会 → 2次会 → 3次会 の流れで4時過ぎに解散となりました。 初めましての方、お久しぶりの方とたくさん話せました。楽しかった!(そして飲みすぎた!

※3次会の始まりのポストより

後日

今年はアフターハックがなかったので、ホテルの隣のエンジニアカフェにてこのブログを書いています。

2019年の記事の最後は

PHPカンファレンス福岡、来年も参加できると良いなー。

2024年の記事の最後は

そしてまた参加できるように願っています。

で終えていました。

PHPカンファレンス福岡は現体制での開催は今年で最後ということが事前に告知されております。 最後の赤瀬さんの挨拶を思い出して、ウルウルしながらこの1行を書いています。

福岡のコミュニティとのつながりでいうと、PHPMatsuri Fukuoka (ブログ記事: PHP Matsuri2012を終えて 〜 Retrospective of PHP Matsuri 2012 in Fukuoka)からで、最初にもリンクした過去の自分の記事を含めて見ながら懐かしいなぁというのと、少し寂しさも感じていました。

ところが懇親会で @kis さんがコミュニティの継続を熱く願って(少し無茶振り?)、fukuoka.php としての活動は新しい世代で始めるという流れを作ってくれました。

そうPHPカンファレンス福岡の始まりも、あのツイートからでしたからね

もちろん各地のPHPカンファレンスなどで福岡のコミュニティのみなさまと再開できることはあると思いますが、また福岡の地での再会があるかもしれません。

最後に Kenichiro トリオ写真を撮っていただいたので入れておきます。

Step Functions の HTTP タスクでプライベート API にアクセスする

Step Functions には HTTP タスクという便利な統合機能があり、ワークフローで HTTPS API を呼び出すことができるようになっています。 そうすることで Step Functions から単に HTTP の API を呼び出すだけなら Lambda を作らなくても良くなりました。便利ですね。

2024 Re:Invent の発表でプライベートAPIへのアクセスがサポートされた

ただ、これには制限があって、インターネットからアクセス可能なパブリックな API にしかアクセスできませんでした。つまり VPC 経由でアクセスする必要がある API には依然として VPC Lambda が必要だったのです。 しかし2024年の Re:Invent でプライベートAPIへのアクセスがサポートされることになり、益々利用シーンが増えそうです。

参考記事:

CDK では?

さっそく使ってみたいと思ったのですが、当然すぐにCDK ではゴニョゴニョできないのかな?とインターネットを調べてみたところ、さっそくサンプルがあるようです。

参考記事:

L2コンストラクタはまだないので、 CfnResourceGatewayCfnResourceConfiguration を使って作っていくようです。

具体的なCDKのコードはこちらです。

やってみた

とりあえず、あっさりできました。

注意点としてはプライベートAPIといいつつ、EventBridge Connection は HTTPs しか使えないので、 HTTP のプライベートAPIにアクセスしたい!という要望がある場合は工夫が必要です。まぁ VPC の中に LB をおいて、そこにプライベートAPIにアクセスするHTTPsエンドポイントを置いて、HTTPに変換して通すみたいな感じですかね。

めっちゃハマること

プライベートAPIのドメイン名を変更したい

0 -> 1 で作るときは問題ないんですけどね。

たとえば仮のドメイン名とかで構築していて、いざ実際の名前が決まったら置き換えたい、みたいなこと普通にあるじゃないですか。

    resourceConfig.addPropertyOverride('ResourceConfigurationDefinition.DnsResource', {
      DomainName: props.apiDomainName,
      IpAddressType: 'IPV4',
    })

たとえばリソースコンフィグでプライベートAPIのドメイン名を設定しておく必要があるんですが、このプロパティの値を変えるみたいなケースです。

変更するとき cdk diff するとちゃんと差分になって変更できそうに見えます。

Resources
[~] AWS::VpcLattice::ResourceConfiguration Api/ResourceConfig ApiResourceConfig50DED18C
 └─ [~] ResourceConfigurationDefinition
     └─ [~] .DnsResource:
         └─ [~] .DomainName:
             ├─ [-] 変更前のドメイン名
             └─ [+] 変更後のドメイン名
✨  Number of stacks with differences: 1

なんで、そのまま cdk deploy するじゃないですか。

DeployStack | 0/4 | 10:07:54 AM | UPDATE_IN_PROGRESS   | AWS::CloudFormation::Stack                 | DeployStack User Initiated
DeployStack | 0/4 | 10:08:00 AM | UPDATE_IN_PROGRESS   | AWS::VpcLattice::ResourceConfiguration     | Api/ResourceConfig (ApiResourceConfig50DED18C) 
DeployStack | 1/4 | 10:08:02 AM | UPDATE_COMPLETE      | AWS::VpcLattice::ResourceConfiguration     | Api/ResourceConfig (ApiResourceConfig50DED18C) 
DeployStack | 2/4 | 10:08:04 AM | UPDATE_COMPLETE_CLEA | AWS::CloudFormation::Stack                 | DeployStack 
DeployStack | 3/4 | 10:08:05 AM | UPDATE_COMPLETE      | AWS::CloudFormation::Stack                 | DeployStack 
 ✅  DeployStack
✨  Deployment time: 38s

成功しているように見えるじゃないですか。

でも、AWSのマネコンから確認すると….. 変わってません!!!!(衝撃、マジか

原因は不明です…

プライベートAPIのドメイン名を変更したいときの解決方法

一度 EventBridge Connection をパブリックAPIに戻します。 下のプライベートAPIの設定や、 CfnResourceGatewayCfnResourceConfiguration も全部削除します(ソースコード的にはコメントアウトでも良いけど)。

    const connection = new Connection(this, 'ApiConnection', {
      authorization: Authorization.apiKey('Authorization', props.secret.secretValue),
      connectionName: 'api-connection',
      description: 'プライベートAPIコネクション',
    })
    // ここから
    const cfnConnection: CfnConnection = connection.node.children[0] as unknown as CfnConnection
    cfnConnection.addPropertyOverride('InvocationConnectivityParameters', {
      ResourceParameters: {
        ResourceConfigurationArn: resourceConfig.attrArn,
      },
    })
    // ここまでを削除

して、一度デプロイしてリソースを削除して、再度元に戻してドメイン名の変更を適用して 0 -> 1 の状態で作り直します。

ちなみに、AWSのマネコンからドメイン名を変更しようと思っても GUI からもできないので、そういうものなのかもしれません。(diff と deploy は何をもって成功したのかはわからん…

まとめ

ということで、あまりスマートな解決策ではありませんが、現時点ではこういうやり方しかないのかなと思います。 いい感じに CDK からでも変更できるようになると良いなーと願っています(時間できたら issue 書こうかな

S3 に設置した zip ファイルから Lambda 関数をデプロイする

CDK で Lambda をデプロイするとき、S3 にある zip ファイルからのデプロイや、ソースコードからのデプロイなどいくつかのやり方があると思います。

今回は S3 の zip からデプロイする方法についてです。App Runner は ECR にコンテナイメージをデプロイして、それを自動連携しているので、Lambda もアプリイメージのデプロイと実際のアプリデプロイを分離しようと思ったわけです。 S3 の zip からデプロイする CDK のコードは以下のような感じになります。

    return new Function(this, 'SampleFunction', {
      functionName: 'sample',
      code: Code.fromBucket(
        Bucket.fromBucketName(this, 'Resource', `lambda-packages`),
        'sample.zip'
      ),
      handler: 'index.handler',
      architecture: Architecture.ARM_64,
      runtime: Runtime.NODEJS_20_X,
    })

Bucket.fromBucketName でアプリ側でデプロイ済みの S3 バケットを指定して、その中に入っている sample.zip からデプロイします。

で、このとき sample.zip の中身が変化していたとき、CDK では差分があるとしてデプロイされるのかな?と思っていました。

S3 の zip が異なるものでもデプロイはされない

でも、実際はそんなことなく zip が変更されたあとで cdk diff しても差分にはなってくれませんでした。 ちょうどそれらしい話題の issue Lambda Function Code.FromBucket() does not update “Code Entry Type” in AWS console がありました。

簡単に解説すると「CloudFormation は、変更されたテンプレートを元のテンプレートと比較し、変更セットを生成します。」ということで props に指定している箇所自体に変更がなければ変更とみなされないということですね。 具体的にいうと zip のプロパティまでは参照されないということです。

解決策として提案されているのは

  • Code.fromAsset() を使う。つまりソースコードからのデプロイ
  • objectVersionプロパティを指定。バージョン番号は別途管理する必要がある。

という感じでした。

S3 の変更イベントをトリガーにデプロイする

ということで、CDK がどうとかではなく、普通に思いつく解決策は S3 への PutObject を起点にデプロイをすることです。

S3へのレプリケーションをトリガーにLambdaのコード更新・新規バージョン発行を実行する とか、これ関係の記事はたくさんあるので、参考になります。

具体的な Lambda コードは、こんな感じになります。

import { LambdaClient, UpdateFunctionCodeCommand, UpdateFunctionCodeCommandInput } from '@aws-sdk/client-lambda'
import { S3Handler } from 'aws-lambda'

const client = new LambdaClient({ region: 'ap-northeast-1' })

export const handler: S3Handler = async (event) => {
  const bucket = event.Records[0].s3.bucket.name
  const object = event.Records[0].s3.object.key

  const functionNameMap =  {
    'sample.zip': 'sample',
    'sample-1.zip': 'sample1',
  }
  const FunctionName = functionNameMap[object]
  if (FunctionName === undefined) return

  const input: UpdateFunctionCodeCommandInput = { FunctionName, S3Bucket: bucket, S3Key: object }
  const updateCommand = new UpdateFunctionCodeCommand(input)
  await client.send(updateCommand)
}

で、それを CDK でデプロイします。

    const func =  new NodejsFunction(this, 'LambdaUpdaterFunction', {
      functionName: 'lambda-function-updater',
      entry: path.join(__dirname, 'src', 'index.ts'),
      handler: 'handler',
      architecture: Architecture.X86_64,
      runtime: Runtime.NODEJS_20_X,
    })
    func.addToRolePolicy(
      new PolicyStatement({
        actions: ['lambda:UpdateFunctionCode'],
        resources: [
            // 更新対象の Lambda の ARN を列挙する
        ],
      })
    )
    const packageBucket = Bucket.fromBucketName(this, 'Resource', `lambda-packages`)
    packageBucket.grantRead(func)
    packageBucket.addEventNotification(
      EventType.OBJECT_CREATED_PUT,
      new LambdaDestination(func)
    )

こちらはソースコードからデプロイするので NodejsFunction とかを使って簡易に実現します。 Lambda に zip ファイルからデプロイする対象の Function ARN を列挙して更新の許可を与えます。

zip ファイルが置かれているバケットの読み込み許可を更新 Lambda につけて、バケットにファイルが追加/変更されたときのイベントで Lambda を実行するように関連づけます。

この記事は参考になりました。

おまけ: NodejsFunction では ARM アーキテクチャを使えない

最初、lambda-function-updater をデプロイするとき、アーキテクチャに ARM を指定していたのですが、うまく動きませんでした。

なんでか調べたところ、こちらの issue Wrong SAM container image architecture when trying to build NodeJS lambda がありました。

使えなくないけど、公式にサポートしてないやり方になるよ、ってことで、まぁこの Lambda ぐらいだったら良いか… ってことで X86 アーキテクチャにしました。 もし ARM にしたいならビルドは自分でやっておいて zip からやった方が良いですね(話がループする)。

おまけ2: Lambda 関数をモノレポにしているとき sam build は使えない

S3 に zip ファイルをアップロードする前に TypeScript で書かれた Lambda を zip にするとき sam cli を使ってビルドしようとしたのですが、うまくいきませんでした。 調べてみると以下の issue を見つけました。

モノレポでビルドするのは茨の道のようです。 自分で esbuild を呼び出してビルドした方が簡単ですね。Lambda のコードを esbuild でビルドする方法はいろいろありますので、困らないでしょう。 僕らは package.json に定義しておきました。

        "build": "esbuild index.ts --bundle --minify --sourcemap --platform=node --target=es2020 --outfile=dist/index.js"

ネスト構造の Step Functions を CDK でデプロイする

この記事はネスト構造の Step Functions を CDK でデプロイしようとしたとき、うまくいかないことがあったので記録したものです。

なお Step Functions のワークフロー定義は、別途 JSON で用意されていて、CDK からは生成しない前提になっています。 ファイルは definitions の下に入っている想定です。

CDK でネスト構造の Step Functions を 定義する

たとえば ParentStateMachine から ChildStateMachine を実行すると仮定しましょう。

    const parent = new StateMachine(this, 'ParentStateMachine', {
      definitionBody: DefinitionBody.fromFile(
        path.join(__dirname, `/definitions/parent.json`),
        {}
      ),
    })
    const child = new StateMachine(this, 'ChildStateMachine', {
      definitionBody: DefinitionBody.fromFile(
        path.join(__dirname, `/definitions/child.json`),
        {}
      ),
    })
    // 子ステートマシーンの実行を親ステートマシーンに許可する
    child.grantStartExecution(parent)

みたいにすれば実行できるのかな?と思ってました。

デプロイしてみたが実行できない

実際にデプロイしてみたところ動きませんでした。

AWS の公式ドキュメント実行中の実行から新しい AWS Step Functions ステートマシンを起動するを確認しました。

ネストされたステートマシンの IAM アクセス許可の設定 というところに書いてあります。

親ステートマシンは、ポーリングとイベントを使用して子ステートマシンが実行を完了したかどうかを判断します。ポーリングには states:DescribeExecution のアクセス許可が必要ですが、EventBridge 経由で Step Functions に送信されるイベントには events:PutTargets、events:PutRule、events:DescribeRule のアクセス許可が必要です。

というか grantStartExecution ってそれらの権限当たらないの?って思うじゃないですか。

CDKのソースコードを見てみました。

  public grantStartExecution(identity: iam.IGrantable): iam.Grant {
    return iam.Grant.addToPrincipal({
      grantee: identity,
      actions: ['states:StartExecution'],
      resourceArns: [this.stateMachineArn],
    });
  }

identityStateMachine だったら actions を追加して欲しいなぁ….(って思いますよね)。これだと grant って言ってる割にただ単に actions に関数名のを追加するだけでしょ?ってなるからなー(愚痴です)。

L2コンストラクタを継承してメソッドを追加する

ということで、 StateMachine を継承したコンストラクタを作り、 grantNestedExecute という許可メソッドを追加してみました。

  grantNestedExecute(parent: StateMachine) {
    this.grantStartExecution(parent)
    this.grantRead(parent)
  }

あとは先ほどの定義を修正するだけです。

    const parent = new MyStateMachine(this, 'ParentStateMachine', {
      definitionBody: DefinitionBody.fromFile(
        path.join(__dirname, `/definitions/parent.json`),
        {}
      ),
    })
    const child = new MyStateMachine(this, 'ChildStateMachine', {
      definitionBody: DefinitionBody.fromFile(
        path.join(__dirname, `/definitions/child.json`),
        {}
      ),
    })
    // 子ステートマシーンの実行を親ステートマシーンに許可する
    child.grantNestedExecute(parent)

HTTP メソッドを実行する場合

Step Functions で HTTP メソッドを実行するみたいなのも同様に HTTP タスクを実行するための IAM アクセス許可を参照して許可設定を追加する必要があります。

先ほどの例と同じで MyStateMachinegrantConnection みたいなメソッドを追加しておきます。

  grantConnection() {
    const stack = Stack.of(this)
    this.addToRolePolicy(
      new PolicyStatement({
        actions: ['states:InvokeHTTPEndpoint'],
        resources: [
          stack.formatArn({
            service: 'states',
            region: stack.region,
            account: stack.account,
            resource: 'stateMachine:*',
          }),
        ],
      })
    )
    this.addToRolePolicy(
      new PolicyStatement({
        actions: ['events:RetrieveConnectionCredentials'],
        resources: [
          stack.formatArn({
            service: 'events',
            region: stack.region,
            account: stack.account,
            resource: 'connection/*',
          }),
        ],
      })
    )
    this.addToRolePolicy(
      new PolicyStatement({
        actions: ['secretsmanager:GetSecretValue', 'secretsmanager:DescribeSecret'],
        resources: [
          stack.formatArn({
            service: 'secretsmanager',
            region: '*',
            account: '*',
            resource: 'secret:events!connection/*',
          }),
        ],
      })
    )
  }

まとめ

Step Functions のワークフロー定義を、別途 JSON から取り込むと、必要な権限がいい感じにはなってくれないことがわかりました。 でも Workflow Studio みたいなビジュアルエディタでワークフロー書くのが便利なんで、それはそれで使いたいんですよね。

grant メソッドの CDK コードを確認して、公式ドキュメントと見比べながら適切な権限があたっているか確認しながら進めていくとハマることなく進められると思います。