この投稿について
Serverlessconf Tokyo 2018で色々と刺激を受け、Lambdaに取り組んでみようと思い、色々と試す上でLambdaをローカル環境で開発や動作確認をするのに色々迷う部分が多かったので、メモとして残したものです。
動作環境
以下のものを使用しています。
- AWS SAM
- AWS CLI
- docker
- docker-compose
# Pythonはvenv
$ python --version
Python 3.6.6
$ aws --version
aws-cli/1.16.24 Python/3.7.0 Darwin/18.0.0 botocore/1.12.14
$ sam --version
SAM CLI, version 0.6.0
SAMプロジェクトの作成
sam init
コマンドを使用して空のプロジェクトを作成します。
- -r はruntime
- -n はプロジェクト名
を指定しています。
$ sam init -r python3.6 -n sam-s3-lambda-app
無事に成功すると以下のようなパッケージ構成が作成されます。
$ cd sam-s3-lambda-app
$ tree .
.
├── README.md
├── hello_world
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-37.pyc
│ │ └── app.cpython-37.pyc
│ └── app.py
├── requirements.txt
├── template.yaml
├── tests
│ └── unit
│ ├── __init__.py
│ ├── __pycache__
│ │ ├── __init__.cpython-37.pyc
│ │ └── test_handler.cpython-37.pyc
│ └── test_handler.py
hello_worldの動作確認
まずは自動的に作成された、hello_world関数を実行してみます。
# ライブラリのインストール
$ pip install -r requirements.txt -t hello_world/build/
$ cp hello_world/*.py hello_world/build/
# イベントの生成
$ sam local generate-event apigateway aws-proxy > api_event.json
# テスト実行
$ sam local invoke HelloWorldFunction --event api_event.json
2018-10-25 18:31:13 Invoking app.lambda_handler (python3.6)
Fetching lambci/lambda:python3.6 Docker container image......
.
.
.
{"statusCode": 200, "body": "{\"message\": \"hello world\", \"location\": \"IP Address\"}"}
無事に動作確認ができました。
実際にS3とDynamoDBを扱う部分を実装
LocalStackコンポーネントの作成
今回はS3とDynamoDBをエミュレートするために、LocalStackを使用しました。
https://github.com/localstack/localstack
LocalStack自体はdockerで立ち上げます。
以下のようにdocker-compose.yml
ファイルを作成します。
version: "3.3"
services:
localstack:
container_name: localstack
image: localstack/localstack
ports:
- "4569:4569"
- "4572:4572"
environment:
- SERVICES=dynamodb,s3
- DEFAULT_REGION=ap-northeast-1
- DOCKER_HOST=unix:///var/run/docker.sock
s3とdynamodbがそれぞれ使用するポートはこちら
- s3:4572
- dynamodb:4569
LocalStackの起動
※初回はイメージのダウンロードがあるため、時間がかかります。
$ docker-compose up
LocalStack用credentialの作成
LocalStack用にcredential情報を追加します。
[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy
[profile localstack]
region = ap-northeast-1
output = json
LocalStack上にDynamoDBのテーブルを作成
AWSのドキュメントを参考にテーブルを作成します。
https://docs.aws.amazon.com/ja_jp/amazondynamodb/latest/developerguide/Tools.CLI.html
$ aws dynamodb create-table \
> --table-name Music \
> --attribute-definitions \
> AttributeName=Artist,AttributeType=S \
> AttributeName=SongTitle,AttributeType=S \
> --key-schema AttributeName=Artist,KeyType=HASH AttributeName=SongTitle,KeyType=RANGE \
> --provisioned-throughput ReadCapacityUnits=1,WriteCapacityUnits=1 \
> --endpoint-url http://localhost:4569 --profile localstack
$ aws dynamodb list-tables --endpoint-url http://localhost:4569 --profile localstack
TABLENAMES Music
LocalStack上のS3にテストデータを配置
LocakStackのS3にバケットの作成と、実ファイルの配置を行います。
# bucketの作成
$ aws s3 mb s3://music/ --endpoint-url=http://localhost:4572 --profile localstack
make_bucket: music
# テストデータのput
$ aws s3 cp ./testdata.json s3://music/rawdata/testdata.json --endpoint-url=http://localhost:4572 --profile localstack
upload: ./testdata.json to s3://music/rawdata/testdata.json
テストデータは以下の内容で作成しました。
{
"Artist": "Acme Band",
"SongTitle": "Happy Day",
"AlbumTitle": "Songs About Life"
}
関数のベースを作成
まずは依存関係にboto3
を追加します。
boto3
はPythonでS3やDynamoDBなどのリソースを扱うためのライブラリです。
https://github.com/boto/boto3
$ pip install boto3
$ pip freeze > requirements.txt
関数の作成
今回はs3_dynamoという関数にしました。
$ mkdir s3_dynamo
$ touch s3_dynamo/__init__.py s3_dynamo/app.py
SAMのテンプレートのResources
セクションに、作成した関数を追記します。
Resources:
S3DynamoFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: s3_dynamo/build/
Handler: app.lambda_handler
Runtime: python3.6
ローカル実行用のプロファイルとして、以下のファイルも合わせて作成しました。
{
"S3DynamoFunction": {
"AWS_SAM_LOCAL": true
}
}
S3イベントの作成
SAM CLIの機能で各Lambda関数のイベントを生成できるようになっているので、S3をputするイベント定義を作成します。
https://docs.aws.amazon.com/ja_jp/lambda/latest/dg/test-sam-cli.html
わかりにくいですが、 --key
にはS3のバケット配下のパスを渡します。
$ sam local generate-event s3 put --bucket music --key rawdata/testdata.json --region ap-northeast-1 > s3_event.json
出力されたイベントファイルがこちら。
awsRegion
,bucket.name
,object.key
が合っていれば正しくLambda側で判定できそうです。
{
"Records": [
{
"eventVersion": "2.0",
"eventSource": "aws:s3",
"awsRegion": "ap-northeast-1",
.
.省略
.
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "testConfigRule",
"bucket": {
"name": "music",
"ownerIdentity": {
"principalId": "EXAMPLE"
},
"arn": "arn:aws:s3:::music"
},
"object": {
"key": "rawdata/testdata.json",
"size": 1024,
"eTag": "0123456789abcdef0123456789abcdef",
"sequencer": "0A1B2C3D4E5F678901"
}
}
}
]
}
S3からの読み込み処理を実装
まずはS3からファイルを読み取れることを確認するために、以下のようにS3からファイルを読んで標準出力に表示する部分だけを実装します。
import os
import json
import boto3
import pprint
import urllib.parse
if os.getenv("AWS_SAM_LOCAL"):
dynamodb = boto3.resource(
'dynamodb',
endpoint_url='http://host.docker.internal:4569/'
)
s3 = boto3.client(
's3',
endpoint_url='http://host.docker.internal:4572/'
)
else:
dynamodb = boto3.resource('dynamodb')
s3 = boto3.client('s3')
def lambda_handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
print("[bucket]: " + bucket + " [key]: " + key)
try:
response = s3.get_object(Bucket=bucket, Key=key)
d = json.loads(response['Body'].read())
pprint.pprint(d)
except Exception as e:
print(e)
raise e
env-local.json
を使用して実行するため、今回はS3 / DynamoDBとしては以下の定義が使われます。
sam localで実行する場合もdocker内部から実行されるようで、ホスト名はhost.docker.internal
を使っています。
dynamodb = boto3.resource(
'dynamodb',
endpoint_url='http://host.docker.internal:4569/'
)
s3 = boto3.client(
's3',
endpoint_url='http://host.docker.internal:4572/'
)
実際に実行してみます。
$ pip install -r requirements.txt -t s3_dynamo/build
$ cp s3_dynamo/*.py s3_dynamo/build/
$ sam local invoke S3DynamoFunction --event s3_event.json --profile localstack
# result
[bucket]: music [key]: rawdata/testdata.json
{'AlbumTitle': 'Songs About Life',
'Artist': 'Acme Band',
'SongTitle': 'Happy Day'}
無事にbuket、keyが渡され、S3ファイルの中身を表示することができています。
DynamoDBへputするコードの実装
以下が最終型です。
import os
import json
import boto3
import pprint
import urllib.parse
if os.getenv("AWS_SAM_LOCAL"):
dynamodb = boto3.resource(
'dynamodb',
endpoint_url='http://host.docker.internal:4569/'
)
s3 = boto3.client(
's3',
endpoint_url='http://host.docker.internal:4572/'
)
else:
dynamodb = boto3.resource('dynamodb')
s3 = boto3.client('s3')
table = dynamodb.Table('Music')
def lambda_handler(event, context):
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
print("[bucket]: " + bucket + " [key]: " + key)
try:
response = s3.get_object(Bucket=bucket, Key=key)
d = json.loads(response['Body'].read())
pprint.pprint(d)
table.put_item(
Item=d
)
except Exception as e:
print(e)
raise e
わかりにくいですが、以下のコードを追加しています。
table = dynamodb.Table('Music')
.
.
.
table.put_item(
Item=d
)
LambdaのTimeoutを伸ばす
デフォルトではLambdaの実行時間上限は3秒になっているので、適当に伸ばしておきます。
Globals:
Function:
Timeout: 100
結果確認
再度実行してみます。
$ cp s3_dynamo/*.py s3_dynamo/build/
$ sam local invoke S3DynamoFunction --event s3_event.json --profile localstack
AWS CLIでMusicテーブルの中身を確認
$ aws dynamodb scan --table-name Music --endpoint-url http://localhost:4569 --profile localstack
1 1
ALBUMTITLE Songs About Life
ARTIST Acme Band
SONGTITLE Happy Day
無事にDynamoDBのテーブルにデータがputされています。
おわりに
今までLambdaのコンソール上で短いコードを書いて実行するといったことはやったことがありましたが、実際にローカルに開発環境を用意して実行してみるという部分は初めてだったので、ハマりどころや分からない部分も多くありました。
実際にSAMを使用してAWS上にデプロイするにはCloudFormationを使うなど、もう少し学ぶところがありそうだという印象でした。
Serverlessのメリットを享受するために、これから吸収していければと思います。