API GatewayのCanary機能を使って、クロスアカウントでLambdaを切り替えてみた
以前、「ALBの加重ルーティング」と「API GatewayのCanary機能」を使ってクロスアカウントの段階的な移行を試してみたという記事で、ALBの加重ターゲットグループとAPI GatewayのCanaryリリースを組み合わせ、クロスアカウントでのB/Gデプロイを検証しました。あの構成はVPC PeeringやPrivateLink・VPC Link V1を組み合わせる必要があり、それなりに構築コストのかかるものでした。
今回はその番外編です。「そもそもALBやVPC Linkを挟まなくても、API GatewayのCanary機能だけでバックエンドのLambdaを直接別アカウントへ切り替えられるのでは?」と思い立ち、実際に試してみました。
結論から言うと、技術的に可能なことを確認できました!
API GatewayのCanaryはクロスアカウントのLambda統合と組み合わせても問題なく機能し、トラフィックの振り分け・昇格・無効化まで一通り実現できました。
この記事は、前回の記事を読んで「もっとシンプルな構成でもできるのでは」と思った方や、API GatewayのCanaryとクロスアカウントLambda統合を組み合わせた場合の実態を知りたい方に向けて書いています。
なお、検証に使ったリソース一式はCloudFormationで再現できるようにしています。
1. 背景・やること
前回記事のおさらい
前回の記事では、ALBの加重ターゲットグループとAPI GatewayのCanaryリリースを組み合わせ、アカウントAからアカウントBへ段階的にリクエストを振り替える構成を検証しました。フロントエンドはVPC PeeringとIPターゲットを使ったALBの加重ルーティング、バックエンドはPrivateLinkとVPC Link V1を組み合わせたAPI Gateway、という構成です。0%→50%→100%と少しずつ切り替えられることは確認できましたが、VPC PeeringやPrivateLink・VPC Linkの準備が必要で、それなりに構築コストがかかるものでした。
もっとシンプルにできないか
API Gatewayには、別アカウントのLambda関数を直接統合バックエンドにできる機能があります。だったら、ALBやVPC Linkを挟まずに、API GatewayのCanaryだけでバックエンドのLambdaを直接別アカウントへ切り替えられるのでは?今回はこの構想を実際に検証してみました。
今回検証する内容
具体的には、API GatewayのステージにCanaryを設定し、base(自アカウントの旧Lambda)とcanary(別アカウントの新Lambda)へトラフィックを振り分けられるかを確認します。振り分けが確認できたらCanaryを昇格して全トラフィックを新Lambdaへ向け、最後にCanary設定を無効化する。このB/Gデプロイの一連の流れを、実機で確認していきます。
先に結論
結論を先に書くと、Canaryの仕組み自体はクロスアカウントLambdaでも問題なく機能し、実際にbase/canaryでの振り分けを実現できました(詳細は4章で紹介します)。
今回の構成方針
検証用のベースリソース(Lambda×2・REST API・prodステージ・クロスアカウント許可)はCloudFormationで作成し、誰でも同じ状態を再現できるようにしています。一方でCanary設定・昇格・無効化はイメージを掴んでいただきたいので、CLI/コンソールで実施しています。
スコープ外
以下は本記事の範囲外とします。
- CodeDeploy/Lambdaエイリアスの加重によるカナリア(今回のAPI Gatewayステージカナリアとは別の仕組みです)
- HTTP API(ステージカナリアはREST API限定の機能です)
- クロスリージョンでの検証
- 監視・自動昇格を組み込んだパイプライン化
2. 前提知識(軽く整理)
検証に入る前に、押さえておきたい前提知識を整理しておきます。
API Gatewayステージカナリアの仕組み
Canaryは、デプロイ(API設定のスナップショット)単位でbaseとcanaryを切り替える仕組みです。Canaryが有効なステージに再デプロイすると、新しいデプロイはcanary側に入り、base(stage.deploymentId)は据え置かれます。つまり同一ステージ上で、baseとcanaryという異なる2つのAPI設定を同時に配信できます。詳細は公式ドキュメントのSet up an API Gateway canary release deploymentをご確認ください。
クロスアカウントLambda統合
REST APIは、別アカウントのLambda関数を統合バックエンドにできます。正式にサポートされた機能で、統合先のLambda側でlambda:InvokeFunctionをAPI Gatewayに許可するリソースベースポリシーを付与すれば呼び出せます。手順の詳細はTutorial: Create a REST API with a cross-account Lambda proxy integrationをご確認ください。
本検証最大の落とし穴:ステージ変数はクロスアカウントLambda非対応
Canaryで呼び出し先を切り替える際によく使われるのが、ステージ変数によるオーバーライドです。ところが公式ドキュメントには、こんな記載があります。
To use a stage variable for a Lambda function, the function must be in the same account as the API. Stage variables don't support cross-account Lambda functions.
つまり、ステージ変数を使ってLambda関数を指定する場合、その関数はAPIと同一アカウントである必要があり、クロスアカウントのLambdaはステージ変数の対象にできません。詳細はAPI Gateway stage variables reference for REST APIsをご確認ください。
この制約により、前回ご紹介した「ステージ変数のcanary overrideで呼び先を切り替える」という手法は今回の用途では使えません。統合URIにフルARNを直書きし、base/canaryのデプロイ差分で切り替えるしかない。これが本検証の設計です。この点が、5章で紹介する運用上の懸念に直結します。
3. 検証環境
検証環境の構成は次のとおりです。
- アカウントA: API Gateway(REST API
canary-xacct)と、旧環境用のLambdahello-oldを配置 - アカウントB: 新環境用のLambda
hello-newを配置
hello-old・hello-newはどちらも/hello宛のGETリクエストに対し、{"account": "A (old)"}のように呼び出し元がどちらのLambdaかをaccountフィールドで返すだけのシンプルな実装です。この違いを見れば、Canaryによる振り分け先がひと目でわかります。
ベースリソースはCloudFormationテンプレート2枚で構築しています。
account-a.yaml: 旧Lambda(hello-old)、REST APIcanary-xacct(/helloGET, AWS_PROXY統合)、canary設定を持たないprodステージの初期デプロイを作成します。account-b.yaml: 新Lambda(hello-new)に加え、アカウントAのAPI Gatewayからの呼び出しを許可するクロスアカウント許可(CrossAccountInvokePermission)を、統合の張り替えより先に付与しておきます。
AWSTemplateFormatVersion: "2010-09-09"
Description: >
[アカウントA] API Gateway カナリア×クロスアカウント検証用のベースリソース。
旧Lambda(hello-old) + REST API(/hello GET, AWS_PROXY統合) + prodステージを作成する。
カナリア設定(canarySettings)・統合先のアカウントBへの張り替え・動作確認は
スコープ外(マネジメントコンソールから手動実施)。
# ---------------------------------------------------------------------------
# デプロイ順: このテンプレート(A)を先にデプロイ → 出力 RestApiId を控える
# → account-b.yaml のパラメータに渡してアカウントBをデプロイする。
# ---------------------------------------------------------------------------
Resources:
# --- 旧Lambda(アカウントA。現行の呼び出し先) の実行ロール ---
OldFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# --- 旧Lambda 本体。account="A (old)" を返す ---
OldFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: hello-old
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt OldFunctionRole.Arn
Timeout: 10
Code:
ZipFile: |
import json
MARKER = "A (old)"
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json", "Account": "A"},
"body": json.dumps({"account": MARKER, "path": event.get("path")}),
}
# --- REST API 本体 ---
RestApi:
Type: AWS::ApiGateway::RestApi
Properties:
Name: canary-xacct
EndpointConfiguration:
Types:
- REGIONAL
# --- /hello リソース ---
HelloResource:
Type: AWS::ApiGateway::Resource
Properties:
RestApiId: !Ref RestApi
ParentId: !GetAtt RestApi.RootResourceId
PathPart: hello
# --- GET メソッド + 旧Lambda への AWS_PROXY 統合 ---
# 初期状態は自アカウントの旧Lambdaを指す。アカウントBへの張り替えは手動(カナリア工程)。
HelloGetMethod:
Type: AWS::ApiGateway::Method
Properties:
RestApiId: !Ref RestApi
ResourceId: !Ref HelloResource
HttpMethod: GET
AuthorizationType: NONE
Integration:
Type: AWS_PROXY
IntegrationHttpMethod: POST
Uri: !Sub arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${OldFunction.Arn}/invocations
# --- 旧Lambda(自アカウント)に API Gateway からの呼び出しを許可 ---
OldFunctionInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref OldFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/hello
# --- デプロイ(APIスナップショット) ---
# カナリアはマネコンで後付けするため、ここでは canarySettings を付けない。
Deployment:
Type: AWS::ApiGateway::Deployment
DependsOn: HelloGetMethod
Properties:
RestApiId: !Ref RestApi
# --- prod ステージ ---
ProdStage:
Type: AWS::ApiGateway::Stage
Properties:
StageName: prod
RestApiId: !Ref RestApi
DeploymentId: !Ref Deployment
Outputs:
RestApiId:
Description: REST API ID。account-b.yaml の RestApiId パラメータに渡す。
Value: !Ref RestApi
AccountAId:
Description: アカウントA(API所有側)のアカウントID。account-b.yaml の ApiGatewayAccountId に渡す。
Value: !Ref AWS::AccountId
Region:
Description: リージョン。account-b.yaml の ApiRegion に渡す。
Value: !Ref AWS::Region
MethodSourceArn:
Description: クロスアカウント許可(アカウントB)で使う SourceArn。base/canary 共通(同一ステージ名prod)。
Value: !Sub arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/hello
InvokeUrl:
Description: 動作確認用 URL(/hello)。初期状態は account="A (old)" が返る。
Value: !Sub https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/hello
OldFunctionArn:
Description: 旧Lambda(アカウントA)のARN。
Value: !GetAtt OldFunction.Arn
AWSTemplateFormatVersion: "2010-09-09"
Description: >
[アカウントB] API Gateway カナリア×クロスアカウント検証の移設先リソース。
新Lambda(hello-new) + アカウントA の API Gateway からの
クロスアカウント呼び出し許可(resource-based policy)を作成する。
統合先の張り替え・カナリア設定・動作確認はスコープ外(マネコンから手動実施)。
# ---------------------------------------------------------------------------
# デプロイ順: account-a.yaml を先にデプロイし、その出力(RestApiId / AccountAId /
# Region)を下記パラメータに渡してからこのテンプレートをデプロイする。
# ---------------------------------------------------------------------------
Parameters:
ApiGatewayAccountId:
Type: String
Description: API Gateway を所有するアカウントA のアカウントID(account-a.yaml の AccountAId 出力)。
AllowedPattern: "^[0-9]{12}$"
RestApiId:
Type: String
Description: アカウントA で作成した REST API の ID(account-a.yaml の RestApiId 出力)。
ApiRegion:
Type: String
Default: ap-northeast-1
Description: API Gateway のリージョン(account-a.yaml の Region 出力。両アカウント同一想定)。
Resources:
# --- 新Lambda(アカウントB。移設先) の実行ロール ---
NewFunctionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: lambda.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# --- 新Lambda 本体。account="B (new)" を返す ---
NewFunction:
Type: AWS::Lambda::Function
Properties:
FunctionName: hello-new
Runtime: python3.12
Handler: index.lambda_handler
Role: !GetAtt NewFunctionRole.Arn
Timeout: 10
Code:
ZipFile: |
import json
MARKER = "B (new)"
def lambda_handler(event, context):
return {
"statusCode": 200,
"headers": {"Content-Type": "application/json", "Account": "B"},
"body": json.dumps({"account": MARKER, "path": event.get("path")}),
}
# --- アカウントA の API Gateway からの呼び出しを許可(クロスアカウント) ---
# SourceArn はアカウントA のメソッドARN。base/canary は同一ステージ名 prod を
# 共有するため、この1つの許可で双方をカバーできる想定(記事の要確認ポイント①)。
CrossAccountInvokePermission:
Type: AWS::Lambda::Permission
Properties:
FunctionName: !Ref NewFunction
Action: lambda:InvokeFunction
Principal: apigateway.amazonaws.com
SourceArn: !Sub arn:aws:execute-api:${ApiRegion}:${ApiGatewayAccountId}:${RestApiId}/*/GET/hello
Outputs:
NewFunctionArn:
Description: 新Lambda(アカウントB)のARN。マネコンで統合URIをこのARNに張り替える(カナリア工程)。
Value: !GetAtt NewFunction.Arn
デプロイはA→Bの順に行い、account-a.yamlのOutputs(RestApiId/AccountAId/Region)をaccount-b.yamlのParametersに渡します。使用するリソースはREST API×1、Lambda×2、IAMロール×2、CloudWatch Logsロググループで、リージョンは両アカウントともap-northeast-1に統一しています。
前提ツールはAWS CLI v2と、アカウントA・B用にそれぞれ設定した2つのプロファイルです。
4. やってみた
4-1. CFNでベースリソースを一括デプロイ
まずはaws cloudformation deployで、account-a.yaml→account-b.yamlの順にスタックをデプロイします。account-a.yamlのOutputs(RestApiId/NewFunctionArn等)を取得し、account-b.yamlのパラメータへ受け渡します。デプロイが完了したら、curlで旧Lambdaの応答"A (old)"が返ってくることを確認しておきます。
アカウントAにCloudFormationのスタックを作成し、API Gatewayおよび旧環境用のLambda関連リソースを作成

作成されたスタックの「出力」タブからアカウントBでスタックを作成する際にパラメータに入力する値を確認する
- アカウントAのアカウントID
- アカウントAに作成されたAPI GatewayのRestApi ID

アカウントBにスタックを作成し、先ほど確認した値をパラメータに入力する

アカウントBのスタック作成完了

4-2. prodステージにカナリアを作成(base 50% / canary 50%)
次に、prodステージにCanaryを作成します。update-stageコマンドでcanary設定を追加し、get-stageでbase/canaryそれぞれのdeploymentIdを確認します。この時点ではまだ統合を張り替えていないため、base/canaryとも同一のdeploymentIdを指しているはずです。
アカウントAでAPI GatewayのprodステージにCanaryを作成する
アカウントAに作成されたスタックの「リソース」タブから論理IDRestApiの物理ID(青字)を押下

左側のメニューバーから「ステージ」を選択
prodステージの下の方にスクロールすると、「Canary」タブがあるため選択
「Canaryを作成」を押下

「Canaryを作成」を押下
※この時点ではCanaryへのトラフィック比率は変更せず「0」のままでOK

確認のため、CloudShell上で以下を実行
※<RestApiId>は先ほどスタックの「リソース」タブからAPI GatewayのREST APIの画面に遷移するために押下した青字を入力
aws apigateway get-stage --rest-api-id <RestApiId> --stage-name prod \
--query '{base:deploymentId, canary:canarySettings}'
期待される結果
deploymentId(base)とcanarySettings.deploymentId(canary)が同一値、percentTraffic=0.0

4-3. 統合を別アカウントBのLambdaに変更して再デプロイ
ここが本検証の肝です。2章で触れたとおり、ステージ変数はクロスアカウントLambdaに対応していません。そこでput-integration相当の操作で、統合URIにアカウントBのLambdaのフルARNを直書きします。クロスアカウントの呼び出し許可はaccount-b.yamlで先回りして付与済みなので、追加のadd-permissionが不要かどうかを実機で確認します。統合を変更したら再デプロイし、get-stageでbase/canaryのdeploymentIdが分岐したことを確認します。
アカウントAのAPI Gatewayコンソールを開き、対象のREST API(canary-xacct)を選択
左メニュー「リソース」(Resources)→ /hello リソース配下の GET メソッドを選択
メソッド実行画面で「統合リクエスト」タブを選択し、「編集」を押下

編集画面で、Lambda関数 のARNが指定されているフィールドを修正
通常はプルダウンに自アカウントの関数のみが候補として出るが、このフィールドはテキスト入力も受け付けるため、アカウントBのLambdahello-newのフルARN
(arn:aws:lambda:ap-northeast-1:<アカウントB>:function:hello-new)を直接貼り付け
※アカウントBに作成したスタックの「出力」タブから確認可能

「保存」を押下し、メソッド実行画面の統合リクエストに、アカウントBのARNが反映されていることを確認

画面右上の「APIをデプロイ」を押下

デプロイ先ステージで prod を選択し、「デプロイ」を押下

確認のため、再度CloudShell上で以下を実行
※<RestApiId>は先ほどスタックの「リソース」タブからAPI GatewayのREST APIの画面に遷移するために押下した青字を入力
aws apigateway get-stage --rest-api-id <RestApiId> --stage-name prod \
--query '{base:deploymentId, canary:canarySettings}'
期待される結果
deploymentId(base)とcanarySettings.deploymentId(canary)が異なる値になっていること、percentTraffic=0.0

4-4. リクエストが振り分けられていることを確認
実際に新環境へトラフィックが振り分けられているかを確認します。カウントスクリプトでリクエストを繰り返し実行し、アカウントA/Bへの振り分けが50:50付近になることを確認します。振り分けはリクエスト毎のランダムでスティッキーではないため、同じURLに連続でアクセスしても都度A/Bどちらかに振り分けられます。ちなみに実行ログを有効化すると、CloudWatch Logsで/Canaryロググループが分離して出力されるので、あわせて確認しておくとよいポイントです。
実際に新環境へ振り分けられるかを確認してみる
左側のメニューバーから「ステージ」を選択
prodステージの下の方にスクロールすると、「Canary」タブがあるため選択
「編集」を押下

Canaryを50%に設定し、「保存」を押下

設定完了

ターミナルで以下を実行し、アカウントAとBに振り分けられていることを確認
for i in $(seq 1 100); do
curl -s "<InvokeUrl>"
echo
done | sed -nE 's/.*"account": *"([^"]*)".*/\1/p' | sort | uniq -c
実際の実行結果
今回はアカウントAに47回、アカウントBに53回振り分けられたことを確認した

