MENU

簡易ファイル共有サイトを CloudFormation で実装してみました(Lambda 編)

簡易ファイル共有サイトを CloudFormation で実装してみました(Lambda 編)

お世話になります、株式会社エイトハンドレッド・テクノロジー本部の細江と申します。

数年前から PPAP 対策が提唱されるようになり、専用の有料サービスなども提供され始めていますが、皆さまはどうされていますでしょうか?
「AWS サービスを組み合わせて簡易的なソリューションを構築できたら便利だなぁ」と考えていたところ、下記 AWS ブログを見つけました。

AWS のサーバーレスと Amazon S3 署名付き URL、クライアントサイド JavaScript で大きなサイズの複数ファイルの一括アップロード・ダウンロード機能を実現する方法

今回は、こちらの AWS ブログのソリューションを CloudFormation で実装する YAML テンプレートを、本投稿を含む合計3つの記事(Cognito, S3 編, API Gateway 編)でご紹介します。


構成図

ソリューションの概要につきましては 当該ブログ記事 をご確認ください。

アーキテクチャ図

アーキテクチャ図 各サービスプロセス詳細


YAML テンプレート

【参考】 AWS CloudFormation テンプレートの構造分析 > YAML

450行以上あるため、アコーディオンブロックにしています。

展開してご確認ください

AWSTemplateFormatVersion: 2010-09-09

Parameters:
  SharedName:
    Type: String
    Default: hatena-file-sharing

