1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

API GatewayのIAM認証を使って別アカウントのAPIを呼び出す

1
Posted at

はじめに

皆さん、複数のAWSアカウントをまたいでAPI GatewayのIAM認証で呼び出しをしたい、といったことを思ったことはないでしょうか。
私はあります。
ということで、主に自分向けにはなりますが、CDKを使って別AWSアカウントのAPI GatewayをIAM認証で呼び出す方法を記載します。

概要

まずは、構成の概要について説明します。

Snag_1000123d.png

アカウントA側にAPI Gatewayを作成し、認証方法はIAM認証として設定します。
アカウントBに存在するLambda関数からアカウントAのAPI Gatewayを呼び出したい、という構成です。

ここで重要なポイントは以下です。

  • アカウントB側のLambbdaを先に作成し、アカウントBのLambdanに紐づくロールを先に作成する
  • アカウントA側のAPI GatewayのリソースポリシーにてアカウントBのLambdaに紐づくロールからの呼び出しを許可する

なお、アカウントBのロールが存在しない状態でアカウントA側でアカウントBのロールに対してAPI呼び出しを許可しようとするとデプロイ時にエラーになります

デプロイする流れは以下となります。
重要な点としては、アカウントB側のLambdaのデプロイが二回必要となる点です。

  • アカウントBのLambdaをデプロイする
  • アカウントAのAPI Gatewayをデプロイする
  • アカウントBのLambdaの環境変数にアカウントAのAPIのエンドポイントを設定、呼び出し権限の情報を更新して再度デプロイする

デプロイ詳細

ソースコードの構成は以下です。(一部省略)

プロジェクトルート/
├── bin/
│   └── cdk-app.ts
├── lib/
│   ├── api-gateway-stack.ts
│   ├── lambda-caller-stack.ts
└── lambda/
    └── index.py

アカウントB側のデプロイ

まず、アカウントB側のLambdaをデプロイします。
スタックのコードは以下となります。
アカウントB側のLambdaのロールにもアカウントAのAPIの呼び出し権限を付与する必要があります。
ただし、これだけではアカウントAのAPI呼び出しは出来ません。

lib/lambda-caller-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";

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

    // アカウントAのAPI情報(デプロイ時に環境変数で指定)
    const apiUrl =
      process.env.API_GATEWAY_URL ||
      "https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/";
    const accountAId = process.env.ACCOUNT_A_ID || "ACCOUNT_A_ID";
    const apiGatewayId = process.env.API_GATEWAY_ID || "API_ID";
    const region = process.env.AWS_REGION || "ap-northeast-1";

    // API呼び出し用Lambda
    const callerLambda = new lambda.Function(this, "ApiCallerLambda", {
      runtime: lambda.Runtime.PYTHON_3_11,
      handler: "index.lambda_handler",
      code: lambda.Code.fromAsset("lambda"),
      environment: {
        API_URL: apiUrl,
        TARGET_AWS_REGION: region,
      },
      timeout: cdk.Duration.seconds(30),
    });

    // アカウントAのAPI Gateway呼び出し許可
    callerLambda.addToRolePolicy(
      new iam.PolicyStatement({
        effect: iam.Effect.ALLOW,
        actions: ["execute-api:Invoke"],
        resources: [
          `arn:aws:execute-api:${region}:${accountAId}:${apiGatewayId}/*/*/*`,
        ],
      })
    );

    // Lambda実行ロールARNを出力(アカウントAのポリシーで使用)
    new cdk.CfnOutput(this, "LambdaExecutionRoleArn", {
      value: callerLambda.role!.roleArn,
      description: "Lambda Execution Role ARN for Account A policy",
    });
  }
}

なお、API呼び出しのLambdaの実装コードは以下になります。
APIの呼び出しURLはまだ決まっていないので仮の値が設定されます。

lambda/index.py
import json
import os

import boto3
import requests
from botocore.auth import SigV4Auth
from botocore.awsrequest import AWSRequest


def lambda_handler(event, context):
    """
    アカウントAのIAM認証付きAPI Gatewayを呼び出す
    """
    api_url = os.environ['API_URL']
    region = os.environ['TARGET_AWS_REGION']
    
    # boto3セッションから認証情報を取得
    session = boto3.Session()
    credentials = session.get_credentials()
    
    # SigV4署名を作成
    request = AWSRequest(method='GET', url=api_url)
    SigV4Auth(credentials, 'execute-api', region).add_auth(request)
    
    # 署名済みリクエストを準備
    prepped = request.prepare()
    
    # APIを呼び出し
    try:
        response = requests.get(prepped.url, headers=dict(prepped.headers))
        return {
            'statusCode': response.status_code,
            'body': json.dumps({
                'message': 'Successfully called Account A API',
                'response': response.text,
                'status': response.status_code
            })
        }
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps({
                'error': str(e)
            })
        }

