Lambdaがコンテナイメージをサーポートしたので試してみた

## はじめに
ローカルでLambdaを開発する際に、いざデプロイしようと思った時に困った方も少なくないと思います。
というのも、デフォルトでプリインストールされていないライブラリをimportしている場合はzipファイルにしてAWSコンソール上でアップロードする必要があります。
もしくはServerless FrameworkやSAMを導入されている方が多いのかと思います。
個人的にはちょっとした関数をLambdaで実装しようとした時にServerless FrameworkやSAMはゴツすぎるというかやりたいことに対して出来ることと、開発環境や設定が手間になると思い敬遠していました。
そこで今回Lambdaがコンテナイメージをサーポートしたので慣れ親しんだDockerfileでデプロイできるなら良いのではと考え試してみました。

### 使用環境とバージョン
- macOS Catalina
- aws-cli/2.0.28
- Docker version 19.03.13
- Lambdaの使用言語 Python3.8

## 記事の対象
- ある程度AWSとDockerの知識がある方
- ECSもしくはEKSを利用してアプリケーションを作成している方

## 事前準備
- AWS CLIを利用できること
- IAMユーザもしくはスイッチロール先に`ecr:GetAuthorizationToken`権限が付与されていること ※1

## Amazon ECR の作成
dockerイメージをpushするリポジトリを事前に作成します。
AWS CLIで作成します。以下のコマンドをローカルで実行して下さい。

```
aws ecr create-repository \
    --repository-name lambda-container
```

## 実行ファイルの作成
今回作成するLambdaには以下の要素を含めようと思います。

- Lambda実行環境に標準で含まれないライブラリを使用する
- boto3により、AWSリソースにアクセスする

自分の経験則から、Lambdaを使用する際にpythonのライブラリを追加したいシーンがよくありました。
今までは`python -m venv {環境名}`でローカルに仮想環境を構築し、追加ライブラリをinstallしてからzipにしてAWSコンソールからLambdaにアップロードしていました。
その辺りがdockerイメージを使用することで改善できることを期待しています。
また、LambdaではよくAWSリソースを参照することがあるのでboto3での権限周りを整理できればより実用的なものになると思います。
以下のpythonファイルを使用することとします。

```python:lambda-container.py
import os
import logging
import requests
import json
import boto3

rds = boto3.client('rds')

logger = logging.getLogger()
logger.setLevel(logging.INFO)

# 返却用クラス
class LambdaResponse:
    # コンストラクタ
    def __init__(self, db, zip):
        self.db = db
        self.zip = zip
    # json形式で返却
    def json(self):
        db = {}
        instances = []
        for instance in self.db['DBInstances']:
            res = {}
            res['Identifier'] = instance['DBInstanceIdentifier']
            res['Status'] = instance['DBInstanceStatus']
            instances.append(res)
        
        db['Instances'] = instances
        return {
            'db': db,
            'zip': self.zip
        }

# メインハンドラー
def lambda_handler(event, context):
    logger.info('event: {}'.format(event))
    # お試し： RDSインスタンスをDescribe
    describe = rds.describe_db_instances(DBInstanceIdentifier='edu-demodb-rds01-postgres11')
    logger.info('describe db instances: {}'.format(describe))
    # お試し： Defaultライブラリーに含まれない機能（郵便番号から住所を検索）
    response = requests.get('https://zipcloud.ibsnet.co.jp/api/search?zipcode={}'.format(event['zip']))
    logger.info('Status: {}, Body: {}'.format(response.status_code, json.dumps(response.json(), ensure_ascii=False)))
    return LambdaResponse(describe, response.json()).json()
```