Resources:
  UserPool:
    Type: AWS::Cognito::UserPool
    Properties:
      UserPoolName: !Sub ${SharedName}-userpool
      UsernameAttributes:
      - email
      AccountRecoverySetting:
        RecoveryMechanisms:
        - Name: verified_email
          Priority: 1
      AdminCreateUserConfig:
        AllowAdminCreateUserOnly: true
      AutoVerifiedAttributes:
      - email
      UserAttributeUpdateSettings:
        AttributesRequireVerificationBeforeUpdate:
        - email
      Schema:
      - Name: email
        Required: true
        Mutable: false

  UserPoolDomain:
    Type: AWS::Cognito::UserPoolDomain
    Properties:
      Domain: !Ref SharedName
      UserPoolId: !Ref UserPool

  ResourceServer:
    Type: AWS::Cognito::UserPoolResourceServer
    Properties:
      Identifier: !Sub ${SharedName}-rs-id
      Name: !Sub ${SharedName}-rs
      UserPoolId: !Ref UserPool

  AppClient:  
    Type: AWS::Cognito::UserPoolClient
    Properties:
      ClientName: !Sub ${SharedName}-appclient
      UserPoolId: !Ref UserPool
      AllowedOAuthFlowsUserPoolClient: true
      CallbackURLs:
      - !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/web/download.html'
      - !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/web/entrance.html'
      - !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/web/upload.html'
      LogoutURLs:
      - !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/prod/web/entrance.html'
      SupportedIdentityProviders:
      - COGNITO
      AllowedOAuthFlows:
      - code
      AllowedOAuthScopes:
      - openid

  AppClientHostedUI:
    Type: AWS::Cognito::UserPoolUICustomizationAttachment
    Properties:
      ClientId: !Ref AppClient
      UserPoolId: !Ref UserPool

  S3Bucket:
    Type: AWS::S3::Bucket
    DeletionPolicy: Delete
    Properties:
      BucketName: !Ref SharedName
      AccessControl: Private 
      PublicAccessBlockConfiguration:
        BlockPublicAcls: true
        BlockPublicPolicy: true
        IgnorePublicAcls: true
        RestrictPublicBuckets: true
      WebsiteConfiguration:
        IndexDocument: index.html
      LifecycleConfiguration:
        Rules:
        - Id: DeleteDownloads
          Status: Enabled
          Prefix: "download/"
          ExpirationInDays: 7
        - Id: DeleteUploads
          Status: Enabled
          Prefix: "upload/"
          ExpirationInDays: 7
        - Id: AbortIncompleteMultipartUpload
          Status: Enabled
          AbortIncompleteMultipartUpload:
            DaysAfterInitiation: 1
      CorsConfiguration:
        CorsRules:
        - AllowedHeaders:
          - '*'
          AllowedMethods:
          - GET
          - POST
          AllowedOrigins:
          - !Sub 'https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com'
          MaxAge: 3000

  S3BucketPolicy:
    Type: AWS::S3::BucketPolicy
    Properties:
      Bucket: !Ref S3Bucket
      PolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Action:
          - s3:GetObject
          Resource: !Sub '${S3Bucket.Arn}/*'
          Principal:
            Service: 'apigateway.amazonaws.com'
          Condition:
            ArnLike:
              'aws:SourceArn': !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/'

  LambdaS3Download:
    Type: AWS::Lambda::Function
    Properties: 
      FunctionName: !Sub ${SharedName}-s3download-function
      Role: !Sub ${LambdaFunctionRole.Arn}
      Runtime: python3.10
      Handler: index.handler
      Environment:
        Variables:
          DURATION_SECONDS: 3600
          S3_BUCKET_NAME: !Ref SharedName
          S3_PREFIX_NAME: 'download/'
      Code:
        ZipFile: |
          import json
          import datetime
          import botocore
          import boto3
          import os

          S3_BUCKET_NAME = os.environ["S3_BUCKET_NAME"]
          S3_PREFIX_NAME = os.environ["S3_PREFIX_NAME"]
          DURATION_SECONDS = int(os.environ["DURATION_SECONDS"])

          # S3クライアント
          s3_client = boto3.client("s3")

          def handler(event, context):
              
              # 戻り値の初期化
              return_obj = dict()
              return_obj["body"] = dict()
              
              # バケット名の設定
              return_obj["body"]["bucket"] = S3_BUCKET_NAME
              # フォルダー名の設定
              return_obj["body"]["prefix"] = S3_PREFIX_NAME
              # ファイル (オブジェクト) 一覧の初期化
              return_obj["body"]["contents"] = []
              
              # ファイル一覧情報の取得
              response = s3_client.list_objects_v2(Bucket=S3_BUCKET_NAME, Prefix=S3_PREFIX_NAME)
              
              for content in response["Contents"]:
              
                  # ファイル情報の初期化
                  object = dict()
                  
                  # ファイルサイズの取得
                  size = content["Size"]
                  if(size == 0):
                    # ファイルサイズが 0 の場合、その後の処理をスキップ
                    continue
                  
                  # ファイル名の取得と戻り値への設定
                  key = content["Key"]
                  object["name"] = key.replace(S3_PREFIX_NAME, "").replace("/", "")
                  
                  # ファイルサイズの戻り値への設定
                  object["size"] = "{:,} Bytes".format(size)
                  
                  # ファイル更新日時の取得と戻り値への設定
                  # 日本のタイムゾーン (JST)
                  tz_jst = datetime.timezone(datetime.timedelta(hours=9))
                  # 取得日時をJSTに変換
                  dt_jst = content['LastModified'].astimezone(tz_jst)
                  object["lastModified"] = dt_jst.strftime('%Y/%m/%d %H:%M:%S')
                  
                  # 署名付き URL の取得と戻り値への設定
                  object["presignedUrl"] = s3_client.generate_presigned_url(
                      ClientMethod = "get_object",
                      Params = {"Bucket" : S3_BUCKET_NAME, "Key" : key},
                      ExpiresIn = DURATION_SECONDS,
                      HttpMethod = "GET"
                  )

                  # 取得した各情報の戻り値への設定
                  return_obj["body"]["contents"].append(object)
            
              return_obj["statusCode"] = 200
              return_obj["body"] = json.dumps(return_obj["body"])
              
              return return_obj

  LambdaS3DownloadPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaS3Download
      Action: 'lambda:InvokeFunction'
      Principal: 'apigateway.amazonaws.com'
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/api/download'

  LambdaS3Upload:
    Type: AWS::Lambda::Function
    Properties: 
      FunctionName: !Sub ${SharedName}-s3upload-function
      Role: !Sub ${LambdaFunctionRole.Arn}
      Runtime: python3.10
      Handler: index.handler
      Environment:
        Variables:
          DURATION_SECONDS: 3600
          S3_BUCKET_NAME: !Ref SharedName
          S3_PREFIX_NAME: 'upload/'
      Code:
        ZipFile: |
          import json
          import boto3
          from botocore.client import Config

          import os

          S3_BUCKET_NAME = os.environ["S3_BUCKET_NAME"]
          S3_PREFIX_NAME = os.environ["S3_PREFIX_NAME"]
          DURATION_SECONDS = int(os.environ["DURATION_SECONDS"])

          s3_client = boto3.client("s3", config=Config(signature_version="s3v4")) 

          def handler(event, context):

              # 戻り値の初期化
              return_obj = dict()
              return_obj["body"] = dict()
              
              # バケット名の設定
              return_obj["body"]["bucket"] = S3_BUCKET_NAME
              # フォルダー名の設定
              return_obj["body"]["prefix"] = S3_PREFIX_NAME

              target_info = s3_client.generate_presigned_post(S3_BUCKET_NAME,
                                                              S3_PREFIX_NAME + "${filename}", 
                                                              Fields=None,
                                                              Conditions=None,
                                                              ExpiresIn=DURATION_SECONDS)

              # 取得した各情報の戻り値への設定
              return_obj["body"]["contents"] = target_info

              return_obj["statusCode"] = 200
              return_obj["body"] = json.dumps(return_obj["body"])

              return return_obj

  LambdaS3UploadPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaS3Upload
      Action: 'lambda:InvokeFunction'
      Principal: 'apigateway.amazonaws.com'
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/api/upload'

  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SharedName}-lambda-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Policies:
      - PolicyName: !Sub ${SharedName}-lambda-policy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
          - Effect: Allow
            Action:
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
          - Effect: Allow
            Action:
            - s3:ListBucket
            Resource: !Sub ${S3Bucket.Arn}
          - Effect: Allow
            Action:
            - s3:GetObject
            - s3:PutObject
            Resource: !Sub '${S3Bucket.Arn}/*'

  RestApi:
    Type: AWS::ApiGateway::RestApi
    Properties:
      Name: !Sub ${SharedName}-apigw

  ApiResourceApi:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApi
      ParentId: !Sub ${RestApi.RootResourceId}
      PathPart: 'api'

  ApiResourceApiDownload:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApi
      ParentId: !Ref ApiResourceApi
      PathPart: 'download'

  ApiResourceApiDownloadMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApi
      ResourceId: !Ref ApiResourceApiDownload
      HttpMethod: GET
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref ApiResourceApiAuthorizer
      MethodResponses:
      - StatusCode: 200
        ResponseModels:
          application/json: Empty
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaS3Download.Arn}/invocations'

  ApiResourceApiUpload:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApi
      ParentId: !Ref ApiResourceApi
      PathPart: 'upload'

  ApiResourceApiUploadMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApi
      ResourceId: !Ref ApiResourceApiUpload
      HttpMethod: GET
      AuthorizationType: COGNITO_USER_POOLS
      AuthorizerId: !Ref ApiResourceApiAuthorizer
      MethodResponses:
      - StatusCode: 200
        ResponseModels:
          application/json: Empty
      Integration:
        IntegrationHttpMethod: POST
        Type: AWS_PROXY
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${LambdaS3Upload.Arn}/invocations'

  ApiResourceApiAuthorizer:
    Type: AWS::ApiGateway::Authorizer
    Properties:
      Name: !Sub ${SharedName}-apigw-authorizer
      RestApiId: !Ref RestApi
      Type: COGNITO_USER_POOLS
      IdentitySource: method.request.header.Authorization
      ProviderARNs:
      - !Sub 'arn:aws:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}'

  ApiResourceWeb:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApi
      ParentId: !Sub ${RestApi.RootResourceId}
      PathPart: 'web'

  ApiResourceWebProxy:
    Type: AWS::ApiGateway::Resource
    Properties:
      RestApiId: !Ref RestApi
      ParentId: !Ref ApiResourceWeb
      PathPart: '{proxy+}'

  ApiResourceWebMethod:
    Type: AWS::ApiGateway::Method
    Properties:
      RestApiId: !Ref RestApi
      ResourceId: !Ref ApiResourceWebProxy
      HttpMethod: GET
      AuthorizationType: NONE
      RequestParameters:
        method.request.path.proxy: true
      MethodResponses:
      - StatusCode: 200
        ResponseParameters:
          method.response.header.Content-Length: true
          method.response.header.Content-Type: true
          method.response.header.Timestamp: true
        ResponseModels:
          application/json: Empty
      - StatusCode: 400
      - StatusCode: 500
      Integration:
        IntegrationHttpMethod: GET
        Type: AWS
        Uri: !Sub 'arn:aws:apigateway:${AWS::Region}:s3:path/${SharedName}/contents/{proxy}'
        Credentials: !Sub ${ApiResourceWebRole.Arn}
        CacheKeyParameters:
        - 'method.request.path.proxy'
        RequestParameters:
          integration.request.path.proxy: 'method.request.path.proxy'
        IntegrationResponses:
        - StatusCode: 200
          ResponseParameters:
            method.response.header.Content-Length: 'integration.response.header.Content-Length'
            method.response.header.Content-Type: 'integration.response.header.Content-Type'
            method.response.header.Timestamp: 'integration.response.header.Date'
          ResponseTemplates:
            application/json: ''
        - StatusCode: 400
          SelectionPattern: '4\d{2}'
        - StatusCode: 500
          SelectionPattern: '5\d{2}'

  ApiResourceWebRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SharedName}-apigw-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - apigateway.amazonaws.com
          Action:
          - sts:AssumeRole
      ManagedPolicyArns:
      - 'arn:aws:iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs'
      Policies:
      - PolicyName: !Sub ${SharedName}-apigw-policy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
            - s3:ListBucket
            Resource: !Sub ${S3Bucket.Arn}
          - Effect: Allow
            Action:
            - s3:GetObject
            - s3:PutObject
            Resource: !Sub '${S3Bucket.Arn}/*'

  ApiResourceDeployment:
    Type: AWS::ApiGateway::Deployment
    DependsOn:
    - ApiResourceApiDownloadMethod
    - ApiResourceApiUploadMethod
    - ApiResourceWebMethod
    Properties:
      RestApiId: !Ref RestApi
      StageName: 'prod'