以下のコマンドでデプロイします。
アカウントA側のアカウントIDを環境変数に設定します。

$ export ACCOUNT_A_ID="990XXXXXXXXX"
$ cdk depoy LambdaCallerStack

デプロイ後に以下のようにLambdaのロールARNが表示されます。
今回のアカウントBは254から始まるアカウントIDなのでその情報が表示されています。

Snag_ffa6ad9.png

アカウントA側のデプロイ

次にアカウントA側のデプロイを実施します。
IAM認証のサンプルのAPIを作成します。

lib/api-gateway-stack.ts
import * as cdk from "aws-cdk-lib";
import { Construct } from "constructs";
import * as apigw from "aws-cdk-lib/aws-apigateway";
import * as lambda from "aws-cdk-lib/aws-lambda";
import * as iam from "aws-cdk-lib/aws-iam";

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

    // アカウントBの特定ロールARN(デプロイ時に環境変数等で指定)
    const accountBRoleArn =
      process.env.ACCOUNT_B_ROLE_ARN ||
      "arn:aws:iam::ACCOUNT_B_ID:role/LambdaExecutionRole";

    // API用のバックエンドLambda(最小実装)
    const apiBackendLambda = new lambda.Function(this, "ApiBackendLambda", {
      runtime: lambda.Runtime.PYTHON_3_11,
      handler: "index.lambda_handler",
      code: lambda.Code.fromInline(`
def lambda_handler(event, context):
  return {
      'statusCode': 200,
      'body': 'Hello from Account A API'
  }
      `),
    });

    // クロスアカウントアクセス用のリソースポリシー
    const apiPolicy = new iam.PolicyDocument({
      statements: [
        new iam.PolicyStatement({
          effect: iam.Effect.ALLOW,
          principals: [new iam.ArnPrincipal(accountBRoleArn)],
          actions: ["execute-api:Invoke"],
          resources: ["execute-api:/*"],
        }),
      ],
    });

    // API Gateway(IAM認証付き)
    const api = new apigw.RestApi(this, "CrossAccountApi", {
      restApiName: "Cross Account IAM API",
      policy: apiPolicy,
      defaultMethodOptions: {
        authorizationType: apigw.AuthorizationType.IAM,
      },
      defaultIntegration: new apigw.LambdaIntegration(apiBackendLambda),
    });

    // ルートパスにGETメソッドを追加
    api.root.addMethod("GET");

    // 任意のパスを追加する例
    const resource = api.root.addResource("example");
    resource.addMethod("GET");

    // API URLを出力
    new cdk.CfnOutput(this, "ApiUrl", {
      value: api.url,
      description: "API Gateway endpoint URL",
    });

    new cdk.CfnOutput(this, "ApiId", {
      value: api.restApiId,
      description: "API Gateway ID",
    });
  }
}

以下でデプロイします。
アカウントBで作成したロールのARNを環境変数に設定します。

$ export ACCOUNT_B_ROLE_ARN="arn:aws:iam::254XXXXXXXXX:role/LambdaCallerStack-ApiCallerLambdaServiceRoleE549994-HJJrRZB6EYa3"
$ cdk deploy ApiGatewayStack

デプロイ後に作成されたリソースは以下です。
APIがIAM認証で設定されており、リソースポリシーにアカウントBのロールが設定されていることが確認できます。

Snag_1008b617.png

Snag_1009d0fd.png

アカウントBのデプロイ

最後にアカウントB側のデプロイを再度実施します。
アカウントAで作成したAPIの情報を設定します。

$ export API_GATEWAY_URL="https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/prod/example"
$ export API_GATEWAY_ID="xxxxxxxxxx"
$ export ACCOUNT_A_ID="990XXXXXXXXX"
$ cdk deploy LambdaCallerStack

アカウントBのLambda関数をテスト実行すると成功することが確認できます。

Snag_10187874.png

最後に

クロスアカウントで権限の付与を実施する場合は、デプロイする順番が重要になります。
許可を出す前に、許可を出す対象のロールが存在している必要があるためです。
この記事が誰かの助けになれば幸いです。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?