カスタムリソースの Lambda でAssumeRoleして別アカウントでS3バケットを作成する

2022.07.13

こんにちは、森田です。

こちらの記事では、タイトルの通り別アカウントからのCloudFormationでS3バケットの作成を行います。

カスタムリソースの Lambdaに対して権限委任を行い、スタック作成・削除に伴いS3バケットの作成・削除をBoto3を用いて行います。

やりたいこと

AWSアカウントA のCloudFormationから AWSアカウントB でS3バケットの作成を行います。

上記を実現させるために、AWSアカウントA のロールに権限委任を行います。

構成は以下のようになります。

上記を実現させるため、

  • カスタムリソースのLambdaのロール作成
  • AWSアカウントAへ権限委任するロール作成
  • カスタムリソースのLambdaからS3を作成する

のCloudFormationテンプレートを作成します。

やってみる

CloudFormationテンプレートの作成

カスタムリソースのLambdaのロール作成

custom_resource_role.yml (クリックして展開)
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  RoleName:
    Type: String
    Description: RoleName
    Default: "AssumeRole-Lambda" 

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${RoleName}-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                    - sts:AssumeRole
                Resource: "*"
Outputs:
  SwitchLambdaRole:
    Value: !GetAtt LambdaExecutionRole.Arn
    Export:
      Name: SwitchLambdaRoleARN

AWSアカウントAへ権限委任するロール作成

assume_role.yml (クリックして展開)
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  RoleName:
    Type: String
    Description: RoleName
    Default: "AssumeRole-Lambda" 
  AccountNumber:
    Type: String
    Description: External AWS Account ID

Resources:
  LambdaExecutionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Ref RoleName
      AssumeRolePolicyDocument:
        Version: "2012-10-17"
        Statement:
          - Effect: Allow
            Principal:
              AWS:
                - !Sub arn:aws:iam::${AccountNumber}:role/${RoleName}
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: !Sub ${RoleName}-policy
          PolicyDocument:
            Version: "2012-10-17"
            Statement:
              - Effect: Allow
                Action:
                  - s3:CreateBucket
                  - s3:DeleteBucket
                Resource: "*"

カスタムリソースのLambdaからS3を作成する

cfn.yml (クリックして展開)
AWSTemplateFormatVersion: "2010-09-09"

Parameters:
  BucketName:
    Type: String
    Description: Create Bucket Name
  RoleName:
    Type: String
    Description: External AWS Account Role
    Default: "AssumeRole-Lambda" 
  AccountNumber:
    Type: String
    Description: External AWS Account ID

Resources:

  SwitchHandler:
    Type: Custom::SwitchHandler
    Properties:
      ServiceToken: !GetAtt "LambdaFunction.Arn"

  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      FunctionName : !Join ['-', [!Sub '${AWS::StackName}-Lambda', !Select [0, !Split ['-', !Select [2, !Split ['/', !Ref 'AWS::StackId' ]]]]]]  
      Role: !ImportValue SwitchLambdaRoleARN
      Runtime: "python3.8"
      Handler: index.lambda_handler
      Timeout: "300"
      Environment:
        Variables: 
          BucketName : !Ref BucketName
          RoleName: !Ref RoleName
          AccountNumber: !Ref AccountNumber
      Code:
        ZipFile: |
          import cfnresponse
          import sys
          import os
          import boto3
          import time
          from boto3.session import Session

          def assume_role(account_number, role_name):
              sts_client = boto3.client('sts')
              my_info = sts_client.get_caller_identity()
              partition = my_info['Arn'].split(":")[1]
              role_arn = 'arn:{}:iam::{}:role/{}'.format(partition, account_number, role_name)
              response = sts_client.assume_role(
                      RoleArn=role_arn,
                      RoleSessionName='Session-CFn-User',
              )
              session = boto3.Session(
                      aws_access_key_id=response['Credentials']['AccessKeyId'],
                      aws_secret_access_key=response['Credentials']['SecretAccessKey'],
                      aws_session_token=response['Credentials']['SessionToken']
              )
              return session

          def create_bucket(session, bucket_name):
              s3_client = session.client("s3")
              s3_client.create_bucket(Bucket=bucket_name, CreateBucketConfiguration={'LocationConstraint': 'ap-northeast-1'})

          def delete_bucket(session, bucket_name):
              s3_client = session.client("s3")
              s3_client.delete_bucket(Bucket=bucket_name, CreateBucketConfiguration={'LocationConstraint': 'ap-northeast-1'})



          def lambda_handler(event, context):
              if 'RequestType' not in event:
                return "Overwrite"
              
              if event['RequestType'] == 'Create':
                  bucket_name = os.getenv('BucketName')

                  account_number = os.getenv('AccountNumber')
                  role_name = os.getenv('RoleName')
                  session = assume_role(account_number, role_name)

                  create_bucket(session, bucket_name)

                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                      {'Response': 'Success'})
              if event['RequestType'] == 'Delete':
                  bucket_name = os.getenv('BucketName')

                  account_number = os.getenv('AccountNumber')
                  role_name = os.getenv('RoleName')
                  session = assume_role(account_number, role_name)

                  delete_bucket(session, bucket_name)
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                      {'Response': 'Success'})

              if event['RequestType'] == 'Update':
                  print('Update')
                  cfnresponse.send(event, context, cfnresponse.SUCCESS,
                                      {'Response': 'Success'})

CloudFormationの実行・スタック作成

まずは、以下のようにカスタムリソースのLambdaのロール作成の CloudFormation を AWSアカウントA で行います。

続いて、AWSアカウントBでAWSアカウントAへ権限委任するロール作成の CloudFormation を実行します。

そして、最後に AWSアカウントA でCloudFormationを実行し、カスタムリソースのLambdaからS3を作成を行います。

スタック作成後、アカウントBで作成したS3バケットが確認できます。

S3バケットの削除も確認するため、スタックの削除を行います。

スタック削除後、アカウントBからS3バケットが削除されていることが確認できます。

最後に

今回は、CloudFormationのカスタムリソースで別のアカウントのリソース(S3バケット)の操作を行いました。

1つ2つくらいのアカウントでのスタック作成であれば、スイッチロールをしての操作とかでも良いですが、それ以上のアカウントでスタックを作成する場合にはロールの切り替えは少々面倒です。

今回のように事前にロールを作成しておき、カスタムリソースでAssumeRoleするとロール切り替えがなくなるので、作業の簡略化ができそうです。