Parameters

【参考】 AWS CloudFormation パラメータ

Parameters:
  SharedName:
    Type: String
    Default: hatena-file-sharing

各リソースに使用する共通の名称として SharedName というパラメータを宣言。デフォルトは hatena-file-sharing としています。


Resources

本記事では Lambda についてご説明しています。Cognito, S3API Gateway リソースについては、それぞれリンク先の記事をご確認ください。

LambdaS3Download

  LambdaS3Download:
    Type: AWS::Lambda::Function
    Properties: 
      FunctionName: !Sub ${SharedName}-s3download-function
      Role: !Sub ${LambdaFunctionRole.Arn}
      Runtime: python3.10
      Handler: index.handler
      Environment:
        Variables:
          DURATION_SECONDS: 3600
          S3_BUCKET_NAME: !Ref SharedName
          S3_PREFIX_NAME: 'download/'
      Code:
        ZipFile: |

S3 ファイルダウンロード用の署名付き URL を生成する Lambda 関数を作成しています。 ZipFile: | 以下の Python コードは省略してあるので、確認されたい場合は以下を展開してください。

ダウンロード用 Lambda - Python コード

import json
import datetime
import botocore
import boto3
import os

S3_BUCKET_NAME = os.environ["S3_BUCKET_NAME"]
S3_PREFIX_NAME = os.environ["S3_PREFIX_NAME"]
DURATION_SECONDS = int(os.environ["DURATION_SECONDS"])

