概要
sam local & localstack を使ってローカルでLambdaのテストする環境を構築したときの手順とハマったところをまとめたものです。おもに[2]を参考にして構築しました。
SAMのリソースの作成方法についてはここではあまり触れていません。
注: SAM local で Lambda を実行するよりも、local から直接テストを実行するほうが開発サイクルが早いです。これから Lambda Function のテストの実装を考えている方はこちらの記事を参照してください。
- [Lambda Function の開発について悩んだところをまとめてみた][lambda]
[lambda]: https://qiita.com/billthelizard/items/be87a7662c34fd5c634e
構築の要件
- Lambda Function の中で S3 を参照している部分をローカル環境でテストしたい
- テストサイクルが早くなることを期待
Stack
- AWS SAM
- SAM CLI, version 0.10.0
- localstack
手順
必要な資材のインストール
公式ページの説明にしたがってインストールします。
- aws-sam-cli
- docker for Mac
localstack の起動
version: '3'
services:
localstack:
image: localstack/localstack
ports:
- 4567-4578:4567-4578
- 8080:8080
dockerコンテナの起動
docker-compose up -d
profileの設定
locastack の場合も、aws-cli を呼び出すときに profile の設定がないとコケます[1]。
[localstack]
aws_access_key_id = dummy
aws_secret_access_key = dummy
[profile localstack]
region = us-east-1
output = text
Localstack上の S3 に Bucket/Object を作成する
Hi, there!
# bucket作成
aws s3 --endpoint-url=http://localhost:4572 mb s3://test-bucket --profile=localstack
# オブジェクト作成
aws s3 --endpoint-url=http://localhost:4572 cp message.txt s3://test-bucket --profile=localstack
docker networkの確認
sam local
実行時に docker network を指定しないと、SAM Local => localstackに疎通ができません。 [2]
$ docker network ls
NETWORK ID NAME DRIVER SCOPE
...
b2fbba06747a localstack_default bridge local
...
環境変数をSAMのテンプレートで指定する
sam local
実行時に --env-vars
で環境変数を指定しても、 SAMテンプレートに環境変数が指定されていないと有効になりません。
{
"S3ReadFunction": {
"IS_LOCAL_STACK" = "true",
"S3_BUCKET": "test-bucket"
}
}
Globals:
Function:
Environment:
Variables:
IS_LOCAL_STACK: 'false'
S3_BUCKET: 'production-bucket'
S3のエンドポイントに localstack のエンドポイントを指定する
環境変数の値に応じて、実物のS3を参照するかlocalstack の S3を参照するかを分岐するコードを書きます。
ただし、後述のように、 Productionのコードでこのやり方は良くないやり方です。
const AWS = require('aws-sdk');
// localstack
const config = {
endpoint: (process.env.IS_LOCAL_STACK === "true"? "http://localstack:4572": undefined),
s3ForcePathStyle: process.env.IS_LOCAL_STACK === "true",
}
const s3 = new AWS.S3(config);
const { S3_BUCKET } = process.env;
// handlerのコード
exports.lambdaHandler = async (event, context) => {
try {
const params = {
Bucket: S3_BUCKET,
Key: event.Key
};
const ret = await s3.getObject(params).promise();
const message = ret.Body.toString();
console.log(message);
} catch (err) {
console.log(err);
return err;
}
};
ビルド
sam build --use-container
sam local 実行
sam local 上に起動した lambda function を起動します
sam local invoke S3ReadFunction --docker-network b2fbba06747a --env-vars env.json --profile=localstack
手動で次のように入力します
{"Key": "message.txt"}<Enter>
<Ctrl-D>
...
2019-02-01T09:34:34.903Z 3d01ad34-1760-1c18-4042-4088ba5fffa1 Hi, there!
...
参考: 本番のコードにテストコードとの分岐ロジックを書かない
プロダクションのコードにテストコードとの分岐のロジックを書くのは良くないやり方です。
上記の例のように、S3 などの外部リソースは 依存性を外部から注入する やり方でテストを書きましょう[3]。
プロダクションのコードとテストコードでS3の実物とlocalstackのエンドポイントを渡すようにします。
本番のコード
module.exports = async ({s3, event, callback}) => {
const params = {
Bucket: event.Bucket,
Key: event.Key
};
const ret = await s3.getObject(params).promise();
const message = ret.Body.toString();
return message;
}
const AWS = require('aws-sdk');
const s3 = new AWS.S3();
const getS3Object = require('./service/getS3Object');
// handlerのコード
exports.lambdaHandler = async (event, context, callback) => {
try {
const message = await getS3Object({s3, event, callback});
console.log(message);
} catch (err) {
console.log(err);
return err;
}
};
テストコード
const AWS = require('aws-sdk');
const config = {
endpoint: 'http://localstack:4572',
s3ForcePathStyle: 'true',
}
const s3 = new AWS.S3(config); // use localstack-version's S3
const getS3Object = require('./service/getS3Object');
const assert = require('power-assert');
// event を作る
event = {
Bucket: 'test-bucket',
Key: 'message.txt'
}
// S3 bucket を作る
// ...(省略)...
// S3 object をコピーする
// ...(省略)...
// getS3Object のテスト
it ("you can get proper s3 object", async () => {
const message = await getS3Object({s3, event, callback});
assert(message === 'Hi, There');
});
// S3 object を削除する
// ...(省略)...
// S3 bucket を削除する
// ...(省略)...
終わりに
SAM Local と localstack を使って Docker Container 上に起動した lambda function から local stack 上に起動した S3 に疎通することができました。
ただ、結局のところ以下のような手順を毎回実行するので、開発サイクルの高速化はさほどのぞめませんでした(AWSコンソール上でソースコード修正 => テスト実行のほうがむしろ早いかも)。
- docker image のビルド (
sam build --use-container
) - docker container の起動 (
sam local invoke
)
CI化したときにS3などの外部リソースを含むテストをテスト環境で一貫して行えるのは良いと思いました。