CloudFormationでAWS Lambdaを作成・更新する際のベストプラクティス

# 結論
まずは結論から

**Lambda関数は別ファイルとして保存し、`aws cloudformation package`して`aws cloudformation deploy`する**

# まえがき

Lambdaをcloudformationでリファレンスを見ながらとりあえず作ることはできる人向きの記事です。

CloudFormationからLambdaを作る際、そのソースコードの指定方法は2種類あります。
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html

- `ZipFile`
- `S3Bucket`と`S3Key`(と`S3ObjectVersion`)

いずれも`Properties`の`Code`に記述します。

## `ZipFile`
`ZipFile`で指定する際は利用するテンプレートファイルの中でソースコードを直接書くことになります。ちなみにPythonかNode.jsでしか作成できないです。また、PythonにおけるBeautiful Soupなど、外部モジュールをインポートすることもできません(一つのファイルとしてしかアップロードできないため)。
さらにテンプレートファイルに直接書くのでシンタックスハイライトがソースコードに反映されないため見にくい、テンプレートファイルもソースコードの分長くなる、ということでひと目でわかるような小さな関数を書くときのみにしかおすすめしないです。

[リファレンス](https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-properties-lambda-function-code.html)からの引用ですが、以下にyaml形式で記述したサンプルコード(一部抜粋)を載せておきます。いずれも受け取った値に5をかけるだけの関数です。

```yaml:Node.jsのZipFileにおけるサンプルコード
Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: >
          var response = require('cfn-response');
          exports.handler = function(event, context) {
            var input = parseInt(event.ResourceProperties.Input);
            var responseData = {Value: input * 5};
            response.send(event, context, response.SUCCESS, responseData);
          };
#------------------以下省略------------------#
```

```yaml:pythonのZipFileにおけるサンプルコード
Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        ZipFile: |
          import json
          import cfnresponse
          def handler(event, context):
            responseValue = int(event['ResourceProperties']['Input']) * 5
            responseData = {}
            responseData['Data'] = responseValue
            cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID")
#------------------以下省略------------------#
```

コードを記述する前に置く記号がNode.jsだと`>`でPythonでは`|`であること以外はコードをベタ書きするだけです。

## `S3Bucket`と`S3Key`

`S3Bucket`と`S3Key`で指定する場合にはzip形式であらかじめアップロードしておいたファイルのパスを指定します。
ソースコードが長くても、他のモジュールを読みこんでいても一つのS3オブジェクト内に圧縮しておけばこちらが利用できます。
以下にyaml形式で記述したサンプルコード(一部抜粋)を示します。

```yaml:S3BucketとS3Keyによるソースコード指定をしたサンプルコード
Code:
  S3bucket: YOUR_S3_BUCKET_NAME
  S3key: YOUR_S3_OBJECT_KEY
```

それぞれ値はString型で、S3keyは特にバケット内での目的のオブジェクトへのパスのことです。

# `S3Bucket`と`S3Key`による指定の際におこる問題

外部モジュールもアップロード時に含めることができ、テンプレートファイルの行数も短くできるので`S3Bucket`と`S3Key`を用いた更新方法をなるべく使いたいです。
ですが、**関数のコードにのみ**変更を加えてスタックを更新するときに問題が起きます。

関数のコードにのみ変更を加えた後、S3bucketに保存したソースコードのzipファイルをアップロードし直すことで更新します。
この後、作成に使ったテンプレートファイルを用いてスタックをアップデートしようとしても
「No updates are to be performed」と怒られます。
テンプレートファイル自体には変更を加えていないので、当たり前といえば当たり前なのですが。。。

# 回避方法: `aws cloudformation package`を使う。

`aws cloudformation package` ( https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html)

このコマンドはテンプレートファイル内で指定したローカルにあるディレクトリやファイルをS3にアップロードし、それに応じたS3のオブジェクトのパスを記入したテンプレートファイルを出力してくれるコマンドです。
これで作成されたテンプレートファイルを用いて`aws cloudformation deploy`でスタックを作成・更新するのがよいでしょう。
ただしローカルのファイルパスに関してなんでも解決してくれるわけではなく特定のリソースの特定のプロパティについてのみ有効です。

