AWS SAM CLI で Lambda Container Support をお試し

この記事は [**セゾン情報システムズ Advent Calendar 2020**](https://qiita.com/advent-calendar/2020/sisco) 4日目の記事です。

## はじめに
AWS re:Invent 2020 で AWS Lambda の Container イメージサポートが発表されました。


:rocket: **AWS Lambda now supports container images as a packaging format**
https://aws.amazon.com/about-aws/whats-new/2020/12/aws-lambda-now-supports-container-images-as-a-packaging-format/

ついに AWS にも Cloud Run のようなものがきたか！と思ったそこのあなた
残念ながらあらゆるコンテナイメージを簡単に Lambda で動かすことができるという機能**ではありません**。
Lambda 関数をコンテナイメージとしてパッケージ化してデプロイできる機能であるため、
作成するコンテナイメージは Lambda Runtime APIを実装し、Lambda関数と互換性を持たせる必要があります。

AWS SAM CLI も v1.13.1 で コンテナイメージをサポートしています。

:rocket: **Release 1.13.1 - Support for Lambda Container Images**
https://github.com/aws/aws-sam-cli/releases/tag/v1.13.1

## やってみる
前述のとおり、AWS SAM CLI は v1.13.1 以上である必要があります。

```shell-session
$ sam --version
SAM CLI, version 1.13.1
```

### プロジェクト作成
`sam init` で利用できるプロジェクトテンプレートでパッケージタイプ `Image` が
選択できるようになっています。
ここでは nodejs12.x のベースイメージを使用してサンプルプロジェクトを作成します。

```shell-session
$ sam init
Which template source would you like to use?
        1 - AWS Quick Start Templates
        2 - Custom Template Location
Choice: 1
What package type would you like to use?
        1 - Zip (artifact is a zip uploaded to S3)
        2 - Image (artifact is an image uploaded to an ECR image repository)
Package type: 2

Which base image would you like to use?
        1 - amazon/nodejs12.x-base
        2 - amazon/nodejs10.x-base
        3 - amazon/python3.8-base
        4 - amazon/python3.7-base
        5 - amazon/python3.6-base
        6 - amazon/python2.7-base
        7 - amazon/ruby2.7-base
        8 - amazon/ruby2.5-base
        9 - amazon/go1.x-base
        10 - amazon/java11-base
        11 - amazon/java8.al2-base
        12 - amazon/java8-base
        13 - amazon/dotnetcore3.1-base
        14 - amazon/dotnetcore2.1-base
Base image: 1

Project name [sam-app]:

Cloning app templates from https://github.com/aws/aws-sam-cli-app-templates

    -----------------------
    Generating application:
    -----------------------
    Name: sam-app
    Base Image: amazon/nodejs12.x-base
    Dependency Manager: npm
    Output Directory: .

    Next steps can be found in the README file at ./sam-app/README.md
```

作成されるファイルは以下のとおりです。
Dockerfile が作成されていることがわかります。

```shell-session
$ cd sam-app
$ tree
.
├── events
│   └── event.json
├── hello-world
│   ├── app.js
│   ├── Dockerfile
│   ├── package.json
│   └── tests
│       └── unit
│           └── test-handler.js
├── README.md
└── template.yaml

```

Dockerfile では AWS が提供しているベースイメージが指定されています。
AWS 提供のベースイメージであれば Lambda 関数を実行するために必要なランタイムと
その他のコンポーネントがプリロードされているため、Lambda 関数のコードとその依存関係を
追加するだけで問題ありません。

```Dockerfile
FROM public.ecr.aws/lambda/nodejs:12

COPY app.js package.json ./

RUN npm install

# Command can be overwritten by providing a different command in the template directly.
CMD ["app.lambdaHandler"]
```

独自コンテナイメージを Lambda 互換にするには Lambda Runtime APIを実装する一連の
ソフトウェアパッケージである [Runtime Interface Clients (RIC) ](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-images.html#runtimes-api-client) を
ベースイメージに追加する必要があります。

`template.yaml` の内容は以下のとおりです。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: >
  sam-app

  Sample SAM Template for sam-app

# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst
Globals:
  Function:
    Timeout: 3

Resources:
  HelloWorldFunction:
    Type: AWS::Serverless::Function
    Properties:
      PackageType: Image
      # ImageConfig:
        # Uncomment this to override command here from the Dockerfile
        # Command: ["app.lambdaHandler"]
      Events:
        HelloWorld:
          Type: Api
          Properties:
            Path: /hello
            Method: get
    Metadata:
      DockerTag: nodejs12.x-v1
      DockerContext: ./hello-world
      Dockerfile: Dockerfile

Outputs:
  # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function
  # Find out more about other implicit resources you can reference within SAM
  # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api
  HelloWorldApi:
    Description: "API Gateway endpoint URL for Prod stage for Hello World function"
    Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/hello/"
  HelloWorldFunction:
    Description: "Hello World Lambda Function ARN"
    Value: !GetAtt HelloWorldFunction.Arn
  HelloWorldFunctionIamRole:
    Description: "Implicit IAM Role created for Hello World function"
    Value: !GetAtt HelloWorldFunctionRole.Arn
```

コンテナイメージでパッケージ化を行うには、`PackageType: Image` を指定する必要があります。
指定は省略可能で、指定可能な値は `Zip` or `Image` です。
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html#sam-function-packagetype


```yaml
    Properties:
      PackageType: Image
```

SAM CLI でコンテナイメージをビルドするには　Metadata リソース属性で、
Dockerfile, Context, Tag などの情報を宣言します。
DockerBuildArgs エントリでビルド時の引数を指定することも可能です。
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-build.html#build-container-image

```yaml
    Metadata:
      DockerTag: nodejs12.x-v1
      DockerContext: ./hello-world
      Dockerfile: Dockerfile
```

### イメージのビルド
sam build コマンドでコンテナイメージをビルドします。

```shell-session
$ sam build
Building codeuri: . runtime: None metadata: {'DockerTag': 'nodejs12.x-v1', 'DockerContext': './hello-world', 'Dockerfile': 'Dockerfile'} functions: ['HelloWorldFunction']
Building image for HelloWorldFunction function
Setting DockerBuildArgs: {} for HelloWorldFunction function
Step 1/4 : FROM public.ecr.aws/lambda/nodejs:12
 ---> ccbddaf00c51
Step 2/4 : COPY app.js package.json ./
 ---> fb16c5342f63
Step 3/4 : RUN npm install
 ---> Running in faa9eb4d503c
npm WARN deprecated debug@3.2.6: Debug versions >=3.2.0 <3.2.7 || >=4 <4.3.1 have a low-severity ReDos regression when used in a Node.js environment. It is recommended you upgrade to 3.2.7 or 4.3.1. (https://github.com/visionmedia/debug/issues/797)
npm WARN deprecated mkdirp@0.5.4: Legacy versions of mkdirp are no longer supported. Please update to mkdirp 1.x. (Note that the API surface has changed to use Promises in 1.x.)
npm notice created a lockfile as package-lock.json. You should commit this file.
added 107 packages from 544 contributors and audited 107 packages in 5.755s

16 packages are looking for funding
  run `npm fund` for details

found 0 vulnerabilities

 ---> 8db6f5505058
Step 4/4 : CMD ["app.lambdaHandler"]
 ---> Running in 08e14cc877aa
 ---> 271c7f34a0c7
Successfully built 271c7f34a0c7
Successfully tagged helloworldfunction:nodejs12.x-v1

Build Succeeded

Built Artifacts  : .aws-sam/build
Built Template   : .aws-sam/build/template.yaml

Commands you can use next
=========================
[*] Invoke Function: sam local invoke
[*] Deploy: sam deploy --guided
```

パッケージ化されたコンテナイメージが作成されました

```shell-session
$ docker image ls
REPOSITORY                     TAG                 IMAGE ID            CREATED             SIZE
helloworldfunction             nodejs12.x-v1       271c7f34a0c7        3 minutes ago       471MB
```

### ローカルテスト
コンテナイメージとしてパッケージ化された Lambda関数をローカルでテストするために
軽量のウェブサーバーである Lambda Runtime Interface Emulator（RIE）を使用することができます。
AWS 提供のベースイメージには RIE が組み込まれているため、ビルドしたイメージから
コンテナを作成するだけですぐにテストを実行できます。

```shell-session
$ docker run -d -p 9000:8080 helloworldfunction:nodejs12.x-v1
a510b1f8bf0fd5c0d7c6a3716ddaf96462452abcdff26285d1e8471c6e829cc8

$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{}'
{"statusCode":200,"body":"{\"message\":\"hello world\"}"}
```

RIE は HTTP リクエストを JSON イベントに変換し、プロキシする役割であるため、
X-Ray や その他の Lambda 統合機能についてはサポートしていません。

独自のベースイメージを使用する場合、RIE をイメージに組み込んだり、
bind マウントした RIE を Entrypoint としてコンテナを起動することでテストを実行できます。
https://docs.aws.amazon.com/lambda/latest/dg/images-test.html#images-test-alternative

もちろん今回は SAM CLI を使用しているので、`sam local invoke` によるテストも実行可能です。

```shell-session
$ sam local invoke
Invoking Container created from helloworldfunction:nodejs12.x-v1
Image was not found.
Building image.................
Skip pulling image and use local one: helloworldfunction:rapid-1.13.1.

START RequestId: 05284faf-5a20-46fc-a695-111eb3d0f085 Version: $LATEST
END RequestId: 05284faf-5a20-46fc-a695-111eb3d0f085
REPORT RequestId: 05284faf-5a20-46fc-a695-111eb3d0f085  Init Duration: 1.86 ms  Duration: 96.09 ms      Billed Duration: 100 ms Memory Size: 128 MB     Max Memory Used: 128 MB
```

### デプロイ
コンテナイメージを push するための リポジトリはあらかじめ作成しておく必要がありますが
イメージの push 自体は SAM CLI に任せることができます！

```shell-session
$ aws ecr create-repository --repository-name lambda-container-test
{
    "repository": {
        "repositoryUri": "123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-container-test",
        "imageScanningConfiguration": {
            "scanOnPush": false
        },
        "registryId": "123456789012",
        "imageTagMutability": "MUTABLE",
        "repositoryArn": "arn:aws:ecr:ap-northeast-1:123456789012:repository/lambda-container-test",
        "repositoryName": "lambda-container-test",
        "createdAt": 1606926983.0
    }
}
```

`sam deploy --guided` を実行します。`Image Repository` に先ほど作成したリポジトリを指定することで
デプロイ時にイメージの push が自動で行われます。 

```shell-session
$ sam deploy --guided

Configuring SAM deploy
======================

        Looking for config file [samconfig.toml] :  Not found

        Setting default arguments for 'sam deploy'
        =========================================
        Stack Name [sam-app]:
        AWS Region [us-east-1]: ap-northeast-1
        Image Repository []: 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-container-test
        Images that will be pushed:
          helloworldfunction:nodejs12.x-v1 to 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-container-test:helloworldfunction-271c7f34a0c7-nodejs12.x-v1

        #Shows you resources changes to be deployed and require a 'Y' to initiate deploy
        Confirm changes before deploy [y/N]: n
        #SAM needs permission to be able to create roles to connect to the resources in your template
        Allow SAM CLI IAM role creation [Y/n]: y
        HelloWorldFunction may not have authorization defined, Is this okay? [y/N]: y
        Save arguments to configuration file [Y/n]: y
        SAM configuration file [samconfig.toml]:
        SAM configuration environment [default]:

        Looking for resources needed for deployment: Found!

                Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx
                A different default S3 bucket can be set in samconfig.toml

        Saved arguments to config file
        Running 'sam deploy' for future deployments will use the parameters saved above.
        The above parameters can be changed by modifying samconfig.toml
        Learn more about samconfig.toml syntax at
        https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
The push refers to repository [123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-container-test]
6d7acece320f: Pushed
105893862807: Pushed
3642f26c4fcb: Pushed
1807102b87b6: Pushed
120614c3628c: Pushed
0d8c48ae73f7: Pushed
e4f26f8be15f: Pushed
af6d16f2417e: Pushed
helloworldfunction-271c7f34a0c7-nodejs12.x-v1: digest: sha256:7a83998f07e54249948db91846abd9dbfecbe175181fe0876b5980a635b9e70f size: 1998


        Deploying with following values
        ===============================
        Stack name                   : sam-app
        Region                       : ap-northeast-1
        Confirm changeset            : False
        Deployment image repository  : 123456789012.dkr.ecr.ap-northeast-1.amazonaws.com/lambda-container-test
        Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-xxxxxxxxxxxxx
        Capabilities                 : ["CAPABILITY_IAM"]
        Parameter overrides          : {}
        Signing Profiles           : {}
~~以降省略~~
```

作成された API Gateway にリクエストして、動作を確認します。

```shell-session
$ curl https://xxxxxxxxxx.execute-api.ap-northeast-1.amazonaws.com/Prod/hello/
{"message":"hello world"}
```

成功しました。
Lambda のコンソール側でも関数コードがコンテナイメージとしてデプロイされていることを確認できます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/221cb95d-b434-1635-b4e7-5c605dac47ba.png)

## 注意点など
* CloudFormation のスタックを削除しても、ECR 側のイメージは削除されません。

* コールドスタード時の実行時間

厳密な比較が行えていないので一概には言えないと思いますが、 zip のデプロイパッケージと比較すると
若干 Init Duration (初期化) に時間を要しているように伺えました。

* 課金期間

初期化時間も課金期間に含まれます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/125105/2baf1464-91fb-8341-4617-ebad11243cae.png)
カスタムランタイムと同じ扱いのようです。

**Building a custom runtime**
https://docs.aws.amazon.com/lambda/latest/dg/runtimes-custom.html#runtimes-custom-build

> Initialization counts towards billed execution time and timeout. 



## 参考
**New for AWS Lambda – Container Image Support**
https://aws.amazon.com/jp/blogs/aws/new-for-aws-lambda-container-image-support/
**AWS Lambda - Developer Guide**
https://docs.aws.amazon.com/lambda/latest/dg/lambda-images.html
**AWS Serverless Application Model - Developer Guide**
https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-using-build.html

以上です。
参考になれば幸いです。