この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。
こんにちは、やっとEchoを入手した城岸です。今回はCloud9のローカルテストでLocalStackを使えるようにしてみようと思います。あらかじめ言っておきますが、結構無理やりな感じで実現しています。
Cloud9やLocalStack、あとCloud9の中で利用されているSAM Localの内容については以下の記事などをご覧ください。
- AWS Cloud9 で Lambda 関数の作成・実行・デバッグ・デプロイをやってみる #reinvent
- AWS Cloud9 で API Gateway がらみのテストをする #reinvent
- LocalStackをつかってローカルでLambdaを実行してみた
- [新ツール] AWS Serverless Application Model (AWS SAM) を使ってサーバーレスアプリケーションを構築する
- [新ツール]AWS SAMをローカル環境で実行できるSAM Localがベータリリース
- AWS SAM Local と LocalStack を使って ローカルでAWS Lambdaのコードを動かす
ちょっと量が多いですが、上記ブログに目を通していただいた後に本ブログを見ていただけると理解しやすいと思います。
前提知識:Cloud9環境の実態とローカルテスト
Cloud9環境の実態
Cloud9はバックエンドでEC2インスタンス(もしくはそれと同等のリソース)が動いていてます。ブラウザ上のEnvironmentに表示されているファイルの実態はEC2インスタンス上に存在しています。
ローカルテスト
Cloud9では作成したLambdaファンクションに対しローカルテストが実行できます。ローカルテストは現在『Lambdaファンクションのテスト』と、『擬似的なAPI Gatewayを経由したLambdaファンクションのテスト』の2パターンが実行できます。名前の通りですがこのテストはEC2インスタンス上で実行されます。
テスト実行時にコンテナが起動していたので「おやっ」と思ったのですが、実態としてはSAM Localが利用されているようです。
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
66d7f31b0590 lambci/lambda:python3.6 "/var/lang/bin/pyt..." 4 seconds ago Up 3 seconds trusting_wilson
$ docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
lambci/lambda nodejs4.3 74d482955025 2 months ago 933 MB
lambci/lambda python2.7 64cb3bf41370 2 months ago 965 MB
lambci/lambda nodejs6.10 711874a983f1 2 months ago 1.01 GB
lambci/lambda python3.6 bcad631a6973 2 months ago 1.09 GB
$ ps aux | grep sam
ec2-user 25491 0.1 0.1 208580 14256 ? Sl 10:23 0:00 sam local start-api --template template.yaml --port 3039 --skip-pull-image
テスト対象のLambdaファンクション内でAWSのリソースを操作している場合、特に考慮しなければそのままAWSのエンドポイントに対し実行されます。まぁこれはこれでしょうがない気がしますが、ちょっと都合が悪い時があります。
はいっ、そこで登場LocalStack。ローカルテストの時にはLocalStackのリソース使っちゃおうぜ!!
ということで、なんか前振りが長くなってしまいましたがこれが今回のやりたいことになります。
Cloud9環境の構築
Cloud9環境は以下のブログを参考に構築します。EC2インスタンスはm4.large、lambdaはPython3.6、Function triggerはAPI Gatewayを選択しています。
- AWS Cloud9 で Lambda 関数の作成・実行・デバッグ・デプロイをやってみる #reinvent
- AWS Cloud9 で API Gateway がらみのテストをする #reinvent
LocalStackのインストール
EC2インスタンス上で以下のコマンドを実行し、LocalStackをインストールしていきます。
$ sudo pip install docker-compose
$ git clone https://github.com/atlassian/localstack.git
$ cd localstack
lambdaの作成
今回はこのコードを使ってテストをしてきます。
lambda_function.py
import boto3
import json
def set_endpoint(event):
if '127.0.0.1' in event['requestContext']['identity']['sourceIp']:
dynamodb = boto3.resource('dynamodb', endpoint_url='http://XXX.XXX.XXX.XXX:4569/')
else:
dynamodb = boto3.resource('dynamodb')
return dynamodb
def lambda_handler(event, context):
body_data=json.loads(event['body'])
first_name=body_data['first_name']
last_name=body_data['last_name']
dynamodb = set_endpoint(event)
table = dynamodb.Table('users')
response = table.put_item(
Item={
'username': (first_name+last_name).lower(),
'first_name': first_name,
'last_name': last_name
}
)
lambda_response = {
'statusCode': response['ResponseMetadata']['HTTPStatusCode'],
'body': json.dumps(response)
}
return lambda_response
4行目から9行目の部分がエンドポイントを変えているところになります。
event実行のソースIPがローカルであれば向き先をLocalStackに変更するようにしています。正直Lambdaファンクション内にこのような分岐のコードができるのはあまり好きじゃないですが、Cloud9でエンドポイントを変えるにはこうする他ないのかなと思っています。他に良い方法を知っている方がいたら教えてください。
6行目のendpoint_urlのXXX.XXX.XXX.XXXの部分は、EC2のプライベートIPを設定します。SAM LocalのコンテナとLocalStackのコンテナが同一のネットワーク上にいれば、LocalStackのIPでもいけると思います。
ローカルテストの実行
まずは、EC2インスタンスでLocalStackを起動し初期データを登録します。今回は事前にDynamoDBを作成します。
$ docker-compose up
別のターミナルで以下を実行します。
$ aws --endpoint-url=http://localhost:4569 dynamodb create-table --table-name users \
--attribute-definitions AttributeName=username,AttributeType=S \
--key-schema AttributeName=username,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
ようやく実行の準備が整いました。2パターンあるテストのうち『Lambdaファンクションのテスト』から実行していきます。 画面左側のRUNを押下後、対象をLambda(local)、Payloadに以下を入力後、画面右側のRUNを実行します。
{
"body": "{\"first_name\":\"jane\",\"last_name\":\"Doe\"}",
"requestContext": {
"identity": {
"sourceIp": "127.0.0.1:0000"
}
}
}
するとレスポンスとしてstatusCodeの200が返ってきていることが確認できると思います。また、以下のコマンドでLocalStack内にリソースが格納されていることも確認できます。
$ aws --endpoint-url=http://localhost:4569 dynamodb \
get-item --table-name users --key '{"username":{"S": "janedoe" }}'
{
"Item": {
"username": {
"S": "janedoe"
},
"first_name": {
"S": "jane"
},
"last_name": {
"S": "Doe"
}
}
}
次に『擬似的なAPI Gatewayを経由したLambdaファンクションのテスト』を実行していきます。 対象をAPI Gateway(local)とし、Payloadに以下を入力後、RUNを実行します。明示的に設定するPayloadが先ほどより少ないのは、 擬似的なAPI Gatewayによりeventが付与されるためです。
{
"first_name":"jane",
"last_name":"Doe2"
}
event.json
{
"httpMethod": "PUT",
"body": "{\n \"first_name\":\"jane\",\n \"last_name\":\"Doe2\"\n}",
"resource": "/testpath",
"requestContext": {
"resourceId": "",
"apiId": "",
"resourcePath": "/testpath",
"httpMethod": "",
"requestId": "",
"accountId": "",
"identity": {
"apiKey": "",
"userArn": "",
"cognitoAuthenticationType": "",
"caller": "",
"userAgent": "",
"user": "",
"cognitoIdentityPoolId": "",
"cognitoIdentityId": "",
"cognitoAuthenticationProvider": "",
"sourceIp": "127.0.0.1:43002",
"accountId": ""
}
},
"queryStringParameters": {},
"headers": {
"Accept": "*/*",
"Content-Length": "51",
"Content-Type": "application/json",
"User-Agent": "curl/7.53.1"
},
"pathParameters": {},
"stageVariables": null,
"path": "/testpath"
}
こちらの方もレスポンスとしてHTTPStatusCodeの200が返ってきていること、LocalStack内にリソースが格納されていることが確認できます。
$ aws --endpoint-url=http://localhost:4569 dynamodb \
get-item --table-name users --key '{"username":{"S": "janedoe2" }}'
{
"Item": {
"username": {
"S": "janedoe2"
},
"first_name": {
"S": "jane"
},
"last_name": {
"S": "Doe2"
}
}
}
デプロイ、API Gatewayからの実行
ローカルでのテストが完了したので、次はAWS環境にデプロイしてテストをしていきます。まず、画面右側のメニュよりLambdaファンクションをデプロイします。
次に以下のコマンドを実行し、AWS環境にDynamoDBを作成します。endpointは指定しません。
aws dynamodb create-table --table-name users \
--attribute-definitions AttributeName=username,AttributeType=S \
--key-schema AttributeName=username,KeyType=HASH \
--provisioned-throughput ReadCapacityUnits=5,WriteCapacityUnits=5
そして、デプロイされたLambdaに対する権限も変更します。今回はDynamoDBを操作するためLambdaに付与されているIAMロールにAmazonDynamoDBFullAccessのポリシーをアタッチしました。
ラストです。対象をAPI Gateway(remote)とし、Payloadに以下を入力後、RUNを実行します。
{
"first_name": "jane",
"last_name": "doe3"
}
DynamoDBにデータが格納されましたー!!大成功!!
まとめ
この辺りAWSとして整備されていくと嬉しいのですが、今のところは利用者側で頑張るしかありません。
また、今回はやりたいことは実現できたのですがベストプラクティスではないかなと思っているので他にもっといいやり方あるよ!っていう方がいたら是非連絡ください。
また、分岐のロジックに若干不安があるので、実際に本番利用するのであれば少なくともCloud9操作者のIAMロールはCloud9以外のリソースにアクセスできないよう制限しておいた方が無難かと思います。