# S3クライアント
s3_client = boto3.client("s3")

def handler(event, context):
    
    # 戻り値の初期化
    return_obj = dict()
    return_obj["body"] = dict()
    
    # バケット名の設定
    return_obj["body"]["bucket"] = S3_BUCKET_NAME
    # フォルダー名の設定
    return_obj["body"]["prefix"] = S3_PREFIX_NAME
    # ファイル (オブジェクト) 一覧の初期化
    return_obj["body"]["contents"] = []
    
    # ファイル一覧情報の取得
    response = s3_client.list_objects_v2(Bucket=S3_BUCKET_NAME, Prefix=S3_PREFIX_NAME)
    
    for content in response["Contents"]:
    
        # ファイル情報の初期化
        object = dict()
        
        # ファイルサイズの取得
        size = content["Size"]
        if(size == 0):
          # ファイルサイズが 0 の場合、その後の処理をスキップ
          continue
        
        # ファイル名の取得と戻り値への設定
        key = content["Key"]
        object["name"] = key.replace(S3_PREFIX_NAME, "").replace("/", "")
        
        # ファイルサイズの戻り値への設定
        object["size"] = "{:,} Bytes".format(size)
        
        # ファイル更新日時の取得と戻り値への設定
        # 日本のタイムゾーン (JST)
        tz_jst = datetime.timezone(datetime.timedelta(hours=9))
        # 取得日時をJSTに変換
        dt_jst = content['LastModified'].astimezone(tz_jst)
        object["lastModified"] = dt_jst.strftime('%Y/%m/%d %H:%M:%S')
        
        # 署名付き URL の取得と戻り値への設定
        object["presignedUrl"] = s3_client.generate_presigned_url(
            ClientMethod = "get_object",
            Params = {"Bucket" : S3_BUCKET_NAME, "Key" : key},
            ExpiresIn = DURATION_SECONDS,
            HttpMethod = "GET"
        )

        # 取得した各情報の戻り値への設定
        return_obj["body"]["contents"].append(object)
  
    return_obj["statusCode"] = 200
    return_obj["body"] = json.dumps(return_obj["body"])
    
    return return_obj

