【別アカウントのLambda】を実行可能なLambdaをNode18 + AWS SDK for JavaScript v3で構築してみる

## 先に結論だけ知りたい人のために （クロスアカウントでLambdaからLambdaをコールする方法）

この記事の要点を列挙するよ。

 - 実行環境がNode18のLambdaにはAWS SDK for JavaScript v3が最初からインストールされていて便利だよ
 - Invoke APIで別アカウントのLambdaをコールするよ
 - コールされる対象の【別アカウントのLambda】には、リソースベースのポリシーが必要だよ
 - コールする側のLambdaには、アイデンティティベースのポリシーが必要だよ

```json:コールされる対象の【別アカウントのLambda】に付与するリソースベースのポリシー
{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "[任意の名前]",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::[呼び出す側のAWSアカウントID]:root"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:[リージョン名]:[呼び出される側のAWSアカウントID]:function:[関数名]"
    }
  ]
}
```

```json:コールする側のLambda（の実行ロール）に付与するアイデンティティベースのポリシー
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "[任意の名前]",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:[リージョン名]:[呼び出される側のAWSアカウントID]:function:[関数名]"
        }
    ]
}
```

## 筆者プロフィール

Kenpal株式会社でITエンジニアとして色々いじってる faable01 です。

https://www.kenpalinc.com/

ものづくりが好きで、学生時代から創作仲間と小説を書いたりして楽しんでいたのですが、当時はその後自分がIT技術者になるとはつゆ程も思っていませんでした。紆余曲折あり、20代の半ばを過ぎた頃に初めてこの業界と出会った形です。

趣味は「技術記事を口語で書くこと」です。

個人サイトでもちょくちょく技術記事を書いてます。読んでね（これを追記した2023年4月時点では、よくAWS CDKをラップした爆速サーバレス開発ツールのSSTについて記事にしてるよ）

https://signote.link

それから、業務日報SaaS **「RevisNote」** を運営しています。リッチテキストでの日報と、短文SNS感のある分報を書けるのが特徴で、組織に所属する人数での従量課金制です。アカウント開設後すぐ使えて、無料プランもあるから、気軽にお試しください。

https://revisnote.com?1710501811980

## やりたいこと

３行でまとめるよ。

 - アカウントその１のLambdaが
 - 別アカウント（アカウントその２）のLambdaを
 - コールできるようにしたい

さっそくやってみる。

## 事前準備：異なる２つのアカウントで簡単なLambdaを作る

２つのアカウントで **Node18で動く** Lambda関数を作成しよう。ここでは下記のように命名するよ。

- １つ目のアカウントのLambda関数は「hello01」
- 2つ目のアカウントのLambda関数は「hello02」

実行環境をNode18にすることを忘れずにね。 **Node18が実行環境のLambdaなら、デフォルトでAWS JavaScript SDK v3を使えるんだ。** つまりnode_modulesを用意してzipにしてから……なんて手間がいらない。

以前のバージョンのNodeが実行環境のLambdaだと、デフォルトのSDKはV2時代のものだから、ちょっと不便だよ。

ちなみにバージョンごとに使えるSDKについて知りたければ下記の公式ドキュメントを見てね。 **（日本語版ドキュメントは更新が遅いケースがあって、2023年01月末現在ではまだNode18に関する記述がないから、あえて英語版ドキュメントのURLを記載しているよ）**

https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html

まずは、アカウント越えとは関係なく、単純なHello Worldの関数を作るよ。

それぞれの実装は下記の通り。 **両方とも東京リージョンで作るよ。**

```js:hello01（アカウントその１。東京リージョン）
export const handler = async(event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify({
            message: 'hello 01',
        }),
    };
    return response;
};
```

```js:hello02（アカウントその２。東京リージョン）
export const handler = async(event) => {
    const response = {
        statusCode: 200,
        body: JSON.stringify({
            message: 'hello 02',
        }),
    };
    return response;
};
```

## アカウントその１がアカウントその２のLambda関数を実行するコードを書いてみる

hello01（アカウントその１のLambda関数）に手を加えて、hello02（アカウントその２のLambda関数）をInvoke APIでコールさせてみるよ。

