こしあん
2025-02-08

Lambdaのクロスアカウントクエリを試す


17{icon} {views}


AWS Organizationを利用し、TerraformでIAMロールを設定して親アカウントのLambdaから子アカウントの関数一覧を取得する手順を解説。AssumeRoleを介してクロスアカウントアクセスを実現し、安全にLambdaリソースの情報を取得できる構成を示す。

はじめに

ポイント

親子アカウントにロールを作る。IAMロールのポリシーが全ての肝

  • 管理アカウント側のロールmanagement-lambda-execution-role
    • 信頼関係はシングルアカウントのときと同様にlambda.amazonaws.com
    • ログの記録用にマネージドポリシーAWSLambdaBasicExecutionRoleを追加
    • 子アカウントのロールLambdaReaderCrossAccountRolests:AssumeRoleするための権限を追加。子アカウントのAssume Roleするためのポリシーがクロスアカウントになって変わった点
  • 子アカウントのロールLambdaReaderCrossAccountRole
    • 信頼関係は親のアカウントのロールmanagement-lambda-execution-rolests:AssumeRoleするための権限を追加。親のアカウントのロールを信頼関係として追加するというのがクロスアカウントになって変わった点
    • lambda:ListFunctionsの権限を追加。これはシングルアカウントのときと同様
  • Lambdaのプログラム:子アカウントのロールをAssume Roleする

図表で書いてみる

もうちょっと整理してみる。シングルアカウントの場合

信頼関係 ポリシー
シングルアカウント lambda.amazonaws.com アプリに必要な権限(例:lambda:ListFunctions)

クロスアカウントの場合。Lambdaは親アカウント側にデプロイしてクエリすることを想定。

信頼関係 ポリシー
クロスアカウント(親) lambda.amazonaws.com 子アカウントのロールをAssume Roleする権限(sts:AssumeRole)
クロスアカウント(子) 親のアカウントのロールへの信頼ポリシー アプリに必要な権限(例:lambda:ListFunctions)

ロール間のAssumeをくっつけてしまえばシングルアカウントと同じ

考え方

  • Lambdaを実行するのは親アカウント側だから、lambda.amazonaws.comへの信頼関係は親側のロールで必要
  • リソースを取ってくるのは子アカウント側だから、lambda:ListFunctionsの権限は子側のロールで必要
  • 親のLambdaのアプリケーションではAssume Roleするので、あとは適当にAssume Roleができるようにする

コード

Terraform

以下のようなプロバイダを定義する

# 管理アカウント用(profile: management)
provider "aws" {
  region  = "ap-northeast-1"
  profile = "management"
}

# develop アカウント用(profile: develop)
provider "aws" {
  alias   = "develop"
  region  = "ap-northeast-1"
  profile = "develop"
}

LambdaとIAMロールのデプロイ

##############################
# 管理アカウント側:Lambda 用実行ロール
##############################

resource "aws_iam_role" "lambda_role" {
  name = "management-lambda-execution-role"

  assume_role_policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "Service": "lambda.amazonaws.com"
        },
        "Action": "sts:AssumeRole"
      }
    ]
  })
}

# Lambda 用基本実行ポリシー(CloudWatch Logs 出力用)
resource "aws_iam_role_policy_attachment" "lambda_logs" {
  role       = aws_iam_role.lambda_role.name
  policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}

# developアカウント側のクロスアカウントロールへのsts:AssumeRoleを許可するポリシーを追加
resource "aws_iam_role_policy" "management_assume_develop_role_policy" {
  name = "allow-assume-develop-role"
  role = aws_iam_role.lambda_role.id

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Sid": "AllowAssumeDevelopRole",
        "Effect": "Allow",
        "Action": "sts:AssumeRole",
        "Resource": aws_iam_role.develop_lambda_reader.arn
      }
    ]
  })
}

##############################
# develop アカウント側:クロスアカウントで Lambda 関数一覧取得を許可するロール
##############################