今回は 既存の Python コード の流用なので、そのまま貼り付けていますが、ご自身でコーディングされる場合、この長さのソースコードを YAML テンプレート内に直接記述するのは、あまりオススメしません。

FunctionName

関数名。 !Sub によって SharedName パラメータを挿入しています。

【参考】 AWS CloudFormation > Fn::Sub

Role

関数を実行する IAM ロール。同じく !Sub によって、本テンプレートで作成している LambdaFunctionRole の ARN を指定しています。

Runtime

参照先の AWS ブログ に従って、ランタイムは python3.10 にしています。

Handler

ソースコードをインラインで記述する場合、ファイル名が index になるため、それに合わせてハンドラも index.handler としています1

Environment > Variables

環境変数の設定。S3 バケット名は !Ref によって SharedName パラメータを挿入し、その他は参照先の AWS ブログ と同じです。

【参考】 AWS CloudFormation > Ref

Code > ZipFile

Lambda 関数のソースコード。末尾のバーティカルバー | は、それ以降の複数行(=ソースコード)がひとつの ZIP ファイルになることを示しています。

LambdaS3DownloadPermission

  LambdaS3DownloadPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaS3Download
      Action: 'lambda:InvokeFunction'
      Principal: 'apigateway.amazonaws.com'
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/api/download'

API Gateway サービスに対して Lambda 関数の実行を許可しています。

FunctionName

!Ref によって、上記作成している LambdaS3Download を指定しています。

Action

API Gateway サービスに対して許可する操作。Lambda 関数の実行を許可しています。

Principal

上記 Action 操作を許可する AWS サービス。API Gateway を指定しています。

SourceArn

上記 Principal の ARN。 !Sub によって、本テンプレートで作成している RestApi の ARN を作成しています。

【参考】
AWS CloudFormation > 擬似パラメータ参照 > AWS::Region
AWS CloudFormation > 擬似パラメータ参照 > AWS::AccountId

