私が考えるLambda開発環境のベストプラクティス
概要
(この記事のアイコン、Lambdaっぽいの頑張って探した..!)
2020年にLambdaにコンテナイメージがサポートされて以来、Lambda開発をしたことがなかった。
AWS Lambda の新機能 – コンテナイメージのサポート
以前Lambdaを開発しているときは、適用するときにzip化が面倒だなとか、ローカルとクラウド上での動きが違うのでバグりやすいなとか、そのバグが適用しないと分からないので開発に時間がかかるなとか、色々と不便があったのを覚えている。
先日、画像圧縮処理で久々にLambdaを触り、その際コンテナイメージを使った快適なLambda開発環境を考えたので紹介する。言語はPythonだが他の言語でも同じ構成で大丈夫なはず。
結論
ディレクトリ構造
$ tree -a
.
├── .env
├── .env.local
├── Dockerfile
├── app.py
├── docker-compose.yaml
└── requirements.txt
app.py
に処理を記述
def lambda_handler(event, context):
# some logic
コンテナを起動せずに動作確認(※何度も回して確認したいとき)
$ python -c "import app; app.lambda_handler(None, None)"`
コンテナを起動して動作確認(※適用後と類似した環境で確認したいとき)
$ 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のローカルへのインストールは公式に書いてある。
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
この時点で動作確認したいはずなので、その場合はターミナルなどから実行する。
$ python -c "import app; app.lambda_handler(None, None)"`
環境変数や引数を渡すのが面倒なので、下記のように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
も必須。
Pillow==8.1.1
動作確認は以下のようにコマンドをうつ。
# イメージをビルド
$ 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
を準備する。
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
というファイルを用意するのがベター。
TARGET_BUCKET_NAME=target-bucket
TARGET_BUCKET_NAME=
これで下記コマンドのみでローカルでLambdaを起動したような状態にできる。
$ docker-compose up --build
因みにdocker-compose.yaml
や.env
はローカルでの開発時のみ利用するので、実際の適用で使うのはDockerfile
とapp.py
、requirements.txt
だけ。
このあと実際のデプロイの仕組みまで整えるとかなりスムーズになるが内容がボリューミーなので別の機会に書けたらなと思う。
2021/04/19 追記
私が考えるLambda周りのIaC及びCI/CDのベタープラクティスという記事を書きました。
Discussion