resource "aws_iam_role" "develop_lambda_reader" {
  provider = aws.develop
  name     = "LambdaReaderCrossAccountRole"

  # 管理アカウント側の Lambda 実行ロール (aws_iam_role.lambda_role) を信頼する
  assume_role_policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Principal": {
          "AWS": aws_iam_role.lambda_role.arn
          # ※ もしクロスアカウント参照が難しい場合は、
          #     "AWS": "arn:aws:iam::<management_account_id>:role/management-lambda-execution-role"
          #   のように直接文字列で指定してください。
        },
        "Action": "sts:AssumeRole"
      }
    ]
  })
}

# develop 側で Lambda 関数一覧取得 (lambda:ListFunctions) を許可するポリシー
resource "aws_iam_policy" "develop_lambda_list_policy" {
  provider = aws.develop
  name     = "DevelopLambdaListPolicy"

  policy = jsonencode({
    "Version": "2012-10-17",
    "Statement": [
      {
        "Effect": "Allow",
        "Action": "lambda:ListFunctions",
        "Resource": "*"
      }
    ]
  })
}

resource "aws_iam_role_policy_attachment" "develop_policy_attach" {
  provider   = aws.develop
  role       = aws_iam_role.develop_lambda_reader.name
  policy_arn = aws_iam_policy.develop_lambda_list_policy.arn
}

##############################
# Lambda のソースコード ZIP 化(同ディレクトリ内の lambda_function.py を利用)
##############################

data "archive_file" "lambda_zip" {
  type        = "zip"
  source_file = "${path.module}/lambda_function.py"
  output_path = "${path.module}/lambda_function.zip"
}

##############################
# 管理アカウント側:Lambda 関数作成
##############################

resource "aws_lambda_function" "list_develop_functions" {
  function_name = "list_develop_functions"
  role          = aws_iam_role.lambda_role.arn
  handler       = "lambda_function.lambda_handler"
  runtime       = "python3.12"
  filename      = data.archive_file.lambda_zip.output_path
  source_code_hash = data.archive_file.lambda_zip.output_base64sha256

  # develop アカウント側のクロスアカウント用ロール ARN を環境変数として渡す
  environment {
    variables = {
      DEVELOP_ROLE_ARN = aws_iam_role.develop_lambda_reader.arn
    }
  }
}

Lambdaのコード

アプリケーション内でAssume Roleしているのが特徴。この仕組みさえ知ってしまえばそこまで難しくないと思う。

import boto3
import json
import os

def lambda_handler(event, context):
    # STS クライアント作成
    sts_client = boto3.client('sts')

    # 環境変数から develop アカウント側のロール ARN を取得
    develop_role_arn = os.environ.get('DEVELOP_ROLE_ARN')
    if not develop_role_arn:
        return {
            'statusCode': 500,
            'body': json.dumps('DEVELOP_ROLE_ARN 環境変数が設定されていません')
        }

    # develop アカウント側のロールを assume する
    try:
        assumed_role = sts_client.assume_role(
            RoleArn=develop_role_arn,
            RoleSessionName="LambdaAssumeRoleSession"
        )
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'AssumeRole エラー: {str(e)}')
        }

    credentials = assumed_role['Credentials']

    # develop アカウント(ap-northeast-1)用の Lambda クライアント作成
    lambda_client = boto3.client(
        'lambda',
        region_name='ap-northeast-1',
        aws_access_key_id=credentials['AccessKeyId'],
        aws_secret_access_key=credentials['SecretAccessKey'],
        aws_session_token=credentials['SessionToken']
    )

    # Lambda 関数一覧を取得
    try:
        response = lambda_client.list_functions()
        functions = [func['FunctionName'] for func in response.get('Functions', [])]
    except Exception as e:
        return {
            'statusCode': 500,
            'body': json.dumps(f'list_functions エラー: {str(e)}')
        }

    return {
        'statusCode': 200,
        'body': json.dumps(functions)
    }

結果

親側でLambdaを実行すると以下のように表示されるはず。関数名は適当に。

Response:
{
  "statusCode": 200,
  "body": "[\"LambdaFunction1\", \"LambdaFunction2\"]"
}

所感

  • 作ってみたらだいぶわかった


Shikoan's ML Blogの中の人が運営しているサークル「じゅ~しぃ~すくりぷと」の本のご案内

技術書コーナー

北海道の駅巡りコーナー


Add a Comment

メールアドレスが公開されることはありません。 が付いている欄は必須項目です