はじめに
皆さん、複数のAWSアカウントをまたいでAPI GatewayのIAM認証で呼び出しをしたい、といったことを思ったことはないでしょうか。
私はあります。
ということで、主に自分向けにはなりますが、CDKを使って別AWSアカウントのAPI GatewayをIAM認証で呼び出す方法を記載します。
概要
まずは、構成の概要について説明します。
アカウント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呼び出しは出来ません。
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はまだ決まっていないので仮の値が設定されます。
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なのでその情報が表示されています。
アカウントA側のデプロイ
次にアカウントA側のデプロイを実施します。
IAM認証のサンプルのAPIを作成します。
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のロールが設定されていることが確認できます。
アカウント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関数をテスト実行すると成功することが確認できます。
最後に
クロスアカウントで権限の付与を実施する場合は、デプロイする順番が重要になります。
許可を出す前に、許可を出す対象のロールが存在している必要があるためです。
この記事が誰かの助けになれば幸いです。




