Lambdaから別のアカウントのLambdaを呼び出すCDK構成

2022.01.25

はじめに

Lambdaから別のアカウントのLambdaを呼び出す場合、2つの方法があります。

  • IAMポリシーを使った呼び出し
  • リソースポリシーを使った呼び出し

それぞれの方法のメリット・デメリットは以下の記事が参考になります。

今回は2つの方法をCDK(v2)で実装する方法を紹介します。

対象読者

  • CDKとLambdaはTypeScriptを利用
  • CDKはv2を利用(v1でも参考になるとは思います)

環境情報

ツール/パッケージ バージョン
Node.js 16.3.0
aws-cdk-lib 2.8.0
aws-cdk 2.8.0
aws-sdk 2.1062.0
esbuild 0.14.12
TypeScript 3.9.7

構成

構成としては、以下の図のようになります。以後呼び出し元Lambdaをcaller、呼び出し先Lambdaをcalleeと表記します。

call-lambda

IAMポリシーを利用した呼び出し方法

callerの実装

CDK

コメントにて補足しています。

lib/caller-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import * as Lambda from 'aws-cdk-lib/aws-lambda'
import * as Iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

const CALLEE_AWS_ACCOUNT_ID = process.env.CALLEE_AWS_ACCOUNT_ID!

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

    // 後述するcallee側CDKで作成するロールのARN
    const CALLEE_ROLE_ARN = `arn:aws:iam::${CALLEE_AWS_ACCOUNT_ID}:role/callee-iam-invoke-role`
    // 後述するcallee側CDKで作成するLambdaのARN
    const CALLEE_LAMBDA_IAM_ARN = `arn:aws:lambda:ap-northeast-1:${CALLEE_AWS_ACCOUNT_ID}:function:sample-callee-iam`

    // callerのLambda実装
    const callerIamLambda = new NodejsFunction(this, 'CallerIamLambda', {
      functionName: "sample-caller-iam",
      entry: "./src/caller-lambda-iam.ts",
      runtime: Lambda.Runtime.NODEJS_14_X,
      environment: {
        CALLEE_ROLE_ARN: CALLEE_ROLE_ARN,
        CALLEE_LAMBDA_ARN: CALLEE_LAMBDA_IAM_ARN
      }
    })

    // calleeを呼び出すためのIAMロール(後述)に対し、assume roleできる権限を付与
    callerIamLambda.addToRolePolicy(
      new Iam.PolicyStatement({
        effect: Iam.Effect.ALLOW,
        actions: ['sts:AssumeRole'],
        resources: [CALLEE_ROLE_ARN],
      })
    );
  }
}

Lambda

src/caller-lambda-iam.ts

import { STS, Lambda } from 'aws-sdk';

const REGION = process.env.REGION!;

export const lambdaClient = new Lambda({
  region: REGION,
  signatureVersion: 'v4',
});

const stsClient = new STS();

const AWS_LAMBDA_FUNCTION_NAME = process.env.AWS_LAMBDA_FUNCTION_NAME!;

const CALLEE_ROLE_ARN = process.env.CALLEE_ROLE_ARN!;
const CALLEE_LAMBDA_ARN = process.env.CALLEE_LAMBDA_ARN!;

export const handler = async (): Promise<void> => {
  console.log(`start caller lambda`)
  // calleeのRoleを利用して、一時クレデンシャルを取得
  const role = await stsClient.assumeRole({
      RoleArn: CALLEE_ROLE_ARN,
      RoleSessionName: AWS_LAMBDA_FUNCTION_NAME,
    }).promise();

  // クレデンシャル情報を更新
  lambdaClient.config.update({
    accessKeyId: role.Credentials?.AccessKeyId,
    secretAccessKey: role.Credentials?.SecretAccessKey,
    sessionToken: role.Credentials?.SessionToken,
  });

  const invocationRequest: Lambda.InvocationRequest = {
    FunctionName: CALLEE_LAMBDA_ARN,
    InvocationType: 'RequestResponse',
    Payload: JSON.stringify({
      "message": "sample-caller-iam"
    }),
  };

  // calleeのLambda呼び出し
  await lambdaClient.invoke(invocationRequest).promise();
};

calleeの実装

CDK

lib/callee-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import * as Lambda from 'aws-cdk-lib/aws-lambda'
import * as Iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

