Lambda で異なる AWS アカウントへ接続してみる
こんにちは!クラウド事業本部コンサルティング部のたかくに(@takakuni_)です。
最近、Lambda で異なる AWS アカウントのリソースへアクセスしたい機会がありました。
私のケースは異なるのですが、たとえば中央集権的にリソースをチェックしたり、以下のように、別アカウントの Bedrock リソースにアクセスしたいケースが挙げられます。
車輪の再開発的なブログですが、私の理解も含めて整理してみたいと思います。
実装方式の策定
今回のポイントは 2 つです。以下の理由や背景をもとに実装方法を検討してみました。
- 何度も同じ処理を、別々の関数内で定義するのは管理が大変なため、 Lambda Rayer でひとつにまとめます。
- SAM での開発を想定し、なるべくローカルと本番側で差分がないように設定します。
接続元のリソース作成
まずは、接続元アカウント向けに IAM ロールを作成します。こちらは CloudFormation を利用します。
Principal は適宜変更しましょう。
Resources:
Role:
Type: AWS::IAM::Role
Properties:
RoleName: external-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: sts:AssumeRole
Principal:
AWS: 接続元AWSアカウントのID
ManagedPolicyArns:
- arn:aws:iam::aws:policy/ReadOnlyAccess
接続先のリソース作成
続いて接続先の AWS アカウントのセットアップを行います。
template
クロスアカウントの処理は、複数 Lambda で再利用される想定で Lambda Layer を使ってみました。(CrossAccountLayer
)
また、SwitchAccountFunction ではレイヤーを使いながら、スイッチロールしてみます。(SwitchAccountFunction
)
デフォルトでは AssumeRole できる権限は付与されていないため、 Policies
にポリシーを加えておきます。(ここ大事)
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: |
Sample SAM Template for Switch Role
Globals:
Function:
Tracing: Active
Api:
TracingEnabled: true
Resources:
####################################
# 00. Layer (Cross Account)
####################################
CrossAccountLayer:
Type: AWS::Serverless::LayerVersion
Metadata:
BuildMethod: python3.12
Properties:
LayerName: CrossAccountLayer
ContentUri: layers/cross_account/
CompatibleRuntimes:
- python3.12
####################################
# 01. Switch Account
####################################
SwitchAccountFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/switch_account/
Handler: app.lambda_handler
Runtime: python3.12
Architectures:
- x86_64
Layers:
- !Ref CrossAccountLayer
Timeout: 60
LoggingConfig:
LogFormat: JSON
Policies:
- Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- sts:AssumeRole
Resource: arn:aws:iam::*:role/external-role
コード周り
レイヤー
アカウント ID をもとに、セッションを作成する処理にしてみました。
Assume する IAM ロールの値は固定値で設定してるため、必要に応じて変更してください。
from typing import Dict, Any
import json
import boto3
import logging
import re
logger = logging.getLogger()
logger.setLevel("INFO")
# 定数定義
ROLE_NAME = "external-role"
SESSION_NAME = "external-session"
ACCOUNT_ID_PATTERN = re.compile(r'^\d{12}$')
def validate_account_id(account_id: str) -> bool:
"""
AWSアカウントIDを検証する
Args:
account_id (str): 検証するアカウントID
Returns:
bool: 有効なアカウントIDの場合True
"""
return bool(ACCOUNT_ID_PATTERN.match(str(account_id)))
def assume(account_id: str) -> Dict[str, Any]:
"""
指定されたアカウントのロールを引き受ける
Args:
account_id (str): 対象のAWSアカウントID
Returns:
Dict[str, Any]: 認証情報を含む辞書
Raises:
ValueError: 無効なアカウントID
ClientError: AWS API呼び出しエラー
"""
if not validate_account_id(account_id):
logger.error(f"Invalid account ID format: {account_id}")
raise ValueError("Invalid account ID format")
try:
sts_client = boto3.client('sts')
role_arn = f'arn:aws:iam::{account_id}:role/{ROLE_NAME}'
logger.info(f"Attempting to assume role: {role_arn}")
response = sts_client.assume_role(
RoleArn=role_arn,
RoleSessionName=SESSION_NAME,
DurationSeconds=3600 # セッション期間を1時間に設定
)
logger.info(f"Successfully assumed role for account: {account_id}")
return response['Credentials']
except ClientError as e:
logger.error(f"Failed to assume role for account {account_id}: {str(e)}")
raise
except Exception as e:
logger.error(f"Unexpected error while assuming role: {str(e)}")
raise
関数
続いて Lambda 関数のコードです。受け取ったセッションからクレデンシャルを引っ張り、クライアントを生成します。
生成されたクライアントを利用することで、スイッチ先アカウントへの操作ができます。
念の為、 finally でクレデンシャルを削除してみました。
import json
import boto3
import cross_account
import logging
from botocore.exceptions import BotoCoreError, ClientError
logger = logging.getLogger()
logger.setLevel("INFO")
def lambda_handler(event, context):
# アカウント ID からクロスアカウントの認証情報を取得
try:
account_id = event.get('account_id')
if not account_id:
logger.error("Missing account_id in event")
return { 'statusCode': 400, 'body': json.dumps({'message': 'Missing account_id'}) }
credential = cross_account.assume(account_id)
# # ここに処理を書く (例: S3 バケットの一覧取得)
# s3 = boto3.client(
# 's3',
# aws_access_key_id=credential['AccessKeyId'],
# aws_secret_access_key=credential['SecretAccessKey'],
# aws_session_token=credential['SessionToken']
# )
# response = s3.list_buckets()
logger.info(f"Successfully processed request for account: {account_id}")
# ここに応答結果を書く
return {
'statusCode': 200,
'body': json.dumps({'message': 'Successfully processed request'})
}
except (BotoCoreError, ClientError) as e:
logger.error(f"AWS API error: {str(e)}")
return {
'statusCode': 500,
'body': json.dumps({'message': 'AWS API error occurred'})
}
except Exception as e:
logger.error(f"Unexpected error: {str(e)}")
return {
'statusCode': 500,
'body': 'Internal server error'
}
finally:
# クレデンシャル情報のクリーンアップ
logger.info(f"Cleaning up credentials for account: {account_id}")
if 'credential' in locals():
del credential
logger.info(f"Credentials cleaned up for account: {account_id}")
動作
以下のイベント内容で sam local invoke
を行ってみます。
{
"account_id": "123456789012"
}
無事、実行できていますね。
takakuni@ hoge % sam local invoke SwitchAccountFunction --skip-pull-image -e events/account.json
Invoking app.lambda_handler (python3.12)
CrossAccountLayer is a local Layer in the template
arn:aws:lambda:ap-northeast-1:234567890123:layer:AWSLambdaPowertoolsPython:33 is already cached. Skipping download
Building image.........................
Requested to skip pulling images ...
Mounting /Users/takakuni.shinnosuke/Documents/hoge/.aws-sam/build/SwitchAccountFunction as /var/task:ro,delegated, inside runtime container
START RequestId: b830db0f-255c-4d2a-ba97-c3903fbe99eb Version: $LATEST
{"timestamp": "2025-01-27T09:26:41Z", "level": "INFO", "message": "Found credentials in environment variables.", "logger": "botocore.credentials", "requestId": "1b152782-b7fc-4c83-9e38-fee15d20c99d"}
{"level":"INFO","location":"assume:48","message":"Attempting to assume role: arn:aws:iam::123456789012:role/external-role","timestamp":"2025-01-27 09:26:43,053+0000","service":"cross_account"}
{"level":"INFO","location":"assume:56","message":"Successfully assumed role for account: 123456789012","timestamp":"2025-01-27 09:26:45,070+0000","service":"cross_account"}
{"timestamp": "2025-01-27T09:26:45Z", "level": "INFO", "message": "Successfully processed request for account: 123456789012", "logger": "root", "requestId": "1b152782-b7fc-4c83-9e38-fee15d20c99d"}
{"timestamp": "2025-01-27T09:26:45Z", "level": "INFO", "message": "Cleaning up credentials for account: 123456789012", "logger": "root", "requestId": "1b152782-b7fc-4c83-9e38-fee15d20c99d"}
{"timestamp": "2025-01-27T09:26:45Z", "level": "INFO", "message": "Credentials cleaned up for account: 123456789012", "logger": "root", "requestId": "1b152782-b7fc-4c83-9e38-fee15d20c99d"}
END RequestId: 1b152782-b7fc-4c83-9e38-fee15d20c99d
REPORT RequestId: 1b152782-b7fc-4c83-9e38-fee15d20c99d Init Duration: 0.67 ms Duration: 5789.42 ms Billed Duration: 5790 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"statusCode": 200, "body": "{\"message\": \"Successfully processed request\"}"}
まとめ
以上、「Lambda で異なる AWS アカウントへ接続してみる」でした。
SAM はとっても便利なデプロイツールです。「なるべくローカルと本番側で差分がないように」をテーマに実装してみました。
このブログがどなたかの参考になれば幸いです。
クラウド事業本部コンサルティング部のたかくに(@takakuni_)でした!