AWS SAMでローカル環境でS3とDynamoDBを扱うLambdaを実行する

AWS SAMでローカル環境でS3とDynamoDBを扱うLambdaを実行する:


この投稿について

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ファイルを作成します。

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情報を追加します。

~/.aws/credentials
[localstack] 
aws_access_key_id = dummy 
aws_secret_access_key = dummy 
~/.aws/config
[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   
テストデータは以下の内容で作成しました。

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セクションに、作成した関数を追記します。

template.yml
Resources: 
    S3DynamoFunction: 
      Type: AWS::Serverless::Function 
      Properties: 
          CodeUri: s3_dynamo/build/ 
          Handler: app.lambda_handler 
          Runtime: python3.6 
ローカル実行用のプロファイルとして、以下のファイルも合わせて作成しました。

env-local.json
{ 
  "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側で判定できそうです。

s3_event.json
{ 
  "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からファイルを読んで標準出力に表示する部分だけを実装します。

s3_dynamo/app.py
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するコードの実装

以下が最終型です。

s3_dynamo/app.py
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秒になっているので、適当に伸ばしておきます。

template.yml
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のメリットを享受するために、これから吸収していければと思います。

コメント

このブログの人気の投稿

投稿時間:2021-06-17 22:08:45 RSSフィード2021-06-17 22:00 分まとめ(2089件)

投稿時間:2021-06-20 02:06:12 RSSフィード2021-06-20 02:00 分まとめ(3871件)

投稿時間:2021-06-17 05:05:34 RSSフィード2021-06-17 05:00 分まとめ(1274件)