#author("2019-03-18T23:49:18+00:00","","")
#mynavi(AWSメモ)
#setlinebreak(on);

* 目次 [#d56f3755]
#contents
- 関連
-- [[AWS CloudFormationメモ]]
-- [[CloudFormationでカスタムドメイン対応の API Gateway を作成する]]
- 参考
-- https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md

* Lambda単体(AWS::Serverless 変換を利用しない場合) [#ve0b49e0]
* template.yml [#xcc16348]
#html(<div style="padding-left: 10px;">)

参考
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

** template.yml [#j0ed220b]
** Lambda単体 [#nd9938d3]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Sample Lambda

Parameters:
  BucketName:
    Type: "String"
    #Default: !Sub 'my-cloudformation-templates-${AWS::AccountId}'  # 文字列以外は指定できない
  BucketKey:
    Type: "String"
    #Default: !Sub "${AWS::StackName}.zip"  # 文字列以外は指定できない
AWS::Lambda::Function リソースを使用して定義した場合、&color(red){コード変更だけ行った場合に aws cloudformation deploy}; では更新されない(※)ので、素直に AWS::Serverless変換 を使用した方が良さそう。
※「No updates are to be performed」と怒られる。( cloudformation update-stack でも同様 )
※ aws lambda update-function-code であれば多分大丈夫。

Resources:
  SampleLambda:
    Type: "AWS::Lambda::Function"
    Properties:
      FunctionName: "CloudFormationLambda1"
      Description: "CloudFormationで作成したLambda1"
      Runtime: python3.6
      MemorySize: 128
      Timeout: 60
      Handler: index.handler
      Code:
          S3Bucket: !Ref BucketName
          S3Key: !Ref BucketKey
          # ZipFile: コードを直接記述する事もできる(ただし最大4096文字)
      Role: !GetAtt SampleLambdaRole.Arn
  SampleLambdaRole:
    Type: AWS::IAM::Role
    Properties:
      AssumeRolePolicyDocument:
        Version: '2012-10-17'
        Statement:
          - Effect: Allow
            Principal:
              Service:
                - lambda.amazonaws.com
            Action:
              - sts:AssumeRole
      Path: /
      Policies:
        - PolicyName: SampleLambdaPolicy
          PolicyDocument:
            Version: '2012-10-17'
            Statement:
              - Effect: Allow
                Action:
                  # 必要な権限を記載
                  - logs:CreateLogGroup
                  - logs:CreateLogStream
                  - logs:PutLogEvents
                Resource: '*' 
}}
#html(</div>)

** デプロイ用シェル [#i0779316]
#html(<div style="padding-left: 10px;">)

build.sh
#mycode2(){{
#!/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にアップロード
rm -rf src.zip
cd src && zip ../src.zip * && cd ../ 
aws s3api put-object --bucket $BUCKET_NAME --key ${STACK_NAME}.zip --body src.zip

# 検証&パッケージング&デプロイ
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 --parameter-overrides BucketName=${BUCKET_NAME} BucketKey=${STACK_NAME}.zip \
  &&     --stack-name $STACK_NAME --capabilities CAPABILITY_IAM CAPABILITY_NAMED_IAM \
  && echo ""
}}
#html(</div>)

** デプロイと動作確認 [#bf71a038]
#html(<div style="padding-left: 10px;">)
#myterm2(){{
# デプロイ
./build.sh

# テスト実行
aws lambda invoke --function-name CloudFormationLambda1 result.json && cat result.json && rm -rf result.json && echo ""
}}
#html(</div>)

#html(</div>)


* Lambda単体(AWS::Serverless 変換を利用する場合) [#d39a32a9]
#html(<div style="padding-left: 10px;">)

参考
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

** template.yml [#uc7e3720]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: Sample Lambda

Resources:
  CloudFormationLambda2:
    # https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md#awsserverlessfunction
    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: "*"
}}
#html(</div>)

** デプロイ用シェル [#x58d7da5]
** Lambda & API Gateway (AWS::Serverless 変換を利用する場合) [#ddf93482]
#html(<div style="padding-left: 10px;">)

ソースコードのzip圧縮とS3アップロードを自前で行わなくて良いこと以外は、Serverless変換を使用しない場合とほぼ同じ。

build.sh
#mycode2(){{
#!/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

# 検証&パッケージング&デプロイ
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_NAMED_IAM \
  && echo ""
}}
#html(</div>)

** デプロイと動作確認 [#bf71a038]
#html(<div style="padding-left: 10px;">)
#myterm2(){{
# デプロイ
./build.sh

# テスト実行
aws lambda invoke --function-name CloudFormationLambda2 result.json && cat result.json && rm -rf result.json && echo ""
}}
#html(</div>)

#html(</div>)

* API Gateway & Lambda (AWS::Serverless 変換を利用しない場合) [#j5ee9a92]
#html(<div style="padding-left: 10px;">)

** template.yml [#a5b9b44f]
#html(<div style="padding-left: 10px;">)
#TODO
#mycode2(){{
AWSTemplateFormatVersion : '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: "Sample API Gateway, Lambda"
XXXX
}}
#html(</div>)

** デプロイ用シェル [#b854552c]
#html(<div style="padding-left: 10px;">)
[[Lambda単体のAWS::Serverless 変換を利用しない場合>#i0779316]] と同じ。
#html(</div>)

#html(</div>)

* API Gateway & Lambda (AWS::Serverless 変換を利用する場合) [#ddf93482]
#html(<div style="padding-left: 10px;">)

AWS::Serverless 変換を使用しない場合と比べるとかなりシンプルに書ける。

** template.yml [#be09e89a]
#html(<div style="padding-left: 10px;">)
#mycode2(){{
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 を作成する]])
#html(</div>)

** デプロイ用シェル [#b854552c]
** Lambda & API Gateway (AWS::Serverless 変換を利用しない場合) [#j5ee9a92]
#html(<div style="padding-left: 10px;">)
[[Lambda単体のAWS::Serverless 変換を利用する場合>#x58d7da5]] と同じ。

上記にも記載したが、AWS::Lambda::Function リソースを使用して定義した場合、&color(red){コード変更だけ行った場合に cloudformation deploy}; では更新されない為、
Lambda自体の定義は AWS::Serverless::Function を使用し、API部分のみ AWS::Serverless変換をしない形で書いてみた。

#mycode2(){{
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

  # デプロイ対象ステージの作成
  # (カスタムドメインやステージ変数を使用しない場合は、定義しなくてもOK)
  #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}/"
}}

#html(</div>)

#html(</div>)

* デプロイ用シェル [#x58d7da5]
#html(<div style="padding-left: 10px;">)

デプロイ処理をシェルやMakefileにしておくと、開発、運用ともに楽。

[[CloudFormation実行用のシェル]] を参照
#html(</div>)

* デプロイと動作確認 [#bf71a038]
#html(<div style="padding-left: 10px;">)
#myterm2(){{
# デプロイ
./build.sh

# スタック状況確認
./build.sh events

# アンデプロイ
./build.sh delete
}}
#html(</div>)


トップ   一覧 単語検索 最終更新   ヘルプ   最終更新のRSS