はじめに
AWS上で動かしているアプリをどうにかしてローカルで実行できないかと思って
色々試してみてどうにか Lambda を動かすことができたのでそのまとめ。
LocalStack を使った構成
使っている技術など
- Docker
- Docker Compose
- LocalStack
- Python3
ディレクトリ構成
.
├── docker-compose.yml
├── app # ここにAPIのコードが入っている感じ。今回は説明対象外
└── docker
├── api
│ └── Dockerfile # 今回は説明対象外
├── lambda
│ └── Dockerfile # AWS で実行するやつ。今回は説明対象外
└── localstack
├── create_lambda.sh
├── dummy_function.py # 別にここにある必要はないけど、同じところにあった方が管理しやすいので
└── dummy_function.zip
ソースコード
全部載せると量が半端ないので、必要なところだけ抜粋。
version: '3'
services:
db:
container_name: db
image: postgres:14.2-alpine
volumes:
- postgres:/var/lib/postgresql
environment:
POSTGRES_USER: test
POSTGRES_PASSWORD: password
POSTGRES_DB: test
PGPASSWORD: password
api:
container_name: api
build:
context: ./docker/api
dockerfile: Dockerfile
environment:
- LAMBDA_FUNCTION_NAME: dummy_function
- AWS_LAMBDA_ENDPOINT: http://localstack:4566
- AWS_ACCESS_KEY_ID: test # localstack内部で使っている KEY ID
- AWS_SECRET_ACCESS_KEY: test # localstack内部で使っている ACCESS KEY
- AWS_DEFAULT_REGION: us-east-1 # localstackと一致していればどこでもよい
localstack:
container_name: localstack
image: localstack/localstack:0.14.2
volumes:
- ./docker/localstack:/docker-entrypoint-initaws.d
- localstack:/localstack
environment:
- DEBUG=${DEBUG- }
- DEFAULT_REGION=us-east-1
- SERVICES=${SERVICES- }
- HOSTNAME_EXTERNAL=localstack
- LS_LOG=info
- DATA_DIR=/localstack
- LAMBDA_HOST=hoge_lambda:8080
hoge_lambda:
container_name: hoge_lambda
build:
context: ./lambda
dockerfile: Dockerfile # 実際に AWS 上で動かす Lambda の Dockerfile
environment:
AWS_ACCESS_KEY_ID: test
AWS_SECRET_ACCESS_KEY: text
AWS_DEFAULT_REGION: us-east-1
volumes:
localstack:
postgres:
import http.client
import json
import os
LAMBDA_HOST = os.getenv("LAMBDA_HOST", "hoge_lambda:8080")
def lambda_handler(event, context):
conn = http.client.HTTPConnection(LAMBDA_HOST)
conn.request(
"POST", "/2015-03-31/functions/function/invocations", json.dumps(event), {"Content-type": "application/json"}
)
response = conn.getresponse()
if response.status != 200:
print(
"Failed to execute request. Status: {}, Message: {}, data: {}".format(
response.status, response.reason, response.read().decode("utf8")
)
)
raise Exception()
body = response.read().decode("utf8")
return json.loads(body)
LAMBDA_HOST
を docker-compose.yml
の environment
などで設定すると、
任意の Lambda にリクエストを送ることが可能。
あと、本当なら requests
などより使いやすい http クライアントを使った方がよいのだと思うのだけど、
単なるリクエストの proxy で且つ外部ライブラリのインストールは極力避けたかったのでこの方法を採用。
#!/bin/bash
awslocal lambda create-function \
--function-name dummy_function \
--zip-file fileb:///docker-entrypoint-initaws.d/dummy_function.zip \
--handler dummy_function.lambda_handler \
--runtime python3.8 \
--role test-role
echo "Finished."
ここで指定している dummy_function.zip
は dummy_function.py
だけを入れた Zip ファイルで、
予め作成して ./docker/localstack
などに置いておく。
import json
import os
import boto3
class AwsLambdaService:
LAMBDA_FUNCTION_NAME = os.getenv("LAMBDA_FUNCTION_NAME")
AWS_LAMBDA_ENDPOINT = os.getenv("AWS_LAMBDA_ENDPOINT")
def invoke(self, payload):
client = self._get_client()
client.invoke(
FunctionName=self.LAMBDA_FUNCTION_NAME, InvocationType="Event", Payload=json.dumps(payload),
)
@staticmethod
def _get_client():
return boto3.client(
"lambda",
endpoint_url=AWS_LAMBDA_ENDPOINT,
)
AWS_LAMBDA_ENDPOINT
を指定することで localstack 側の Lambda にリクエストを送ることが可能で、
ここが None
だと AWS の Lambda にリクエストを送る。
なので、ローカルと本番で同じコードを使うことが可能。
おわりに
この方法だと、アプリと Lambda の間に proxy 用の Lambda が入るだけで他は同じものを使える点がよいと思っている。
あと、今回はアプリケーションから Lambda を呼び出すパターンに対応したけど、
S3 のファイル作成をトリガーとして実行されるパターンも基本的には問題ないはず
( localstack で実行できれば。。。)