LoginSignup
23
31

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-02-01

概要

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などの外部リソースを含むテストをテスト環境で一貫して行えるのは良いと思いました。

参考資料

23
31
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
23
31