| リソース名                                  | プロパティ名                        |
|:------------------------------------------|:----------------------------------|
| AWS::ApiGateway::RestApi                  | BodyS3Location                    |
| AWS::Lambda::Function                     | Code                              |
| AWS::Serverless::Function                 | CodeUri                           |
| AWS::AppSync::GraphQLSchema               | DefinitionS3Location              |
| AWS::AppSync::Resolver                    | RequestMappingTemplateS3Location  |
| AWS::AppSync::Resolver                    | ResponseMappingTemplateS3Location |
| AWS::Serverless::Api                      | DefinitionUri                     |
| AWS::ElasticBeanstalk::ApplicationVersion | SourceBundle                      |
| AWS::CloudFormation::Stack                | TemplateURL                       |

また、`AWS::Include Transform`の`Location`パラメータにも使えます。

### サンプル


まず、lambda関数のファイルを置くためのS3バケット(`my-lambda-bucket`)をあらかじめ作成しておき、bashのコマンドは全てプロジェクトルートで実行します。
また、ディレクトリの構造が以下のようになっているとします。([こちら](https://dev.classmethod.jp/cloud/aws/understanding-codeuri-property-and-deployment-package-in-serverless-application-model/)を参考にしました)

```bash:ディレクトリ構造
$ tree
.
├── sample.yml
└── src
    └── handlers
        └── my_lambda_function
            ├── my_lambda_function.py
            └── other_modules
                └── ・・・
```

`aws cloudformation package`を用いてテンプレートを作成するためのテンプレートのサンプルを作成します。
先程の例と同じく、デプロイするlambda関数(`my_lambda_function.py`)は受け取った値に5を掛ける関数です。

```yaml:sample.yml
AWSTemplateFormatVersion : '2010-09-09'
Description: Lambda Function.

Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code: src/handlers/my_lambda_function # 相対パスを用いるときはテンプレートファイルからの相対パスである必要がある
#------------------以下省略------------------#
```

```src/handlers/my_lambda_function/my_lambda_function.py
import json
import cfnresponse
def handler(event, context):
  responseValue = int(event['ResourceProperties']['Input']) * 5
  responseData = {}
  responseData['Data'] = responseValue
  cfnresponse.send(event, context, cfnresponse.SUCCESS, responseData, "CustomResourcePhysicalID")
```

そして、`aws cloudformation package`を実行します。

```bash
$ aws cloudformation package \
    --template-file sample.yml \
    --s3-bucket my-lambda-bucket \
    --output-template-file packaged-template.yml
```

実行できれば`my_lambda_function`以下のモジュールも含めたlambdaの関数が一つのオブジェクトとして圧縮されS3にアップロードされます。
さらに、このアップロードしたS3オブジェクトへのパスが書き込まれたテンプレートファイル(`packaged-template.yml`)が出力されます。

```yaml:packaged-template.yml
AWSTemplateFormatVersion : '2010-09-09'
Description: Lambda Function.

Resources:
  LambdaFunction:
    Type: AWS::Lambda::Function
    Properties:
      Code:
        S3Bucket: my-bucket-name
        S3Key: 4b5ba6316bdbweac8b1de6a498ef6317
#------------------以下省略------------------#
```

あとはこの作成された`packaged-template.yml`に対して`aws cloudformation deploy`を実行することでlambdaを作成・更新ができます。

```bash
$ aws cloudformation deploy \
    --template-file packaged-template.yml \
    --stack-name my-lambda-stack-name \
    --capabilities CAPABILITY_NAMED_IAM CAPABILITY_IAM
```

# 補足

`aws cloudformation package`ではサンプルのようにランダムな文字列を用いて命名したファイルをアップロードしていくのでS3バケット上にどんどん圧縮されたファイルが保存されていきます。
過去の関数をS3に置き続ける必要がない場合はライフサイクルを設定して消してしまってもいいと思います。

実は関数以外のところを書くのが一番面倒なのがCloudformationです。
こんな記事を書いておきながらですが、雰囲気掴んでしまった後はAWS SAM使いましょう。
ちなみにSAMでもpackageコマンド使えます。


# 参考資料
**リファレンス**
https://docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-lambda-function.html
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/deploy/index.html
https://docs.aws.amazon.com/cli/latest/reference/cloudformation/package.html

**ブログ**
https://www.ketancho.net/entry/2017/12/30/022511
https://dev.classmethod.jp/cloud/aws/understanding-codeuri-property-and-deployment-package-in-serverless-application-model/
https://www.magata.net/memo/index.php?Lambda%A1%F5API%20Gateway%A4%F2CloudFormation%A4%C7%BA%EE%C0%AE%A4%B9%A4%EB