LambdaS3Upload

  LambdaS3Upload:
    Type: AWS::Lambda::Function
    Properties: 
      FunctionName: !Sub ${SharedName}-s3upload-function
      Role: !Sub ${LambdaFunctionRole.Arn}
      Runtime: python3.10
      Handler: index.handler
      Environment:
        Variables:
          DURATION_SECONDS: 3600
          S3_BUCKET_NAME: !Ref SharedName
          S3_PREFIX_NAME: 'upload/'
      Code:
        ZipFile: |

S3 ファイルアップロード用の署名付き URL を生成する Lambda 関数を作成しています。 ZipFile: | 以下の Python コードは省略してあるので、確認されたい場合は以下を展開してください。

アップロード用 Lambda - Python コード

import json
import boto3
from botocore.client import Config

import os

S3_BUCKET_NAME = os.environ["S3_BUCKET_NAME"]
S3_PREFIX_NAME = os.environ["S3_PREFIX_NAME"]
DURATION_SECONDS = int(os.environ["DURATION_SECONDS"])

s3_client = boto3.client("s3", config=Config(signature_version="s3v4")) 

def handler(event, context):

    # 戻り値の初期化
    return_obj = dict()
    return_obj["body"] = dict()
    
    # バケット名の設定
    return_obj["body"]["bucket"] = S3_BUCKET_NAME
    # フォルダー名の設定
    return_obj["body"]["prefix"] = S3_PREFIX_NAME

    target_info = s3_client.generate_presigned_post(S3_BUCKET_NAME,
                                                    S3_PREFIX_NAME + "${filename}", 
                                                    Fields=None,
                                                    Conditions=None,
                                                    ExpiresIn=DURATION_SECONDS)

    # 取得した各情報の戻り値への設定
    return_obj["body"]["contents"] = target_info

    return_obj["statusCode"] = 200
    return_obj["body"] = json.dumps(return_obj["body"])

    return return_obj

LambdaS3UploadPermission

  LambdaS3UploadPermission:
    Type: AWS::Lambda::Permission
    Properties:
      FunctionName: !Ref LambdaS3Upload
      Action: 'lambda:InvokeFunction'
      Principal: 'apigateway.amazonaws.com'
      SourceArn: !Sub 'arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${RestApi}/*/GET/api/upload'

本テンプレートで作成している RestApi に対して、上記作成している LambdaS3Upload の実行を許可しています。

LambdaFunctionRole

  LambdaFunctionRole:
    Type: AWS::IAM::Role
    Properties:
      RoleName: !Sub ${SharedName}-lambda-role
      AssumeRolePolicyDocument:
        Version: 2012-10-17
        Statement:
        - Effect: Allow
          Principal:
            Service:
            - lambda.amazonaws.com
          Action:
          - sts:AssumeRole
      Policies:
      - PolicyName: !Sub ${SharedName}-lambda-policy
        PolicyDocument:
          Version: 2012-10-17
          Statement:
          - Effect: Allow
            Action:
            - logs:CreateLogGroup
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:*'
          - Effect: Allow
            Action:
            - logs:CreateLogStream
            - logs:PutLogEvents
            Resource: !Sub 'arn:aws:logs:${AWS::Region}:${AWS::AccountId}:log-group:/aws/lambda/*'
          - Effect: Allow
            Action:
            - s3:ListBucket
            Resource: !Sub ${S3Bucket.Arn}
          - Effect: Allow
            Action:
            - s3:GetObject
            - s3:PutObject
            Resource: !Sub '${S3Bucket.Arn}/*'
RoleName

IAM ロール名。 !Sub によって SharedName パラメータを挿入しています。

AssumeRolePolicyDocument

信頼関係(信頼されたエンティティ)の編集。Lambda サービス lambda.amazonaws.com から本 IAM ロールを引き受けられるアクション sts:AssumeRole を記述しています。

Policies

インラインポリシーの作成。参照先の AWS ブログ で IAM ポリシーサンプルとして JSON 表記されているものをそのまま YAML で表現しています。


本投稿でご紹介する内容は以上になります。続きは下記の記事をご確認ください。

簡易ファイル共有サイトを CloudFormation で実装してみました(API Gateway 編)


  1. インラインで記述している Python コードの関数名も参照先 AWS ブログの lambda_handler から handler に変更しています。