## requirements.txtの作成
こちらは`pip`での一般的なインストール方法のため説明を省きます。
詳しく知りたい方は以下を参照して下さい。
[pip install - pip documentation v20.3.3](https://pip.pypa.io/en/stable/reference/pip_install/)

```
###### Requirements without Version Specifiers ######
requests
# boto3 　<- Lambdaのdockerイメージに含まれているため不要です。無くても動きます

###### Requirements with Version Specifiers ######

```

## Dockerfileの作成
以下に使用するDockerfileになります。
`pip install -r requirements.txt`にて追加のライブラリをインストールします。
インストール先はDockerコンテナ内になるため、ローカル環境を汚すことはありませんでした。
`CMD`には実行対象となるハンドラーを指定して下さい。

```dockerfile:Dockerfile
FROM public.ecr.aws/lambda/python:3.8
COPY lambda-container.py requirements.txt ${LAMBDA_TASK_ROOT}/
RUN pip install --upgrade pip
RUN pip install -r requirements.txt
RUN pip list
CMD [ "lambda-container.lambda_handler" ]
```

dockerイメージは２種類ありDocker Hubにも用意されています。
基本的に同じものでリポジトリがAWSかDockerかの違いだと思うので基本的には手順通りのAWS側を使用すれば良いと思います。
[amazon/aws-lambda-python - Docker Hub](https://hub.docker.com/r/amazon/aws-lambda-python)

```dockerfile
FROM amazon/aws-lambda-python:3.8
```

また、dockerイメージから以下の環境変数が提供されています。
`${LAMBDA_TASK_ROOT}`に必要なファイルを`COPY`して下さい。
> The AWS base images provide the following environment variables:

> - LAMBDA_TASK_ROOT=/var/task
> - LAMBDA_RUNTIME_DIR=/var/runtime

[Creating Lambda container images - AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html)

## ローカル環境でLambdaを実行
以下のディレクトリに成果物を配置することにします。事前に作成しておいて下さい。

```
$ mkdir lambda-container
$ cd lambda-container
$ ls
Dockerfile lambda-container.py requirements.txt
```

以下の手順でローカル環境にてLambdaを実行することができます。

```
$ docker build -t lambda-container .
$ docker run -p 9000:8080 lambda-container
$ curl -XPOST "http://localhost:9000/2015-03-31/functions/function/invocations" -d '{"zip":"1310045"}'
{
    "db": {
        "Instances": [
            {
                "Identifier": "xxxxxxxxxx",
                "Status": "stopped"
            }
        ]
    },
    "zip": {
        "message": null,
        "results": [
            {
                "address1": "東京都",
                "address2": "墨田区",
                "address3": "押上",
                "kana1": "ﾄｳｷｮｳﾄ",
                "kana2": "ｽﾐﾀﾞｸ",
                "kana3": "ｵｼｱｹﾞ",
                "prefcode": "13",
                "zipcode": "1310045"
            }
        ],
        "status": 200
    }
}
```

## boto3の認証について
上記の通りにローカル実行すると、以下のようなエラーが発生します。
これはdockerコンテナ内にboto3用のCredentialsが存在しないためです。

```json
{
    "errorMessage": "Unable to locate credentials",
    "errorType": "NoCredentialsError",
    "stackTrace": [
        "  File \"/var/task/lambda-container.py\", line 37, in lambda_handler\n    describe = rds.describe_db_instances(DBInstanceIdentifier='xxxxxxxxx')\n",
        "  File \"/var/runtime/botocore/client.py\", line 357, in _api_call\n    return self._make_api_call(operation_name, kwargs)\n",
        "  File \"/var/runtime/botocore/client.py\", line 662, in _make_api_call\n    http, parsed_response = self._make_request(\n",
        "  File \"/var/runtime/botocore/client.py\", line 682, in _make_request\n    return self._endpoint.make_request(operation_model, request_dict)\n",
        "  File \"/var/runtime/botocore/endpoint.py\", line 102, in make_request\n    return self._send_request(request_dict, operation_model)\n",
        "  File \"/var/runtime/botocore/endpoint.py\", line 132, in _send_request\n    request = self.create_request(request_dict, operation_model)\n",
        "  File \"/var/runtime/botocore/endpoint.py\", line 115, in create_request\n    self._event_emitter.emit(event_name, request=request,\n",
        "  File \"/var/runtime/botocore/hooks.py\", line 356, in emit\n    return self._emitter.emit(aliased_event_name, **kwargs)\n",
        "  File \"/var/runtime/botocore/hooks.py\", line 228, in emit\n    return self._emit(event_name, kwargs)\n",
        "  File \"/var/runtime/botocore/hooks.py\", line 211, in _emit\n    response = handler(**kwargs)\n",
        "  File \"/var/runtime/botocore/signers.py\", line 90, in handler\n    return self.sign(operation_name, request)\n",
        "  File \"/var/runtime/botocore/signers.py\", line 162, in sign\n    auth.add_auth(request)\n",
        "  File \"/var/runtime/botocore/auth.py\", line 357, in add_auth\n    raise NoCredentialsError\n"
    ]
}
```

幾つか解決方法があると思いますが、簡単な方法として以下のように、`docker run`する時に環境変数を設定します。

```bash
$ docker run \
    -e AWS_ACCESS_KEY_ID=${AWS_ACCESS_KEY_ID} \
    -e AWS_SECRET_ACCESS_KEY=${AWS_SECRET_ACCESS_KEY} \
    -e AWS_DEFAULT_REGION=${AWS_DEFAULT_REGION} \
    -p 9000:8080 lambda-container
```

以下のドキュメントにあるように、boto3はConfigオブジェクトを使用して明示的に上書しない限り、環境変数を使用して認証を行うようです。

> # Using environment variables
Configurations can be set through the use of system-wide environment variables. If set, these configurations are global and will affect all clients created unless explicitly overwritten through the use of a Config object.

[Configuration — Boto3 Docs 1.16.49 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html)

## Amazon ECRにpush
無事ローカルでLambdaを実行することができたのでECRにpushします。
ECRへpushする方法については、以下の記事で解説していますので、手順のみ記載します。
[Amazon ECRのDockerイメージをローカルにpull、pushする](https://qiita.com/kano1234/items/12d2fc74ee6cd7bce58e)

```
$ aws ecr get-login-password --region ap-northeast-1 \
    | docker login --username AWS --password-stdin {aws_account_id}.dkr.ecr.{region}.amazonaws.com

$ docker images
REPOSITORY                                                   TAG                 IMAGE ID            CREATED             SIZE
lambda-container                                             latest              9ec9f9c8cabf        5 months ago        402MB

$ docker tag 9ec9f9c8cabf {aws_account_id}.dkr.ecr.{region}.amazonaws.com/lambda-container
$ docker push {aws_account_id}.dkr.ecr.{region}.amazonaws.com/lambda-container
```

## Lmabdaをdockerイメージで作成
dockerイメージの準備が整ったので、Lambdaを作成していきます。
AWSコンソールにログインしてLambdaの作成でコンテナイメージを選択して下さい。
![screencapture-ap-northeast-1-console-aws-amazon-lambda-home-2021-01-07-15_26_14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756249/e84bdc0c-10b3-4917-d749-152fd9faa036.png)

Lambdaの作成が完了しました。現状ではAWSコンソールではコードが表示されないようです。
![screencapture-ap-northeast-1-console-aws-amazon-lambda-home-2021-01-07-17_40_20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756249/d499eea8-0655-42fa-c914-6c660a1262de.png)

テストも無事成功です。
![screencapture-ap-northeast-1-console-aws-amazon-lambda-home-2021-01-07-17_45_23.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/756249/9d1a6250-7fb5-5284-3ea1-aa334d607e3e.png)

## まとめ
ちょっとしたLambdaの場合は不要かと思いますが、何度も修正が想定される場合は良いと思いました。
また、CodeCommitで履歴管理し、CodePipelineで自動デプロイすればより使いやすいと思います。

## 参考記事
- [pip install - pip documentation v20.3.3](https://pip.pypa.io/en/stable/reference/pip_install/)
- [amazon/aws-lambda-python - Docker Hub](https://hub.docker.com/r/amazon/aws-lambda-python)
- [Creating Lambda container images - AWS Lambda](https://docs.aws.amazon.com/lambda/latest/dg/images-create.html)
- [Configuration — Boto3 Docs 1.16.49 documentation](https://boto3.amazonaws.com/v1/documentation/api/latest/guide/configuration.html)
- [Amazon ECRのDockerイメージをローカルにpull、pushする](https://qiita.com/kano1234/items/12d2fc74ee6cd7bce58e)