事業本部 Delivery 部のアベシです。
今回はLocalStackを用いたローカル環境でのテストについて書きました。
導入方法から簡単なlambda関数のテストを実行するところまでやってみましたので手順やコードをブログに残したいと思います。
はじめにLocalStackとは
LocalStackのDockerイメージを使用してローカル環境にAWSの擬似的なサービスを立ち上げることができます。 テストの際に実際のAWSサービスにリソースをデプロイすること無く、AWSの挙動を確かめる事ができます。 AWSにデプロイしてE2Eテストを行う場合に比べて以下の恩恵を受けられると考えます。
- 実際のAWSにデプロイするテストと比べて速やかにテストできる
- AWSを使用しないので利用費が発生が無い。
- コンテナを立ち下げる事ですぐに環境をリセットできる
前提
以下のソフトのインストールが完了していることを前提とします。
- Docker
- Docker Compose
注意:Docker Composeのバージョンは1.9.0以上が必要です。
実行環境
実行時に使用したソフトウェア等のバージョンは以下の通りです。
項目名 | バージョン |
---|---|
mac os | Ventura 13.2 |
Docker | 20.10.20 |
Docker Compose | 2.14.0 |
jest | 29.5.0 |
AWS SDK for JavaScript | v3 |
LocalStackをローカル環境で実行する方法
LocalStackを実行する方法は以下の4通りあります
- LocalStack CLI
- Docker
- Docker Compose
- Kubernetes
今回は一番簡単そうな方法のDocker ComposeでLocalStackを実行する方法を紹介致します。
Docker Composeを使ってLocalStackを実行
LocalStackを起動するにはdocker-compose.ymlに起動設定を記載してコンテナを立ち上げます。
プロジェクトの作成とdocker-compose.ymlの作成
まずはプロジェクトの作成と、LocalStackをDocker上で起動するための設定を記載するファイルdocker-compose.yml
を作成していきます。
$ mkdir localstack-test
$ cd localstack-test
$ touch docker-compose.yml
docker-compose.yml
の書き方については公式に公開されているリファレンスが以下のリポジトリにありますので参考にしました。
docker-compose.ymlの内容
先程のリファレンスを参考にして記述したymlファイルが以下です。
docker-compose.yml
version: "3.8"
services:
localstack:
image: localstack/localstack:2.0.1
ports:
- "127.0.0.1:4566:4566"
environment:
- DEBUG=1
- DOCKER_HOST=unix:///var/run/docker.sock
1 書式のバージョンの設定
version: "3.8"
今使える最新の3.8を使います。
Docker Enginのバージョンが19.03.0
以上であることが使用条件となっています。
2 使用するLocalStackのイメージの指定
image: localstack/localstack:2.0.1
今回は現時点で一番新しいlocalstack/localstack:2.0.1
を指定しています。
イメージは以下のリンク先のDockerHubから確認できます。
3 ポートの指定
ports:
- "127.0.0.1:4566:4566"
リファレンスに記載の通りに指定します。
4 環境変数の指定
environment:
- DEBUG=1
- DOCKER_HOST=unix:///var/run/docker.sock
DEBUG
はLocalStackのログを出力するかどうかを指定する環境変数です。1
を指定するとログをリアルタイムに出力してくれます。DOCKER_HOST
はDocker Composeで起動されるコンテナがDockerデーモンと通信するために使用するDockerホストのアドレスを指定する環境変数です。
テスト対象のLambda関数
以下のLambda関数に対してテストを行います。
S3にGetObjectを実行して、バケットに保存されているJSONファイルを取得して返却する処理です。
sample-localstack.ts
import { S3Client, GetObjectCommand } from '@aws-sdk/client-s3';
interface Event {
key: string;
}
interface Response {
prefecture_code: string;
prefecture: string;
}
export const handler = async (event: Event): Promise<Response> => {
const awsConfig = {
endpoint: process.env.S3_ENDPOINT_LOCALSTACK || '',
region: 'ap-northeast-1',
};
const s3Client = new S3Client(awsConfig);
try {
const response = await s3Client.send(
new GetObjectCommand({
Bucket: process.env.BUCKET_NAME,
Key: event.key,
}),
);
const stringResponse = await response.Body?.transformToString();
const objectResponse = JSON.parse(stringResponse as string);
return objectResponse;
} catch (error) {
console.error(error);
throw error;
}
};
コードについて
使用しているSDKはAWS SDK for JavaScript v3です。
v3のGetObjectの戻り値はReadableStream
となっていて、transformToString()
という便利なメソッドを使用して文字列に変換しています。
S3Client
をnewする時に引数に渡すendpoint
は、テスト時はdotenv
の環境変数を使用します。
渡しているURLは以下です。
https://s3.localhost.localstack.cloud:4566
テストコード
テストコードは以下のような内容となっています。
テストフレームワークにはjest
を使用しています。
sample-localstack.test.ts
import {
S3Client,
CreateBucketCommand,
PutObjectCommand,
DeleteBucketCommand,
DeleteObjectCommand,
} from '@aws-sdk/client-s3';
import * as fs from 'fs';
import * as path from 'path';
import { handler } from '../../../src/lambda/handlers/sample-localstack.test.ts';
const awsConfig = {
endpoint: 'https://s3.localhost.localstack.cloud:4566',
region: 'ap-northeast-1',
};
const s3Client = new S3Client(awsConfig);
describe('正常系', () => {
const event = {
key: 'sample/value1',
};
beforeAll(async () => {
await s3Client.send(
new CreateBucketCommand({ Bucket: 'sample-get-object-from-s3' }),
);
await s3Client.send(
new PutObjectCommand({
Body: fs.readFileSync(path.join(__dirname, 'test-data.json')),
Bucket: 'sample-get-object-from-s3',
Key: 'sample/value1',
}),
);
});
afterAll(async () => {
await s3Client.send(
new DeleteObjectCommand({
Bucket: 'sample-get-object-from-s3',
Key: 'sample/value1',
}),
);
await s3Client.send(
new DeleteBucketCommand({ Bucket: 'sample-get-object-from-s3' }),
);
});
test('テストデータのオブジェクトを返却する事を確認', async () => {
const response = await handler(event);
expect(response).toHaveProperty('prefecture_code');
expect(response.prefecture_code).toBe('001');
expect(response).toHaveProperty('prefecture');
expect(response.prefecture).toBe('北海道');
});
});
コードについて
S3バケットとオブジェクトのセットアップ、クリーンアップ
beforeAllとafterAllで、テスト用バケットとオブジェクトのセットアップとクリーンアップを行っています。
beforeAll
でS3にバケットを作成し、テストデータを保存しています。afterAll
でバケットとオブジェクトを削除しています。
LocalStack上のS3のエンドポイント
クラスnew S3Client()
のインスタンス化の際に渡すendpoint
は、LocalStackのS3用のエンドポイントを指定しています。
以下のURLの場合、LocalStackのS3に対して仮想ホスト形式
のURLでアクセスするようになります。
'https://s3.localhost.localstack.cloud:4566';
これに対して、以下のURLを指定した場合はLocalStackのS3にパス形式
でアクセスするようになります。
'http://localhost:4566';
しかしAWS SDK for JavaScript v3は仮想ホスト形式
で接続する挙動をとるので、このままではS3に接続できません。
こちらには回避方法があり、forcePathStyle: true
を設定する事でパス形式
を強制してS3に接続できます。
テストデータ
test-data.json
{
"prefecture_code": "001",
"prefecture": "北海道"
}
テスト実行
テストの実行前にLocalStackのイメージを使ってコンテナを立ち上げます。
Docker Composeを使用すると、テスト前の準備はこのコマンドでコンテナを立ち上げるだけなので非常に楽に思えます。
以下のコマンドを実行します。
$ docker-compose up
テストを実行します。
npx dotenv -e ./test/environment/.env jest test/unit/handlers/sample-localstack.test.ts
テスト結果
テストパスしました。
PASS test/unit/handlers/sample-localstack.test.ts
正常系
✓ テストデータのオブジェクトを返却する事を確認 (73 ms)
Test Suites: 1 passed, 1 total
Tests: 1 passed, 1 total
Snapshots: 0 total
Time: 4.8 s, estimated 5 s
Ran all test suites matching /test\/unit\/handlers\/sample-localstack.test.ts/i.
コンテナの後始末
テストが終わったら、以下のコマンドでコンテナを停止、削除します。
$ docker-compose down
コンテナが停止しても前回の設定が残ってしまい、次回起動時にうまくコンテナの設定が反映されない時があるので以下のコマンドでコンテナを削除します。
$ docker rm $(docker ps -q -a)
以下のコマンドでコンテナが無いことを確かめます。
$ docker ps -q -a