**現時点では相応の権限を持っていないから、エラーになるはずだけどね。**

ちなみにInvoke APIっていうのは下記のことだよ。

https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/API_Invoke.html

AWS JavaScript SDK v3におけるInvoke APIは下記だね。

https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lambda/classes/invokecommand.html

ドキュメントに目を通した上で早速実装してみよう。

```js:hello01（アカウントその１。東京リージョン）
import { LambdaClient, InvokeCommand } from "@aws-sdk/client-lambda";

// アカウントその２の「アカウントID」。
// マネジメントコンソール右上のメニュー等から確認できる
// ---- TODO: 自分が用意したアカウントその２のIDに置き換えてね ----
const ACCOUNT_02_ID = '123456789012'

export const handler = async(event) => {
    const client = new LambdaClient();
    
    // 引数: https://docs.aws.amazon.com/AWSJavaScriptSDK/v3/latest/clients/client-lambda/interfaces/invokecommandinput.html
    const command = new InvokeCommand({
        FunctionName: `arn:aws:lambda:ap-northeast-1:${ACCOUNT_02_ID}:function:hello02`
    });
    
    // hello02（アカウントその２のLambda）からの応答本文（要デコード）
    const responseBodyFromAccount02 = JSON.parse(JSON.parse(Buffer.from((
        await client.send(command)
    ).Payload).toString()).body);
    // 自分自身（アカウントその１）としての応答
    const responseSelf = {
        statusCode: 200,
        body: JSON.stringify({
            // アカウントその２の応答メッセージをここに入れてみる
            messageFromAccount02: responseBodyFromAccount02.message,
            // もともと返していたメッセージはここに入れておく
            messageSelf: 'hello 01',
        }),
    };
    return responseSelf;
};
```

ポイントは下記だね。

 - `await client.send(command)` の返り値Payloadはデコードが必要
 - デコード後するとhello02の応答JSONを得られる
 - 応答本文は応答JSONをパースして `body` から得ることができる
 - 今回は応答本文そのものもJSONなので、もう一度JSON.parseしている

実装したら早速試してみよう。AWSマネジメントコンソールでhello01関数の画面を開いて「Test」押下で試し実行用のイベントを作成できるよ。

（Lambdaというのはイベント駆動の関数実行サービスだから、何か実行したいなら「イベント」を作成してやる必要があるんだ）

押下時のイベント名や引数（イベントJSON）は適当でいいからね。

イベントを作成したらもう一度「Test」を押せば実行できる。試しにやってみとこんなエラーが出るよ。

```jsonc
{
  "errorType": "AccessDeniedException",
  "errorMessage": "User: arn:aws:sts::[アカウントその１のID]:assumed-role/hello01-role-xxxxxxxx/hello01 is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-northeast-1:[アカウントその２のID]:function:hello02 because no resource-based policy allows the lambda:InvokeFunction action",
  "trace": [
    // ---- 略 ----
  ]
}
```

エラーメッセージに `resource-based policy` っていう単語があるよね。

> because no resource-based policy allows the lambda:InvokeFunction action

これは日本語ドキュメントで「リソースベースのポリシー」と呼ばれている存在だよ。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_identity-vs-resource.html

エラーメッセージを読めば、「リソースベースのポリシーがLambdaのInvokeFunctionを許可していないよー」って言われてることが分かるよね。

そういうわけだから次の手順で、リソースベースのポリシーに `lambda:InvokeFunction` を許可してあげるように設定するよ。

## hello02（アカウントその２のLambda）のリソースベースのポリシーに `lambda:InvokeFunction` を許可させる

hello02関数に下記のアクセス権限（リソースベースのポリシー）を与えるよ。

 - アカウントその１のhello01関数が、アカウントその２のhello02関数をlambda:InvokeFunctionでトリガーできるアクセス権限

そのために、hello02関数のマネジメントコンソール上から **「アクセス権限を追加」** メニューを開こう。hello02関数の「設定」タブ内に「アクセス権限を追加」ボタンがあるから、そこを押下してね。