// callerのアカウントID
const CALLER_AWS_ACCOUNT_ID = process.env.CALLER_AWS_ACCOUNT_ID!

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

    // calleeのLambda実装
    const calleeIamLambda = new NodejsFunction(this, 'CalleeIamLambda', {
      functionName: "sample-callee-iam",
      entry: "./src/callee-lambda.ts",
      runtime: Lambda.Runtime.NODEJS_14_X
    })

    // calleeの実行をcallerに許可にするIAMロール実装
    new Iam.Role(this, 'InvokerRole', {
      roleName: `callee-iam-invoke-role`,
      assumedBy: new Iam.AccountPrincipal(CALLER_AWS_ACCOUNT_ID), // callerのアカウントを許可
      inlinePolicies: {
        thing: new Iam.PolicyDocument({
          statements: [
            new Iam.PolicyStatement({
              actions: ['lambda:InvokeFunction'], // callerに付与される権限
              resources: [calleeIamLambda.functionArn], // callerが実行できるLambdaのARN
            }),
          ],
        }),
      },
    });
  }
}

Lambda

受け取るだけですので、ロギングのみしています

/src/callee-lambda.ts

interface Event {
  message: string
}

export const handler = async (event: Event) => {
  console.log(`start callee lambda`)
  console.log(`event: ${JSON.stringify(event)}`)
}

リソースポリシーを利用した呼び出し方法

callerの実装

CDK

lib/caller-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import * as Lambda from 'aws-cdk-lib/aws-lambda'
import * as Iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

const CALLEE_AWS_ACCOUNT_ID = process.env.CALLEE_AWS_ACCOUNT_ID!

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

    const CALLEE_LAMBDA_RESOURCE_ARN = `arn:aws:lambda:ap-northeast-1:${CALLEE_AWS_ACCOUNT_ID}:function:sample-callee-resource`

    // caller側のLambda実装
    const callerResourceBasedLambda = new NodejsFunction(this, 'CallerResourceLambda', {
      functionName: "sample-caller-resource",
      entry: "./src/caller-lambda-resource-based.ts",
      runtime: Lambda.Runtime.NODEJS_14_X,
      environment: {
        CALLEE_LAMBDA_ARN: CALLEE_LAMBDA_RESOURCE_ARN
      }
    })

    // caller側のLambdaにcallee側のLambdaを実行する権限を付与
    // (後述するcallee側のLambdaのリソースベースポリシーでcallerアカウントからの実行を許可する)
    callerResourceBasedLambda.addToRolePolicy(new Iam.PolicyStatement({
      effect: Iam.Effect.ALLOW,
      actions: ['lambda:InvokeFunction'],
      resources: [`${CALLEE_LAMBDA_RESOURCE_ARN}`]
    }))
  }
}

Lambda

src/caller-lambda.ts

import { Lambda } from 'aws-sdk';

const REGION = process.env.REGION!;

export const lambdaClient = new Lambda({
  region: REGION,
  signatureVersion: 'v4',
});

const CALLEE_LAMBDA_ARN = process.env.CALLEE_LAMBDA_ARN!;

export const handler = async (): Promise<void> => {
  console.log(`start caller lambda`)

  const invocationRequest: Lambda.InvocationRequest = {
    FunctionName: CALLEE_LAMBDA_ARN,
    InvocationType: 'RequestResponse',
    Payload: JSON.stringify({
      "message": "sample-caller-resource"
    }),
  };

  await lambdaClient.invoke(invocationRequest).promise();
};

calleeの実装

CDK

lib/callee-stack.ts

import { Stack, StackProps } from 'aws-cdk-lib';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'
import * as Lambda from 'aws-cdk-lib/aws-lambda'
import * as Iam from 'aws-cdk-lib/aws-iam';
import { Construct } from 'constructs';

const CALLER_AWS_ACCOUNT_ID = process.env.CALLER_AWS_ACCOUNT_ID!

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

    // calleeのLambda実装
    const calleeResourceBasedLambda = new NodejsFunction(this, 'CalleeResourceLambda', {
      functionName: "sample-callee-resource",
      entry: "./src/callee-lambda.ts",
      runtime: Lambda.Runtime.NODEJS_14_X
    })

    // リソースベースポリシーでcallerのAWSアカウントIDを許可
    calleeResourceBasedLambda.addPermission('invokePermission', {
      principal: new Iam.AccountPrincipal(CALLER_AWS_ACCOUNT_ID),
      action: 'lambda:InvokeFunction',
    });
  }
}

Lambda

src/callee-lambda.ts

interface Event {
  message: string
}

export const handler = async (event: Event) => {
  console.log(`start callee lambda`)
  console.log(`event: ${JSON.stringify(event)}`)
}

さいごに

本稿では、IAMポリシーとリソースポリシーを使ったクロスアカウントでのLambdaからLambdaを呼び出す方法を紹介しました。 caller側で呼び出し要件がLambdaしかない場合は、リソースベースポリシーの方が実装が薄く、管理するリソースも減るのでメリットが大きいです。 ユースケースに合わせて利用してみてください。