🏃

私が考えるLambda開発環境のベストプラクティス

2021/03/14に公開

概要

(この記事のアイコン、Lambdaっぽいの頑張って探した..!)

2020年にLambdaにコンテナイメージがサポートされて以来、Lambda開発をしたことがなかった。
AWS Lambda の新機能 – コンテナイメージのサポート

以前Lambdaを開発しているときは、適用するときにzip化が面倒だなとか、ローカルとクラウド上での動きが違うのでバグりやすいなとか、そのバグが適用しないと分からないので開発に時間がかかるなとか、色々と不便があったのを覚えている。

先日、画像圧縮処理で久々にLambdaを触り、その際コンテナイメージを使った快適なLambda開発環境を考えたので紹介する。言語はPythonだが他の言語でも同じ構成で大丈夫なはず。

結論

ディレクトリ構造

mac_terminal
$ tree -a
.
├── .env
├── .env.local
├── Dockerfile
├── app.py
├── docker-compose.yaml
└── requirements.txt

app.pyに処理を記述

app.py
def lambda_handler(event, context):
    # some logic

コンテナを起動せずに動作確認(※何度も回して確認したいとき)

mac_terminal
$ python -c "import app; app.lambda_handler(None, None)"`

コンテナを起動して動作確認(※適用後と類似した環境で確認したいとき)

mac_terminal
$ docker-compose up --build
$ curl -d '{}' http://localhost:9000/2015-03-31/functions/function/invocations

この時用いるdocker-compose.yaml.envについては具体例とともに後述する。

具体例で解説

S3へのPutイベントをトリガーに、そのオブジェクト(今回は画像)をPillowライブラリで圧縮し、別バケットにPutするLambdaを開発する。

なお、Pythonがローカルにインストールされていることを前提とする。

app.pyに処理を書いて簡単に動作確認する

まずはapp.pyに処理を記述する。lambda_handlerの引数に何が渡ってくるかは実際にクラウドでLambdaを作って動かしてpprint等で出力するか、公式で確認しておく。Pillowのローカルへのインストールは公式に書いてある。

app.py
import os
import boto3
import urllib.parse
from io import BytesIO
from PIL import Image, ImageFilter

TARGET_BUCKET_NAME = os.environ.get('TARGET_BUCKET_NAME')

def lambda_handler(event, context):
    s3 = boto3.client('s3')

    # Putされたバケット名やキーを取得
    origin_bucket_name = event['Records'][0]['s3']['bucket']['name']
    key = urllib.parse.unquote_plus(
        event['Records'][0]['s3']['object']['key'], encoding='utf-8')

    # 画像をGet
    response = s3.get_object(Bucket=origin_bucket_name, Key=key)

    # 圧縮
    im = Image.open(response['Body'])
    im = im.convert('RGB')
    im_io = BytesIO()
    im.save(im_io, 'JPEG', quality=50, progressive=True)

    # 画像をPut
    s3.put_object(Body=im_io.getvalue(),
                  Bucket=TARGET_BUCKET_NAME, Key=key)

    return None

この時点で動作確認したいはずなので、その場合はターミナルなどから実行する。

mac_terminal
$ python -c "import app; app.lambda_handler(None, None)"`

環境変数や引数を渡すのが面倒なので、下記のようにapp.pyを書き換えて実行してしまう。

app.py
# ~省略~

# TARGET_BUCKET_NAME = os.environ.get('TARGET_BUCKET_NAME')
TARGET_BUCKET_NAME = 'target-bucket'

def lambda_handler(event, context):

    s3 = boto3.client('s3')

    # Putされたバケット名やキーを取得
    # origin_bucket_name = event['Records'][0]['s3']['bucket']['name']
    # key = urllib.parse.unquote_plus(
    #     event['Records'][0]['s3']['object']['key'], encoding='utf-8')

    origin_bucket_name = 'origin-bucket'
    key = 'key/to/image.jpg'
    
    # ~省略~

また、boto3は~/.aws/credentials[default]を読みに行くので忘れずに用意しておく。

このやり方は実行の簡単さがメリットなので、ロジックの全体像を書き終えるまでこのまま進むのがいいと思う。

Dockerfileからコンテナを起動して動作確認する

Lambdaのコンテナイメージ用のDockerfileを記述する。このイメージで実際のLambdaも動くので、ローカルでコンテナを起動することでクラウドとほぼ同じ環境を構築できる。

FROM public.ecr.aws/lambda/python:3.8

COPY requirements.txt ${LAMBDA_TASK_ROOT}
RUN pip install --upgrade pip
RUN pip install -r requirements.txt

COPY app.py ${LAMBDA_TASK_ROOT}

CMD ["app.lambda_handler"]

今回は外部ライブラリ(Pillow)をインストールしているので、requirements.txtも必須。

requirements.txt
Pillow==8.1.1

動作確認は以下のようにコマンドをうつ。

mac_terminal
# イメージをビルド
$ docker build -t {イメージ名} .
# コンテナ起動(ポートは任意)
$ docker run --rm -p 9000:8080 {イメージ名} -v $HOME/.aws/:/root/.aws/
# functionの呼び出し(引数を渡したい時は'{}'にdict型で渡す)
$ curl -d '{}' http://localhost:9000/2015-03-31/functions/function/invocations

app.pyの処理を変更するとイメージからビルドしなおす必要があるので、きりのいい時点と最終確認で実施するのが良いと思う。

docker-compose.yamlで快適にしつつ保守しやすくする

先程紹介したdocker runコマンドを毎回打つのは面倒だし、別の人がバグ修正などで触るときに起動方法が分からなかったり、そのためにREADMEを用意したりしなければならない。

それを回避するために、起動の単純化、兼ドキュメンテーションの役割を果たす下記のようなdocker-compose.yamlを準備する。

docker-compose.yaml
version: "3.6"

services:
  compress-image:
    container_name: compress-image
    build: .
    volumes:
      - $HOME/.aws/:/root/.aws/
    ports:
      - "9000:8080"
    env_file:
      - .env

また、今回は環境変数を用いるので.envファイルも必要。他の開発者のことも考え、Gitに上がるように変数名だけ記した.env.localというファイルを用意するのがベター。

.env
TARGET_BUCKET_NAME=target-bucket
.env.local
TARGET_BUCKET_NAME=

これで下記コマンドのみでローカルでLambdaを起動したような状態にできる。

mac_terminal
$ docker-compose up --build

因みにdocker-compose.yaml.envはローカルでの開発時のみ利用するので、実際の適用で使うのはDockerfileapp.pyrequirements.txtだけ。

このあと実際のデプロイの仕組みまで整えるとかなりスムーズになるが内容がボリューミーなので別の機会に書けたらなと思う。

2021/04/19 追記
私が考えるLambda周りのIaC及びCI/CDのベタープラクティスという記事を書きました。

Discussion