Edited at

ローカル環境でLambda+S3のテストをする


概要

sam local & localstack を使ってローカルでLambdaのテストする環境を構築したときの手順とハマったところをまとめたものです。おもに[2]を参考にして構築しました。

SAMのリソースの作成方法についてはここではあまり触れていません。


: SAM local で Lambda を実行するよりも、local から直接テストを実行するほうが開発サイクルが早いです。これから Lambda Function のテストの実装を考えている方はこちらの記事を参照してください。



構築の要件


  • Lambda Function の中で S3 を参照している部分をローカル環境でテストしたい


    • テストサイクルが早くなることを期待




Stack


  • AWS SAM


    • SAM CLI, version 0.10.0



  • localstack


手順


必要な資材のインストール

公式ページの説明にしたがってインストールします。


  • aws-sam-cli

  • docker for Mac


localstack の起動


docker-compose.yml

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]。


~/.aws/credentials

[localstack]

aws_access_key_id = dummy
aws_secret_access_key = dummy


~/.aws/config

[profile localstack]

region = us-east-1
output = text


Localstack上の S3 に Bucket/Object を作成する


message.txt

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テンプレートに環境変数が指定されていないと有効になりません。


env.json

{

"S3ReadFunction": {
"IS_LOCAL_STACK" = "true",
"S3_BUCKET": "test-bucket"
}
}


template.yaml

Globals:

Function:
Environment:
Variables:
IS_LOCAL_STACK: 'false'
S3_BUCKET: 'production-bucket'


S3のエンドポイントに localstack のエンドポイントを指定する

環境変数の値に応じて、実物のS3を参照するかlocalstack の S3を参照するかを分岐するコードを書きます。

ただし、後述のように、 Productionのコードでこのやり方は良くないやり方です


app.js

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のエンドポイントを渡すようにします。


本番のコード


service/getS3Object.js


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;
}


app.js

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;
}
};



テストコード


test/getS3ObjectTest.js

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コンソール上でソースコード修正 => テスト実行のほうがむしろ早いかも)。


  1. docker image のビルド (sam build --use-container)

  2. docker container の起動 (sam local invoke)

CI化したときにS3などの外部リソースを含むテストをテスト環境で一貫して行えるのは良いと思いました。


参考資料