カスタムdockerイメージでlambdaを動かす

[AWS & Game Advent Calendar 2020](https://qiita.com/advent-calendar/2020/aws-game) の 20 日目の記事になります。

## agenda

* はじめに
* 利用するツール
* ローカルでの実装
* Lambdaへのdeploy
* 動作チェック
* まとめ

## はじめに

2020年のre:Inventで、Lambdaがコンテナをサポートする発表がありました。
今までカスタムランタイムでLambdaを動かすには面倒なセットアップが必要でしたがこのアップデートでかなり使いやすくなるのではないでしょうか。

この記事ではコンテナを使ってミニマムの(bashを使ってイベントデータを表示させる)関数をLambdaで動作させる解説を行います。

## 利用するツール

* sam: Lambdaのdeploy
* docker: コンテナイメージのビルドなど
* ECR: イメージのレジストリ
* bash: 何でも良いのですが今回はbashでechoをさせます

### samのインストール

samはHomebrewを使ってインストールしました

```
$ brew tap aws/tap
$ brew install aws-sam-cli
```

```
$ sam --version
SAM CLI, version 1.15.0
```

### ECRのリポジトリ作成

awscliを使ってECRリポジトリを作成します。リポジトリ名は``docker-lambda-bash``とします。

```
$ aws ecr create-repository --repository-name docker-lambda-bash
```

## ローカルでの実装

カスタムランタイムのチュートリアルは[こちら](https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/runtimes-walkthrough.html)です。
bashでLambdaを動作させるために必要なものは以下の３つです。

* カスタムランタイムのDockerイメージ
* bootstrap
* function.sh

### カスタムランタイムのイメージ

[こちら](https://gallery.ecr.aws/lambda/provided)の公式のイメージを使って実装を進めます。

### bootstrap

詳細はチュートリアルを参照してください。
簡単に説明すると、whileを回してLambdaAPIを使ってLambdaのイベントを取得します。
取得したイベントデータをレスポンスとして返却しています。

```bootstrap
#!/bin/sh -xv

set -euo pipefail

# ハンドラ関数の読み込み
source $LAMBDA_TASK_ROOT/"$(echo $_HANDLER | cut -d. -f1).sh"

while true
do
  HEADERS="$(mktemp)"

  # イベントの取得
  EVENT_DATA=$(curl -sS -LD "$HEADERS" -X GET "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/next")
  REQUEST_ID=$(grep -Fi Lambda-Runtime-Aws-Request-Id "$HEADERS" | tr -d '[:space:]' | cut -d: -f2)

  # ハンドラ関数の実行
  RESPONSE=$($(echo "$_HANDLER" | cut -d. -f2) "$EVENT_DATA")

  # 結果を返却
  curl -X POST "http://${AWS_LAMBDA_RUNTIME_API}/2018-06-01/runtime/invocation/$REQUEST_ID/response" -d "$RESPONSE"
done
```

### function.sh

こちらも詳細はチュートリアルを参照してください。
イベントデータをechoするだけのscriptです。


```function.sh
function handler () {
  EVENT_DATA=$1
  echo "$EVENT_DATA" 1>&2;
  RESPONSE="Echoing request: '$EVENT_DATA'"
  echo $RESPONSE
}
```

### ローカル環境での実行

ローカルでこの関数を実行するには、以下のDockerfileを使ってイメージをビルドします。

```dockerfile
FROM public.ecr.aws/lambda/provided:al2
COPY bootstrap ${LAMBDA_RUNTIME_DIR}
COPY function.sh ${LAMBDA_TASK_ROOT}
CMD [ "function.handler" ]
```

内容は至ってシンプルで、AmazonLinux2(al2)のイメージをベースにして、``bootstrap``をLambdaのランタイムDirに、``function.sh``をLambdaのタスクDirに配置し、``function.handler``をCMDで実行するというものです。

Lambdaの関数の指定方法は、``(スクリプト名).(関数名)``です。
この例では、``function.sh``という名前のスクリプトの中の、``handler``という関数を実行させる必要があるので、このような指定方法となります。

dockekrfileが整ったので以下の様にイメージをビルドします。

```
$ docker build -t docker-lambda-bash .
```

ビルドしたイメージを使ってrunします。

```
$ docker run -p 9000:8080 docker-lambda-bash
```

curlで9000番portにアクセスするとリクエストデータが返却されます。

```
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"payload":"hello world!"}'
Echoing request: '{"payload":"hello world!"}'
```

## Lambdaへのdeploy

Lambdaへのdeployはsamを利用します。
samの詳細な使い方は割愛しますが、大まかな手順は以下の通りです。

* ECRにログイン
* docker tagでイメージにtagging
* ECRにpush
* samでイメージを指定してdeploy(Lambda関数の作成)

### ECRにログイン

ECRにログインするには以下のコマンドを実行します。

```
$ export aws_region=ap-northeast-1 # 環境変数にリージョンを設定
$ export aws_account_id=xxxxxxxxxx # 環境変数にAWSのアカウントIDを設定
$ aws ecr get-login-password --region $aws_region | docker login --username AWS --password-stdin $aws_account_id.dkr.ecr.$aws_region.amazonaws.com
```

### tagging & push

```
$ docker tag docker-lambda $aws_account_id.dkr.ecr.$aws_region.amazonaws.com/docker-lambda-bash
$ docker push $aws_account.dkr.ecr.$aws_region.amazonaws.com/docker-lambda-bash
```

### samでイメージを指定してdeploy

samでLambdaをdeployするにはtemplateを作成する必要があります。
環境変数``aws_account_id``と``aws_region``を指定する必要があるので、適宜``(YOUR-AWS-ACCOUNT-ID)``と``(YOUR-AWS-REGION)``を差し替えてください。

```template.yaml
AWSTemplateFormatVersion: '2010-09-09'
Transform: AWS::Serverless-2016-10-31
Description: lambda-custom-docker-sample

Globals:
  Function:
    Timeout: 3

Resources:
  MyCustomDocker:
    Type: AWS::Serverless::Function
    Properties:
      FunctionName: CustomDockerWithBash
      ImageUri: (YOUR-AWS-ACCOUNT-ID).dkr.ecr.(YOUR-AWS-REGION).amazonaws.com/docker-lambda-bash:latest
      PackageType: Image
      Events:
        CustomDockerEcho:
          Type: Api
          Properties:
            Path: /echo
            Method: get

Outputs:
  MyCustomDockerApi:
    Description: 'API Gateway endpoint URL'
    Value: !Sub 'https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/echo/'
```

``ImageUri``でECRにpushしたコンテナイメージを指定しています。
また、``Outputs``を指定してApiGatewayを使って確認できるようにエンドポイントを作成しています。

### deploy

``sam deploy --guided``でdeployします。deploy時のログは以下となります。

```
$ sam deploy --guided

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

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

	Setting default arguments for 'sam deploy'
	=========================================
	Stack Name [sam-app]: lambda-custiom-docker-sample
	AWS Region [us-east-1]: ap-northeast-1
	Image Repository for MyCustomDocker: xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/docker-lambda-bash

	#Shows you resources changes to be deployed and require a 'Y' to initiate deploy
	Confirm changes before deploy [y/N]: y
	#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
	MyCustomDocker 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: Not found.
	Creating the required resources...
	Successfully created!

		Managed S3 bucket: aws-sam-cli-managed-default-samclisourcebucket-1b4wws4k1oxg
		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


	Deploying with following values
	===============================
	Stack name                   : lambda-custiom-docker-sample
	Region                       : ap-northeast-1
	Confirm changeset            : True
	Deployment image repository  :
                                       {
                                           "MyCustomDocker": "xxxxxxxxxx.dkr.ecr.ap-northeast-1.amazonaws.com/docker-lambda-bash"
                                       }
	Deployment s3 bucket         : aws-sam-cli-managed-default-samclisourcebucket-1b4wws4k1oxg
	Capabilities                 : ["CAPABILITY_IAM"]
	Parameter overrides          : {}
	Signing Profiles             : {}

Initiating deployment
=====================
MyCustomDocker may not have authorization defined.
Uploading to lambda-custiom-docker-sample/e1f6334b9552a49d5e9e07a9970bbae3.template  697 / 697.0  (100.00%)

Waiting for changeset to be created..

CloudFormation stack changeset
-------------------------------------------------------------------------------------------------------------------------------------------------------------
Operation                               LogicalResourceId                       ResourceType                            Replacement
-------------------------------------------------------------------------------------------------------------------------------------------------------------
+ Add                                   MyCustomDockerCustomDockerEchoPermiss   AWS::Lambda::Permission                 N/A
                                        ionProd
+ Add                                   MyCustomDockerRole                      AWS::IAM::Role                          N/A
+ Add                                   MyCustomDocker                          AWS::Lambda::Function                   N/A
+ Add                                   ServerlessRestApiDeploymentc8d8b9cf15   AWS::ApiGateway::Deployment             N/A
+ Add                                   ServerlessRestApiProdStage              AWS::ApiGateway::Stage                  N/A
+ Add                                   ServerlessRestApi                       AWS::ApiGateway::RestApi                N/A
-------------------------------------------------------------------------------------------------------------------------------------------------------------

Changeset created successfully. arn:aws:cloudformation:ap-northeast-1:xxxxxxxxxx:changeSet/samcli-deploy1608550235/9b8db82c-4197-4d15-9624-fc35f2e35bd8


Previewing CloudFormation changeset before deployment
======================================================
Deploy this changeset? [y/N]: y

2020-12-21 20:31:50 - Waiting for stack create/update to complete

CloudFormation events from changeset
-------------------------------------------------------------------------------------------------------------------------------------------------------------
ResourceStatus                          ResourceType                            LogicalResourceId                       ResourceStatusReason
-------------------------------------------------------------------------------------------------------------------------------------------------------------
CREATE_IN_PROGRESS                      AWS::IAM::Role                          MyCustomDockerRole                      -
CREATE_IN_PROGRESS                      AWS::IAM::Role                          MyCustomDockerRole                      Resource creation Initiated
CREATE_COMPLETE                         AWS::IAM::Role                          MyCustomDockerRole                      -
CREATE_IN_PROGRESS                      AWS::Lambda::Function                   MyCustomDocker                          -
CREATE_IN_PROGRESS                      AWS::Lambda::Function                   MyCustomDocker                          Resource creation Initiated
CREATE_COMPLETE                         AWS::Lambda::Function                   MyCustomDocker                          -
CREATE_IN_PROGRESS                      AWS::ApiGateway::RestApi                ServerlessRestApi                       -
CREATE_IN_PROGRESS                      AWS::ApiGateway::RestApi                ServerlessRestApi                       Resource creation Initiated
CREATE_COMPLETE                         AWS::ApiGateway::RestApi                ServerlessRestApi                       -
CREATE_IN_PROGRESS                      AWS::ApiGateway::Deployment             ServerlessRestApiDeploymentc8d8b9cf15   -
CREATE_IN_PROGRESS                      AWS::Lambda::Permission                 MyCustomDockerCustomDockerEchoPermiss   -
                                                                                ionProd
CREATE_IN_PROGRESS                      AWS::ApiGateway::Deployment             ServerlessRestApiDeploymentc8d8b9cf15   Resource creation Initiated
CREATE_IN_PROGRESS                      AWS::Lambda::Permission                 MyCustomDockerCustomDockerEchoPermiss   Resource creation Initiated
                                                                                ionProd
CREATE_COMPLETE                         AWS::ApiGateway::Deployment             ServerlessRestApiDeploymentc8d8b9cf15   -
CREATE_IN_PROGRESS                      AWS::ApiGateway::Stage                  ServerlessRestApiProdStage              -
CREATE_IN_PROGRESS                      AWS::ApiGateway::Stage                  ServerlessRestApiProdStage              Resource creation Initiated
CREATE_COMPLETE                         AWS::ApiGateway::Stage                  ServerlessRestApiProdStage              -
CREATE_COMPLETE                         AWS::Lambda::Permission                 MyCustomDockerCustomDockerEchoPermiss   -
                                                                                ionProd
CREATE_COMPLETE                         AWS::CloudFormation::Stack              lambda-custiom-docker-sample            -
-------------------------------------------------------------------------------------------------------------------------------------------------------------

CloudFormation outputs from deployed stack
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Outputs
---------------------------------------------------------------------------------------------------------------------------------------------------------------
Key                 MyCustomDockerApi
Description         API Gateway endpoint URL
Value               https://ddcq0ciwhb.execute-api.ap-northeast-1.amazonaws.com/Prod/echo/
---------------------------------------------------------------------------------------------------------------------------------------------------------------

Successfully created/updated stack - lambda-custiom-docker-sample in ap-northeast-1
$
```

## 動作チェック

成功したので、``Outputs``のURLにアクセスしてみます。

```
$ curl 'https://ddcq0ciwhb.execute-api.ap-northeast-1.amazonaws.com/Prod/echo?param1=val1&param2=val2'
{"message": "Go Serverless v1.0! Your function executed successfully!", "input": {"resource": "/echo", "path": "/echo", "httpMethod": "GET", "headers": {"Accept": "*/*", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "JP", "Host": "ddcq0ciwhb.execute-api.ap-northeast-1.amazonaws.com", "User-Agent": "curl/7.64.1", "Via": "2.0 06f6824c0d57ccd48408cb017c7bce76.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "-lVoexDRq5sBmapw-aMSE8l0ypW4IXMZK3kECxkp4qZVT2ZpLgsAZQ==", "X-Amzn-Trace-Id": "Root=1-5fe08a9d-26bf7d24044bf6127630150f", "X-Forwarded-For": "124.213.96.14, 70.132.19.137", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "multiValueHeaders": {"Accept": ["*/*"], "CloudFront-Forwarded-Proto": ["https"], "CloudFront-Is-Desktop-Viewer": ["true"], "CloudFront-Is-Mobile-Viewer": ["false"], "CloudFront-Is-SmartTV-Viewer": ["false"], "CloudFront-Is-Tablet-Viewer": ["false"], "CloudFront-Viewer-Country": ["JP"], "Host": ["ddcq0ciwhb.execute-api.ap-northeast-1.amazonaws.com"], "User-Agent": ["curl/7.64.1"], "Via": ["2.0 06f6824c0d57ccd48408cb017c7bce76.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["-lVoexDRq5sBmapw-aMSE8l0ypW4IXMZK3kECxkp4qZVT2ZpLgsAZQ=="], "X-Amzn-Trace-Id": ["Root=1-5fe08a9d-26bf7d24044bf6127630150f"], "X-Forwarded-For": ["124.213.96.14, 70.132.19.137"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"]}, "queryStringParameters": {"param1": "val1", "param2": "val2"}, "multiValueQueryStringParameters": {"param1": ["val1"], "param2": ["val2"]}, "pathParameters": null, "stageVariables": null, "requestContext": {"resourceId": "co37s7", "resourcePath": "/echo", "httpMethod": "GET", "extendedRequestId": "X5qYlFZ8tjMFcbg=", "requestTime": "21/Dec/2020:11:44:29 +0000", "path": "/Prod/echo", "accountId": "800832305859", "protocol": "HTTP/1.1", "stage": "Prod", "domainPrefix": "ddcq0ciwhb", "requestTimeEpoch": 1608551069250, "requestId": "f7c4fb6d-9621-44c3-8c60-03a0d259ce67", "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "sourceIp": "124.213.96.14", "principalOrgId": null, "accessKey": null, "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "curl/7.64.1", "user": null}, "domainName": "ddcq0ciwhb.execute-api.ap-northeast-1.amazonaws.com", "apiId": "ddcq0ciwhb"}, "body": null, "isBase64Encoded": false}}
$
```

Lambdaのイベントデータが表示されました。

## まとめ

カスタムランタイムをdockerコンテナで動作させるサンプルを紹介しました。
このプログラム自体はあまり意味のあるものではありませんが、カスタムランタイムのチュートリアルの例と比べたらかなりシンプルになっています。
また、ローカルとクラウドで実行環境が一致するので（同じコンテナの上で動作するので）動作が保証される点も嬉しいです。

## sample code

https://github.com/ohr486/lambda-container-image-sample