異なるAWSアカウントのDynamoDBにLambdaでアクセスする (CloudFormationで作成)

異なるAWSアカウントのリソースに対して、Lambdaからアクセスしてみました。
2021.09.02

異なるAWSアカウントにあるDynamoDBテーブルに対して、Lambdaでアクセスする方法を試してみました。

概要図

おすすめの方

  • 異なるAWSアカウントのリソースにLambdaでアクセスしたい
  • CloudFormation、AWS SAMで上記の仕組みを構築したい

アクセスする側のデプロイ(LambdaとIAMロール)

まずは、DynamoDBテーブルにアクセスする側のLambdaとIAMロールをデプロイします。

SAM Init

sam init \
    --runtime python3.8 \
    --name Cross-Access-Lambda-Sample \
    --app-template hello-world \
    --package-type Zip

SAMテンプレート

Lambda用のIAMロールを作成しています。この時点では、アクセスされる側のIAMロールは未作成ですが、名前は事前に決めておきます。

template.yaml

AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31

Parameters:
  DstAwsAccountId:
    Type: String

  DstRoleName:
    Type: String

  TableName:
    Type: String

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      CodeUri: hello_world/
      Handler: app.lambda_handler
      Runtime: python3.8
      Timeout: 5
      Environment:
        Variables:
          DST_AWS_ACCOUNT_ID: !Ref DstAwsAccountId
          DST_ROLE_NAME: !Ref DstRoleName
          TABLE_NAME: !Ref TableName
      Role: !GetAtt HelloWorldFunctionRole.Arn
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get

  HelloWorldFunctionLogGroup:
      Type: AWS::Logs::LogGroup
      Properties:
        LogGroupName: !Sub /aws/lambda/${HelloWorldFunction}

  HelloWorldFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: other-account-dynamodb-access-lambda-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              Service: lambda.amazonaws.com
      Policies:
        - PolicyName: other-account-dynamodb-access-lambda-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: "*"
              - Effect: Allow
                Action:
                  - sts:AssumeRole
                Resource: !Sub arn:aws:iam::${DstAwsAccountId}:role/${DstRoleName}

Outputs:
  HelloWorldApi:
    Value: !Sub https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/

Lambdaコード

アクセスされる側のIAMロールを指定し、AssumeRoleを行います。 boto3で「AssumeRoleの結果として得たAWSアクセスキー」を使って、DynamoDBテーブルにアクセスし、scan()を実行しています。

app.py

import os
import json
import boto3

DST_AWS_ACCOUNT_ID = os.environ['DST_AWS_ACCOUNT_ID']
DST_ROLE_NAME = os.environ['DST_ROLE_NAME']
TABLE_NAME = os.environ['TABLE_NAME']

def lambda_handler(event, context):
    sts = boto3.client('sts')
    access_info = sts.assume_role(
        RoleArn=f'arn:aws:iam::{DST_AWS_ACCOUNT_ID}:role/{DST_ROLE_NAME}',
        RoleSessionName='cross_acct_lambda'
    )

    dynamodb = boto3.resource(
        'dynamodb',
        aws_access_key_id=access_info['Credentials']['AccessKeyId'],
        aws_secret_access_key=access_info['Credentials']['SecretAccessKey'],
        aws_session_token=access_info['Credentials']['SessionToken'],
    )

    table = dynamodb.Table(TABLE_NAME)

    res = table.scan()

    return {
        'statusCode': 200,
        'body': json.dumps(
            res.get('Items', [])
        ),
    }

デプロイ

デプロイします。アクセスされる側のAWSアカウントIDとIAMロール名もパラメータで付与しています。

sam build

sam package \
    --output-template-file packaged.yaml \
    --s3-bucket cm-fujii.genki-deploy

sam deploy \
    --template-file packaged.yaml \
    --stack-name Cross-Access-Lambda-Sample-Stack \
    --s3-bucket cm-fujii.genki-deploy \
    --parameter-overrides \
        DstAwsAccountId=222222222222 \
        DstRoleName=dynamodb-access-role \
        TableName=access-sample-table \
    --capabilities CAPABILITY_NAMED_IAM \
    --no-fail-on-empty-changeset

動作確認用にAPIエンドポイントを取得

aws cloudformation describe-stacks \
    --stack-name Cross-Access-Lambda-Sample-Stack \
    --query 'Stacks[].Outputs'

APIにアクセスすると、失敗する

$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message": "Internal server error"}

CloudWatchのログを見ると、アクセスされる側のIAMロールに対して、AssumeRoleが失敗しています。 この時点では、まだ存在していないからです。

[ERROR] ClientError: An error occurred (AccessDenied) when calling the AssumeRole operation: User: arn:aws:sts::111111111111:assumed-role/other-account-dynamodb-access-lambda-role/Cross-Access-Lambda-Sample-Stac-HelloWorldFunction-MoDfF3dqCFI3 is not authorized to perform: sts:AssumeRole on resource: arn:aws:iam::222222222222:role/dynamodb-access-role

アクセスされる側のデプロイ

DynamoDBテーブルとIAMロールをデプロイします。

CloudFormationテンプレート

AssumeRole元として、アクセスする側のIAMロール(Lambdaが使うIAMロール)を指定します。 これによって、Lambdaが使うIAMロール以外はAssumeRoleができません。

dynamodb_and_iam.yaml

AWSTemplateFormatVersion: 2010-09-09
Description: DynamoDB and IAM Role

Parameters:
  SrcAwsAccountId:
    Type: String

Resources:
  AccessSampleTable:
    Type: AWS::DynamoDB::Table
    Properties:
      TableName: access-sample-table
      BillingMode: PAY_PER_REQUEST
      AttributeDefinitions:
        - AttributeName: deviceId
          AttributeType: S
      KeySchema:
        - AttributeName: deviceId
          KeyType: HASH

  DynamodbAccessRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: dynamodb-access-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
          - Effect: Allow
            Action: sts:AssumeRole
            Principal:
              AWS:
                # AWSアカウントIDだけの指定も可能だが、セキュリティ的な意味でに強く非推奨
                # - !Sub ${SrcAwsAccountId}
                - !Sub arn:aws:iam::${SrcAwsAccountId}:role/other-account-dynamodb-access-lambda-role
      Policies:
        - PolicyName: dynamodb-access-policy
          PolicyDocument:
            Version: 2012-10-17
            Statement:
              - Effect: Allow
                Action:
                  - dynamodb:Scan
                Resource:
                  - !GetAtt AccessSampleTable.Arn
      MaxSessionDuration: 3600

Outputs:
  DynamodbAccessRoleArn:
    Value: !GetAtt DynamodbAccessRole.Arn

デプロイ

アクセスする側のAWSアカウントIDをパラメータで付与しています。

aws cloudformation deploy \
    --template-file dynamodb_and_iam.yaml \
    --stack-name DynamoDB-and-IAM-Stack \
    --parameter-overrides \
        SrcAwsAccountId=111111111111 \
    --capabilities CAPABILITY_NAMED_IAM

DynamoDBテーブルにデータを格納する

適当にデータを格納しました。

DynamoDBテーブルの様子

あらためてAPIアクセスし、異なるAWSアカウントのDynamoDBからデータを取得する

無事に取得できました!!

$ curl https://xxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
[{"name": "Fooooooo", "deviceId": "d0002"}, {"name": "This is a pen.", "deviceId": "d0001"}]

さいごに

異なるアカウントのリソースにアクセスする方法の助けになれば幸いです。

参考