AWS::Lambda::Function リソースを使用して定義した場合、コード変更だけ行った場合に aws cloudformation deploy では更新されない(※)ので、素直に AWS::Serverless変換 を使用した方が良さそう。
※「No updates are to be performed」と怒られる。( cloudformation update-stack でも同様 )
※ aws lambda update-function-code であれば多分大丈夫。
参考
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/transform-aws-serverless.html
https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
AWSTemplateFormatVersion : '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Sample Lambda Resources: CloudFormationLambda2: Type: "AWS::Serverless::Function" Properties: FunctionName: CloudFormationLambda2 Description: "CloudFormationで作成したLambda2(AWS::Serverless変換)" Runtime: python3.6 Handler: index.handler # CodeUri に指定したパス配下がアップロードされる # ※S3のPATHを指定する事も可能。(自分でS3にアップロードする必要あり) # ※省略した場合はカレント配下のファイルが全てアップロード対象となる。 CodeUri: ./src MemorySize: 128 Timeout: 60 Role: !GetAtt CloudFormationLambda2Role.Arn CloudFormationLambda2Role: Type: "AWS::IAM::Role" Properties: RoleName: CloudFormationLambda2Role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: "lambda.amazonaws.com" Action: "sts:AssumeRole" Policies: - PolicyName: "CloudFormationLambda2Policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*"
AWS::Serverless 変換を使用しない場合と比べるとかなりシンプルに書ける。
AWSTemplateFormatVersion : '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Test serverless application. Resources: SampleApi: Type: AWS::Serverless::Function Properties: FunctionName: CloudFormationApiLambda2 Description: "CloudFormationApiLambda2" Handler: index.handler Runtime: python3.6 CodeUri: ./src Events: ListApi: Type: Api Properties: Path: / Method: get GetApi: Type: Api Properties: Path: /{id} Method: get Role: !GetAtt CloudFormationApiLambda2Role.Arn CloudFormationApiLambda2Role: Type: "AWS::IAM::Role" Properties: RoleName: CloudFormationApiLambda2Role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: "lambda.amazonaws.com" Action: "sts:AssumeRole" Policies: - PolicyName: "CloudFormationApiLambda2Policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" Outputs: SampleBookApiUri: # 作成したAPIのURIをエクスポートしておく(AWS::Serverless::Function を使用した場合、ステージは Stage,Prod の2つが作成される) # ※AWS::Serverless変換を使用した場合の RestApi は "ServerlessRestApi" という論理名で定義されるので、これを使用してAPIのIDを取得する事が可能。 Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/" Export: Name: CloudFormationApiLambda2Uri
※実際の運用ではカスタムドメインを使用してベースパスマッピングを設定する事になるが、ここでは割愛する。(関連: CloudFormationでカスタムドメイン対応の API Gateway を作成する)
上記にも記載したが、AWS::Lambda::Function リソースを使用して定義した場合、コード変更だけ行った場合に cloudformation deploy では更新されない為、
Lambda自体の定義は AWS::Serverless::Function を使用し、API部分のみ AWS::Serverless変換をしない形で書いてみた。
AWSTemplateFormatVersion : '2010-09-09' Transform: AWS::Serverless-2016-10-31 Description: Sample API Gateway template. Parameters: StageName: Type: "String" Default: "dev" Resources: # Lambda本体 SampleFunction1: Type: "AWS::Serverless::Function" Properties: FunctionName: SampleFunction1 Runtime: python3.6 Handler: index.handler CodeUri: ./src MemorySize: 128 Timeout: 60 Role: !GetAtt SampleFunction1Role.Arn # Lambdaのロール SampleFunction1Role: Type: "AWS::IAM::Role" Properties: RoleName: SampleFunction1Role AssumeRolePolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Principal: Service: "lambda.amazonaws.com" Action: "sts:AssumeRole" Policies: - PolicyName: "SampleFunction1Policy" PolicyDocument: Version: "2012-10-17" Statement: - Effect: "Allow" Action: - logs:CreateLogGroup - logs:CreateLogStream - logs:PutLogEvents Resource: "*" # RestApiの定義 # (AWS::ApiGateway::Resource と AWS::ApiGateway::Method に分けて書くこともできるが、 # ここでは Body プロパティに全て定義した。) SampleRestApi1: Type: 'AWS::ApiGateway::RestApi' Properties: Body: swagger: '2.0' info: version: '1.0' title: !Ref 'AWS::StackName' paths: /: get: # Lambdaプロキシ統合をONにする場合は x-amazon-apigateway-integration オブジェクトを使用して type: aws_proxy を指定する # (参考) https://docs.aws.amazon.com/ja_jp/apigateway/latest/developerguide/api-gateway-swagger-extensions-integration.html x-amazon-apigateway-integration: httpMethod: POST # 統合リクエストで使用されるHTTP メソッド。Lambdaの場合はPOSTを指定 type: aws_proxy # Lambdaプロキシ統合 uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SampleFunction1.Arn}/invocations" responses: {} '/{id}': get: x-amazon-apigateway-integration: httpMethod: POST type: aws_proxy uri: !Sub "arn:aws:apigateway:${AWS::Region}:lambda:path/2015-03-31/functions/${SampleFunction1.Arn}/invocations" responses: {} # API GatewayへのLambda実行許可 SampleRestApi1Permission: Type: 'AWS::Lambda::Permission' Properties: Action: 'lambda:invokeFunction' Principal: apigateway.amazonaws.com FunctionName: !Ref SampleFunction1 Principal: "apigateway.amazonaws.com" # SourceArn でURI毎に個別定義する事もできる #SourceArn: !Sub # - >- # arn:aws:execute-api:${AWS::Region}:${AWS::AccountId}:${__ApiId__}/${__Stage__}/GET/{id} # - __Stage__: Prod # __ApiId__: !Ref SampleRestApi1 # デプロイ対象ステージの作成 # (無くても通る。不要?) #SampleRestApi1ProdStage: # Type: 'AWS::ApiGateway::Stage' # Properties: # DeploymentId: !Ref SampleRestApi1Deploy # RestApiId: !Ref SampleRestApi1 # StageName: !Ref StageName # ステージへのデプロイ SampleRestApi1Deploy: Type: 'AWS::ApiGateway::Deployment' #DependsOn: "SampleApiMethod" Properties: RestApiId: !Ref SampleRestApi1 StageName: !Ref StageName Outputs: SampleRestApi1Uri: Export: Name: SampleRestApi1Uri Value: !Sub "https://${SampleRestApi1}.execute-api.${AWS::Region}.amazonaws.com/${StageName}/"
デプロイ処理をシェルやMakefileにしておくと、開発、運用ともに楽。
build.sh
#!/bin/bash MODE=$1 echo "" # テンプレートファイル TEMPLATE=template.yml # スタック名 cd `dirname $0` STACK_NAME=`basename \`pwd\`` if [ -e ".git" ]; then STACK_NAME=`cat .git/config | grep url | head -1 | awk -F"/" '{print $NF}'` fi STACK_NAME=`echo $STACK_NAME | sed 's/_/-/g' | sed 's/Repo$/Stack/'` echo "Stack: ${STACK_NAME}" # アカウントIDの取得 ACCOUNT_ID=`aws sts get-caller-identity | grep Account | awk '{print $2}' | sed -e "s/[^0-9]//g"` # スタック作成時のイベント確認 if [ "${MODE}" == "desc" ]; then aws cloudformation describe-stack-events --stack-name $STACK_NAME exit 0 fi # 削除 if [ "${MODE}" == "del" ]; then aws cloudformation delete-stack --stack-name $STACK_NAME aws cloudformation wait stack-delete-complete --stack-name $STACK_NAME exit 0 fi # S3バケットがない場合は作る(バケット名は世界で唯一である必要がある為、末尾にアカウントID等を付与しておく) BUCKET_NAME=my-cloudformation-templates-${ACCOUNT_ID} BUCKET_COUNT=`aws s3api list-buckets | grep -e "\"${BUCKET_NAME}\"" | wc -l` if [ "${BUCKET_COUNT}" == "0" ]; then echo aws s3api create-bucket --create-bucket-configuration '{"LocationConstraint": "ap-northeast-1"}' --bucket $BUCKET_NAME fi # ソースコードをzip圧縮しS3にアップロード # (Serverless変換を使用する場合、package 時に自動でソースのzip圧縮とS3アップロードを行ってくれる為、自分で行なう必要はない) #cd src && zip ../src.zip * && cd ../ #aws s3api put-object --bucket $BUCKET_NAME --key ${STACK_NAME}.zip --body src.zip #rm -rf src.zip # 検証&パッケージング&デプロイ(成功時は作成したAPIのURIを表示する) aws cloudformation validate-template --template-body file://${TEMPLATE} \ && aws cloudformation package --template-file $TEMPLATE --s3-bucket $BUCKET_NAME --output-template-file packaged-template.yml \ && aws cloudformation deploy --template-file packaged-template.yml --stack-name $STACK_NAME --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \ && echo "" \ && echo "### Exported Value ###" \ && aws cloudformation describe-stacks --stack-name $STACK_NAME \ | awk 'BEGIN{key=""}{ if ($1 == "\"OutputKey\":") key=$2; if ($1 == "\"OutputValue\":") print key" : "$2 }' \ | sed 's/[",]//g' \ && echo "######################/" \ && echo ""
# デプロイ ./build.sh # スタック状況確認 ./build.sh desc # アンデプロイ ./build.sh del