「アクセス権限を追加」メニューが開いたら、次のようにアクセス権限を入力するよ。

 - アクセス権限の付与対象を「AWSアカウント」にする
 - 任意のステートメントIDを入力
 - アカウントその１のAWSアカウントIDを入力
 - 許可するアクションに「lambda:InvokeFunction」を入力

これで決定を押せば完了！　作成されたリソースベースのポリシードキュメントを確認すると次のようになっているはずだよ。

```json
{
  "Version": "2012-10-17",
  "Id": "default",
  "Statement": [
    {
      "Sid": "from_hello01",
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::[アカウントその１のID]:root"
      },
      "Action": "lambda:InvokeFunction",
      "Resource": "arn:aws:lambda:ap-northeast-1:[アカウントその２のID]:function:hello02"
    }
  ]
}
```

さて、これで改めてhello01を実行してみると……まだエラーが出るよね。次のようなエラーが出力されたはずだよ。

```jsonc
{
  "errorType": "AccessDeniedException",
  "errorMessage": "User: arn:aws:sts::[アカウントその１のID]:assumed-role/hello01-role-xxxxxxxx/hello01 is not authorized to perform: lambda:InvokeFunction on resource: arn:aws:lambda:ap-northeast-1:[アカウントその２のID]:function:hello02 because no identity-based policy allows the lambda:InvokeFunction action",
  "trace": [
    // ---- 略 ----
  ]
}
```

ちょっと気付きにくいけど、よく見たらエラーメッセージが変わったよね。さっきはリソースベースのポリシーが足りなくて怒られていたけど、 **今度は `identity-based policy` が足りないって書いてあるよね。**

> because no identity-based policy allows the lambda:InvokeFunction action

これは日本語ドキュメントで「アイデンティティベースのポリシー」と呼ばれている存在だよ。リソースベースのポリシーと同じドキュメントに説明が書いてあるから、単語としては見覚えがあるかもしれないね。

https://docs.aws.amazon.com/ja_jp/IAM/latest/UserGuide/access_policies_identity-vs-resource.html

そういうわけだから、今度はhello01（アカウントその１のLambda）の実行ロールにlambda:InvokeFunctionを許可するよう手を加えてあげようか。

## hello01（アカウントその１のLambda）のアイデンティティベースのポリシーとして、hello01実行ロールにlambda:InvokeFunctionを許可させる

hello01（アカウントその１のLambda）側のマネジメントコンソールの設定タブから、割り当てられている実行ロールを確認できるよね。IAMの編集画面から、このロールにlambda:InvokeFunction実行可能な権限を付与してあげよう。

IAMのマネジメントコンソールに遷移して、このロールの許可ポリシーにlambda:InvokeFunctionを追加するよ。インラインポリシーとして次の権限を付与すればいいからね。

```json
{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Sid": "[任意の名前]",
            "Effect": "Allow",
            "Action": "lambda:InvokeFunction",
            "Resource": "arn:aws:lambda:ap-northeast-1:[アカウントその2のID]:function:hello02"
        }
    ]
}
```

hello02（アカウントその２のLambda）リソースに対してInvokeFunctionを実行できるようにしているわけだね。

## hello01（アカウントその１のLambda）からhello02（アカウントその２のLambda）をコールしてみる

これでhello01（アカウントその１のLambda）からhello02（アカウントその２のLambda）をコールする準備は整ったはずだね。さっそく、ちゃんとアカウント越えできるかを試してみよう。

hello01側のマネジメントコンソールから、Test押下でhello01を実行してみるよ。そうすると……

```json
{
  "statusCode": 200,
  "body": "{\"messageFromAccount02\":\"hello 02\",\"messageSelf\":\"hello 01\"}"
}
```

無事にアカウント02のLambda関数hello02の応答を得ることができた！

狙い通り【別アカウントのLambda】を実行することができたから、これで完了だよ。

## まとめ

 - 実行環境がNode18のLambdaにはデフォルトでAWS SDK for JavaScript v3がインストールされているから、node_modulesを用意してzip化するような手間がいらないよ
 - 別アカウントのLambdaを実行するには、Invoke API（InvokeCommand）を使えばいいよ
 - そのためにはリソースベースのポリシーと、アイデンティティベースのポリシーの双方でlambda:InvokeFunctionを許可する必要があるよ