今回やること
CDK で Serverless アプリを開発する際に、Lambda や API の動作検証を行うには一旦デプロイした上で実行する必要がありました。これは比較的規模の大きいパイプラインを使っていたりする場合に結構厄介で、修正する度にデプロイ、パイプラインの実行を待つというプロセスを行わなければいけません。一応、cdk deploy --hostswap
を使うことで CloudFormation を介さずに Lambda の API を利用してコードを直接更新することもできますが、手元との環境に差分が出たりと、デプロイ環境を壊しかねないのであまり使いたくありません。
そんな中、今回待望の機能である SAM CLI による CDK アプリのローカルテストが GA されました!
早速触ってみたいと思います。
前提条件
試す前に、CDK がインストールされているのは前提としても、その他いくつか前提条件があります。
SAM CLI のインストール
SAM CLI が手元の Macbook にインストールされていなかったのでやっておきます。Homebrew で一発です。
$ brew tap aws/tap
$ brew install aws-sam-cli
インストールされたらバージョンも一応確認しておきます。
$ sam --version
SAM CLI, version 1.37.0
Docker のインストール
SAM CLI でローカルテストを実行するには Docker 環境が必要になります。MacOS の場合は ここ からインストールできます。インストールが完了したら Docker デーモンを起動しておきます。
早速試してみる
それでは実際に試してみましょう。ローカルテストの際は sam local
コマンドを利用することになりますが、help を見るとわかるとおり以下のようにいくつかサブコマンドがありそうです。
$ sam local --help
Usage: sam local [OPTIONS] COMMAND [ARGS]...
Run your Serverless application locally for quick development & testing
Options:
-h, --help Show this message and exit.
Commands:
generate-event You can use this command to generate sample payloads from...
invoke Invokes a local Lambda function once.
start-api Sets up a local endpoint you can use to test your API.
Supports hot-reloading so you don't need to restart this
service when you make changes to your function.
start-lambda Starts a local endpoint you can use to invoke your local
Lambda functions.
ざっとまとめるとそれぞれ以下の役割を持ってそうです。
コマンド | 説明 |
---|---|
sam local generate-event | Lambda 関数に渡すイベントのテンプレートを標準出力する |
sam local invoke | Lambda 関数を一度だけ実行する |
sam local start-api | API テスト用のローカルエンドポイントを作成する |
sam local start-lambda | Lambda 関数を実行するためのローカルエンドポイントを作成する |
sam local generate-event
まずは sam local generate-event
から試していきます。help を見ると以下のように色々テンプレートが用意されていそうです。
$ sam local generate-event --help
...
Commands:
alexa-skills-kit
alexa-smart-home
apigateway
appsync
batch
cloudformation
cloudfront
cloudwatch
codecommit
codepipeline
cognito
config
connect
dynamodb
kinesis
lex
rekognition
s3
sagemaker
ses
sns
sqs
stepfunctions
試しに API Gateway を出力してみます。
$ sam local generate-event apigateway aws-proxy
{
"body": "eyJ0ZXN0IjoiYm9keSJ9",
"resource": "/{proxy+}",
"path": "/path/to/resource",
"httpMethod": "POST",
"isBase64Encoded": true,
"queryStringParameters": {
"foo": "bar"
},
"multiValueQueryStringParameters": {
"foo": [
"bar"
]
},
"pathParameters": {
"proxy": "/path/to/resource"
},
"stageVariables": {
"baz": "qux"
},
"headers": {
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8",
"Accept-Encoding": "gzip, deflate, sdch",
"Accept-Language": "en-US,en;q=0.8",
"Cache-Control": "max-age=0",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "US",
"Host": "1234567890.execute-api.us-east-1.amazonaws.com",
"Upgrade-Insecure-Requests": "1",
"User-Agent": "Custom User Agent String",
"Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==",
"X-Forwarded-For": "127.0.0.1, 127.0.0.2",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
},
...
"requestContext": {
"accountId": "123456789012",
"resourceId": "123456",
"stage": "prod",
"requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef",
"requestTime": "09/Apr/2015:12:34:56 +0000",
"requestTimeEpoch": 1428582896000,
"identity": {
"cognitoIdentityPoolId": null,
"accountId": null,
"cognitoIdentityId": null,
"caller": null,
"accessKey": null,
"sourceIp": "127.0.0.1",
"cognitoAuthenticationType": null,
"cognitoAuthenticationProvider": null,
"userArn": null,
"userAgent": "Custom User Agent String",
"user": null
},
"path": "/prod/path/to/resource",
"resourcePath": "/{proxy+}",
"httpMethod": "POST",
"apiId": "1234567890",
"protocol": "HTTP/1.1"
}
}
これで Lambda 関数に渡すテストイベントもぱぱっと用意できそうですね。
sam local invoke
次は sam local invoke
です。sam local generate-event
以外は事前に cdk synth
で出力されるテンプレートが必要となります。今回は 前回 作成したテンプレートに含まれる以下の hello.py でテストしようと思います。
def handler(event, context):
return {
'statusCode': 200,
'headers': {
'Content-Type': 'text/plain'
},
'body': 'Hello'
}
まずは cdk synth
します。
$ cdk synth
これにより、アプリ名に応じたテンプレートファイル YourAppName.template.json が cdk.out/ 配下に生成されていると思います。
$ ls -l cdk.out/
total 536
-rw-r--r-- 1 yuk staff 663 Jan 21 15:15 ReactCDKAppTemplate.assets.json
-rw-r--r-- 1 yuk staff 59915 Jan 21 15:15 ReactCDKAppTemplate.template.json
drwxr-xr-x 6 yuk staff 192 Jan 21 15:15 assembly-ReactCDKAppTemplate-DeployBackend
drwxr-xr-x 6 yuk staff 192 Jan 21 15:15 assembly-ReactCDKAppTemplate-DeployFrontend
drwxr-xr-x 3 yuk staff 96 Jan 21 15:15 asset.0b3f59240ca16c5fc72224f1e8667f294b908b9b305c47894ea3a368e7e27041
-rw-r--r-- 1 yuk staff 20 Jan 21 15:15 cdk.out
-rw-r--r-- 1 yuk staff 9153 Jan 21 15:15 manifest.json
-rw-r--r-- 1 yuk staff 189598 Jan 21 15:15 tree.json
但し、今回利用するのは上記にある ReactCDKAppTemplate.template.json ではありません。これは CDK Pipeline によるパイプライン周りのリソースのみについて記載のあるテンプレートとなり、実際にアプリで利用するリソースについては assembly- から始まるディレクトリに格納されたテンプレートに記述されています。今回の場合は cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ 配下にある、ReactCDKAppTemplateDeployBackend42D3D30F.template.json となります。
$ ls -l cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/
total 72
-rw-r--r-- 1 yuk staff 1321 Jan 21 15:15 ReactCDKAppTemplateDeployBackend42D3D30F.assets.json
-rw-r--r-- 1 yuk staff 18886 Jan 21 15:15 ReactCDKAppTemplateDeployBackend42D3D30F.template.json
-rw-r--r-- 1 yuk staff 20 Jan 21 15:15 cdk.out
-rw-r--r-- 1 yuk staff 6256 Jan 21 15:15 manifest.json
それではまずはイベント無しでテストしてみます。
$ sam local invoke HelloHandler --no-event -t ./cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ReactCDKAppTemplateDeployBackend42D3D30F.template.json
Invoking hello.handler (python3.9)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-python3.9:rapid-1.37.0-x86_64.
Mounting /Users/yuk/Development/cdk/react-cdk-template/cdk.out/asset.0b3f59240ca16c5fc72224f1e8667f294b908b9b305c47894ea3a368e7e27041 as /var/task:ro,delegated inside runtime container
START RequestId: 42f7106f-9518-4a58-bb7d-ce414191705c Version: $LATEST
END RequestId: 42f7106f-9518-4a58-bb7d-ce414191705c
REPORT RequestId: 42f7106f-9518-4a58-bb7d-ce414191705c Init Duration: 1.47 ms Duration: 554.23 ms Billed Duration: 555 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"statusCode": 200, "headers": {"Content-Type": "text/plain"}, "body": "Hello"}
いい感じに実行されていそうですね。
それではテストイベントもつけてみましょう。テストイベント用 JSON ファイルを格納するディレクトリを作成し、そこに出力していきます。
$ mkdir localtest
$ sam local generate-event apigateway aws-proxy > ./localtest/apigateway-event.json
また、Lambda 側もテストイベントを出力するように変更を加えておきます。
import json
def handler(event, context):
print('request {}'.format(json.dumps(event)))
return {
'statusCode': 200,
'headers': {
'Content-Type': 'text/plain'
},
'body': 'Hello'
}
あとは cdk synth
して sam local invoke
でテストするだけです。
$ sam local invoke -e ./localtest/apigateway-event.json -t ./cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ReactCDKAppTemplateDeployBackend42D3D30F.template.json
Invoking hello.handler (python3.9)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-python3.9:rapid-1.37.0-x86_64.
Mounting /Users/yuk/Development/cdk/react-cdk-template/cdk.out/asset.7ecf47ee722d2d5e851f2b2a598aaed93023ca964474a4baf059f4f6b8f73f85 as /var/task:ro,delegated inside runtime container
START RequestId: 978c4f4b-51e4-41c4-8500-054b8aeb8036 Version: $LATEST
request {"body": "eyJ0ZXN0IjoiYm9keSJ9", "resource": "/{proxy+}", "path": "/path/to/resource", "httpMethod": "POST", "isBase64Encoded": true, "queryStringParameters": {"foo": "bar"}, "multiValueQueryStringParameters": {"foo": ["bar"]}, "pathParameters": {"proxy": "/path/to/resource"}, "stageVariables": {"baz": "qux"}, "headers": {"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", "Accept-Encoding": "gzip, deflate, sdch", "Accept-Language": "en-US,en;q=0.8", "Cache-Control": "max-age=0", "CloudFront-Forwarded-Proto": "https", "CloudFront-Is-Desktop-Viewer": "true", "CloudFront-Is-Mobile-Viewer": "false", "CloudFront-Is-SmartTV-Viewer": "false", "CloudFront-Is-Tablet-Viewer": "false", "CloudFront-Viewer-Country": "US", "Host": "1234567890.execute-api.us-east-1.amazonaws.com", "Upgrade-Insecure-Requests": "1", "User-Agent": "Custom User Agent String", "Via": "1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)", "X-Amz-Cf-Id": "cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA==", "X-Forwarded-For": "127.0.0.1, 127.0.0.2", "X-Forwarded-Port": "443", "X-Forwarded-Proto": "https"}, "multiValueHeaders": {"Accept": ["text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8"], "Accept-Encoding": ["gzip, deflate, sdch"], "Accept-Language": ["en-US,en;q=0.8"], "Cache-Control": ["max-age=0"], "CloudFront-Forwarded-Proto": ["https"], "CloudFront-Is-Desktop-Viewer": ["true"], "CloudFront-Is-Mobile-Viewer": ["false"], "CloudFront-Is-SmartTV-Viewer": ["false"], "CloudFront-Is-Tablet-Viewer": ["false"], "CloudFront-Viewer-Country": ["US"], "Host": ["0123456789.execute-api.us-east-1.amazonaws.com"], "Upgrade-Insecure-Requests": ["1"], "User-Agent": ["Custom User Agent String"], "Via": ["1.1 08f323deadbeefa7af34d5feb414ce27.cloudfront.net (CloudFront)"], "X-Amz-Cf-Id": ["cDehVQoZnx43VYQb9j2-nvCh-9z396Uhbp027Y2JvkCPNLmGJHqlaA=="], "X-Forwarded-For": ["127.0.0.1, 127.0.0.2"], "X-Forwarded-Port": ["443"], "X-Forwarded-Proto": ["https"]}, "requestContext": {"accountId": "123456789012", "resourceId": "123456", "stage": "prod", "requestId": "c6af9ac6-7b61-11e6-9a41-93e8deadbeef", "requestTime": "09/Apr/2015:12:34:56 +0000", "requestTimeEpoch": 1428582896000, "identity": {"cognitoIdentityPoolId": null, "accountId": null, "cognitoIdentityId": null, "caller": null, "accessKey": null, "sourceIp": "127.0.0.1", "cognitoAuthenticationType": null, "cognitoAuthenticationProvider": null, "userArn": null, "userAgent": "Custom User Agent String", "user": null}, "path": "/prod/path/to/resource", "resourcePath": "/{proxy+}", "httpMethod": "POST", "apiId": "1234567890", "protocol": "HTTP/1.1"}}
END RequestId: 978c4f4b-51e4-41c4-8500-054b8aeb8036
REPORT RequestId: 978c4f4b-51e4-41c4-8500-054b8aeb8036 Init Duration: 0.97 ms Duration: 554.80 ms Billed Duration: 555 ms Memory Size: 128 MB Max Memory Used: 128 MB
{"statusCode": 200, "headers": {"Content-Type": "text/plain"}, "body": "Hello"}
イベントが渡されているのも確認できますね。
sam local start-api
sam local start-api
では作成した API のローカルエンドポイントを生成してくれます。これにより curl やブラウザを用いてリクエストを投げることで API をテストできます。それでは実際にやってみます。
$ sam local start-api -t ./cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ReactCDKAppTemplateDeployBackend42D3D30F.template.json
Mounting HelloHandler2E4FBA4D at http://127.0.0.1:3000/{proxy+} [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]
Mounting HelloHandler2E4FBA4D at http://127.0.0.1:3000/ [DELETE, GET, HEAD, OPTIONS, PATCH, POST, PUT]
You can now browse to the above endpoints to invoke your functions. You do not need to restart/reload SAM CLI while working on your functions, changes will be reflected instantly/automatically. You only need to restart SAM CLI if you update your AWS SAM template
2022-01-21 16:48:09 * Running on http://127.0.0.1:3000/ (Press CTRL+C to quit)
すると上記のように http://127.0.0.1:3000/ がエンドポイントとして用意されたことがわかるので、実際に API を叩いてみます。
$ curl http://127.0.0.1:3000/
Hello
ちゃんとレスポンスが返却されたのがわかります。また、sam local start-api
を実行した端末側にはアクセスログ及び Lambda 関数のログが記録されます。
$ sam local start-api -t ./cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ReactCDKAppTemplateDeployBackend42D3D30F.template.json
...
Invoking hello.handler (python3.9)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-python3.9:rapid-1.37.0-x86_64.
Mounting /Users/yuk/Development/cdk/react-cdk-template/cdk.out/asset.7ecf47ee722d2d5e851f2b2a598aaed93023ca964474a4baf059f4f6b8f73f85 as /var/task:ro,delegated inside runtime container
START RequestId: bcbd5cbf-8cfe-45dd-a2cb-5e088982cb76 Version: $LATEST
request {"body": null, "headers": {"Accept": "*/*", "Host": "127.0.0.1:3000", "User-Agent": "curl/7.64.1", "X-Forwarded-Port": "3000", "X-Forwarded-Proto": "http"}, "httpMethod": "GET", "isBase64Encoded": false, "multiValueHeaders": {"Accept": ["*/*"], "Host": ["127.0.0.1:3000"], "User-Agent": ["curl/7.64.1"], "X-Forwarded-Port": ["3000"], "X-Forwarded-Proto": ["http"]}, "multiValueQueryStringParameters": null, "path": "/", "pathParameters": null, "queryStringParameters": null, "requestContext": {"accountId": "123456789012", "apiId": "1234567890", "domainName": "127.0.0.1:3000", "extendedRequestId": null, "httpMethod": "GET", "identity": {"accountId": null, "apiKey": null, "caller": null, "cognitoAuthenticationProvider": null, "cognitoAuthenticationType": null, "cognitoIdentityPoolId": null, "sourceIp": "127.0.0.1", "user": null, "userAgent": "Custom User Agent String", "userArn": null}, "path": "/", "protocol": "HTTP/1.1", "requestId": "3fe26cb2-6a4b-4748-bf18-1473de3fbb75", "requestTime": "22/Jan/2022:00:48:09 +0000", "requestTimeEpoch": 1642812489, "resourceId": "123456", "resourcePath": "/", "stage": "prod"}, "resource": "/", "stageVariables": null, "version": "1.0"}
END RequestId: bcbd5cbf-8cfe-45dd-a2cb-5e088982cb76
REPORT RequestId: bcbd5cbf-8cfe-45dd-a2cb-5e088982cb76 Init Duration: 0.95 ms Duration: 574.24 ms Billed Duration: 575 ms Memory Size: 128 MB Max Memory Used: 128 MB
2022-01-21 16:49:47 127.0.0.1 - - [21/Jan/2022 16:49:47] "GET / HTTP/1.1" 200 -
sam local start-lambda
最後は sam local start-lambda
です。これは API Gateway で作成した API を介さずに Lambda を実行できるようなエンドポイントを生成するためのコマンドとなります。それでは早速試してみます。
$ sam local start-lambda -t ./cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ReactCDKAppTemplateDeployBackend42D3D30F.template.json
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2022-01-21 16:59:39 * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)
これで Lambda 用のエンドポイントができたので、あとは AWS CLI で inovke すれば OK です。
$ aws lambda invoke --function-name HelloHandler --endpoint-url "http://127.0.0.1:3001" --no-verify-ssl out.txt
{
"StatusCode": 200
}
$ cat out.txt
{"statusCode": 200, "headers": {"Content-Type": "text/plain"}, "body": "Hello"}
また、sam local start-lambda
を実行した端末側でもログが出力されます。
$ sam local start-lambda -t ./cdk.out/assembly-ReactCDKAppTemplate-DeployBackend/ReactCDKAppTemplateDeployBackend42D3D30F.template.json
Starting the Local Lambda Service. You can now invoke your Lambda Functions defined in your template through the endpoint.
2022-01-21 17:05:42 * Running on http://127.0.0.1:3001/ (Press CTRL+C to quit)
Invoking hello.handler (python3.9)
Skip pulling image and use local one: public.ecr.aws/sam/emulation-python3.9:rapid-1.37.0-x86_64.
Mounting /Users/yuk/Development/cdk/react-cdk-template/cdk.out/asset.7ecf47ee722d2d5e851f2b2a598aaed93023ca964474a4baf059f4f6b8f73f85 as /var/task:ro,delegated inside runtime container
START RequestId: 01d52364-95bd-4be5-995d-b63bad46220c Version: $LATEST
request {}
END RequestId: 01d52364-95bd-4be5-995d-b63bad46220c
REPORT RequestId: 01d52364-95bd-4be5-995d-b63bad46220c Init Duration: 1.08 ms Duration: 549.82 ms Billed Duration: 550 ms Memory Size: 128 MB Max Memory Used: 128 MB
2022-01-21 17:07:14 127.0.0.1 - - [21/Jan/2022 17:07:14] "POST /2015-03-31/functions/HelloHandler/invocations HTTP/1.1" 200 -
おわりに
いかがでしたでしょうか。個人的には sam local
で用意されるサブコマンドはどれも有用だと感じました。これだとデプロイ環境を壊さずに安全に API や Lambda 関数のテストが行えますね。