4-5. カナリアを昇格
Canaryを昇格させます。CLIであればupdate-stageでcanarySettingsのdeploymentIdをステージ本体にコピーする操作に相当しますが、今回はコンソールから昇格を実行し、全リクエストが新Lambda(アカウントB)に向くことを確認します。
「Canaryを昇格」を押下

「Canaryを昇格させる」を押下
※チェックボックスはチェックをつけたままでOK

昇格完了画面

再度、ターミナルで以下を実行し、すべてアカウントBに振り分けられていることを確認
for i in $(seq 1 100); do
curl -s "<InvokeUrl>"
echo
done | sed -nE 's/.*"account": *"([^"]*)".*/\1/p' | sort | uniq -c
実際の実行結果
アカウントBに100回すべて振り分けられていることを確認した

アカウントBのLambdaの「モニタリング」タブから「呼び出し」メトリクスを観測
Canary設定に応じて、呼び出し数が変化していることを確認

4-6. カナリアを無効化
最後にCanaryを無効化します。percentTrafficを0にするか、canarySettings自体を削除する方法がありますが、今回はコンソールから「Canaryを削除」を実行し、移設が完了したことを確認します。
「Canaryを削除」を押下

「削除」を押下

削除完了画面

5. ハマりどころ・実機で確認したポイント
フルARN直書き方式の運用上の懸念点
今回の検証で一番引っかかったのが、統合URIにフルARNを直書きする方式そのものが抱える運用上の懸念です。
- 切り替えのたびに人間がコンソール/CLIで統合URIを書き換える必要がある: ステージ変数のように値を差し替えるだけでは済まず、統合そのものを編集して再デプロイする、という手順を毎回踏むことになります。
- 変更履歴がIaCの外側に残る: CFNで管理しているステージやLambdaのすぐ隣で、統合の向き先だけが手動変更されるため、いつ誰がどのARNに向けたのかを追いにくくなります。
- 切り戻しが「戻し忘れ」を誘発しやすい: 昇格後の切り戻しは「統合を旧ARNに書き戻して再デプロイ」でしか行えず、ワンクリックで戻せる仕組みがありません。
前回記事のALB加重ターゲットグループ方式では、ターゲットグループの重みを変更するだけで切り替えが完結し、今回のような「統合の書き換え」は発生しません。この点だけを比べても、繰り返し切り替えが発生する現場では前回の方式に分がありそうです。
そのほか実機で確認したポイント
- クロスアカウント許可をCFN(
account-b.yaml)で先回り付与しておく設計が、統合張り替え後に追加操作なしで機能するか。SourceArnはメソッドARN1つでbase/canary両方をカバーできるかを確認しました。 - 統合を編集したあとは、必ず再デプロイしないと反映されない点。忘れやすいので注意が必要です。
- Canaryが有効なステージを、非canaryの別デプロイに紐付けることができない(無効化が必要)点。
- 手動で入れたCanary設定・統合変更が、CFNスタックの状態とズレる(ドリフトする)点。スタックを更新・削除する際は、この手動変更分をどう扱うか意識しておく必要があります。
6. まとめ
今回の検証を通じてわかったのは、API GatewayのCanary機能自体は、クロスアカウントのLambda統合と組み合わせても問題なく動作する、ということです。base/canaryへのトラフィック振り分け、昇格、無効化まで、一連の流れを実機で確認できました。
ただ実用面では、Canaryのステージ変数を使った切り替えがクロスアカウントLambdaには使えないという制約があり、統合URIのフルARNの書き換えと再デプロイという手段しか取れないことがわかりました。一度きりの検証や小規模な移行であれば割り切れる範囲ですが、何度も切り戻しが発生しうる本番移行や、変更をIaCで厳密に管理したい現場では、ヒューマンエラーによる作業ミスのリスクが高いと感じました。
7. 参考リンク(一次情報)
- 「ALBの加重ルーティング」と「API GatewayのCanary機能」を使ってクロスアカウントの段階的な移行を試してみた(前回記事)
- Set up an API Gateway canary release deployment
- Create a canary release deployment
- Promote a canary release
- CanarySettings(APIリファレンス)
- Tutorial: Create a REST API with a cross-account Lambda proxy integration
- API Gateway stage variables reference for REST APIs







