docker-composeでlocalstackのコンテナを立てて、テスト環境用のAWSリソースを構築する(TypeScript)
#Docker
#docker-compose
#localstack
#TypeScript
◇概要
jestなどでユニットテストを行うとき、DynamoDBやS3,SSMparameterStoreなどのSDKを使用する場合実際のAWSアカウントにアクセスする場合課金がかかってしまうためAWSリソース部分をMockしたい。 そこでLocalStackというDocker上に偽物のAWSリソースを構築し、そこに通信させることでユニットテストに活用することができる
docker-compose.ymlサンプル
docker-compose.yml
version: '3.1' services: localstack: container_name: "${LOCALSTACK_DOCKER_NAME-localstack_main}" image: localstack/localstack network_mode: bridge environment: - SERVICES=${SERVICES- } ports: - "4566:4566" volumes: - "${TMPDIR:-/tmp/localstack}:/tmp/localstack" - "/var/run/docker.sock:/var/run/docker.sock"
.envrc(direnvを使用)
export TMPDIR=/private$TMPDIR export SERVICES=s3,ssm,dynamodb
docker-composeの環境変数設定について
・${VARIABLE:-default}はVARIABLEがセットされていないか空文字であるときに default として評価される。 ・${VARIABLE-default}はVARIABLEがセットされていないときのみ default として評価される。
引用: https://matsuand.github.io/docs.docker.jp.onthefly/compose/environment-variables/#the-env-file
SERVICESについて
SERVICESに
- SERVICES=s3,ssm,dynamodb
のように設定することでS3, SSM, DynamoDBのmockリソースをDocker上に建てることができる。
docker-composeコマンド
$ docker-compose -f <指定のディレクトリ>/docker-compose.yml up -d
DynamoDB利用サンプル
import { DynamoDBClient, CreateTableCommand, ListTablesCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, GetCommand, QueryCommand, QueryCommandInput } from "@aws-sdk/lib-dynamodb"; import { DynamodbTableOperation } from "../../../src/lambda/lib/DynamoDB/dynamoOperation"; import { createTableInput } from "./tables/createSampleTableInput"; const tableName = "sampleTable"; const dynamoClient = new DynamoDBClient({ region: "ap-northeast-1", endpoint: "http://localhost:4566" }); const ddbdc = DynamoDBDocumentClient.from(dynamoClient); describe("batchWriteRecords", () => { beforeAll(async () => { // テーブル作成 const createTableInput = createTableInput(tableName); await dynamoClient.send(new CreateTableCommand(createTableInput)); }); afterAll(async () => { // テーブルリスト const listTablesParams = {}; const listTablesResponse = await dynamoClient.send(new ListTablesCommand(listTablesParams)); // テーブル削除 const tableNames = listTablesResponse.TableNames; if (tableNames) { for (const table of tableNames) { await dynamoClient.send(new DeleteTableCommand({ TableName: table })); } } }); test("batchWriteRecords", async () => { // ここに好きなテストを書く }); });
/tables/createSampleTableInput
export const createSampleTableInput = (tableName: string) => { return { AttributeDefinitions: [ { AttributeName: "id", AttributeType: "S", }, { AttributeName: "meta", AttributeType: "S", }, { AttributeName: "timestamp", AttributeType: "N", }, ], KeySchema: [ { AttributeName: "id", KeyType: "HASH", }, { AttributeName: "meta", KeyType: "RANGE", }, ], GlobalSecondaryIndexes: [ { IndexName: "meta-timestamp-idx", KeySchema: [ { AttributeName: "meta", KeyType: "HASH", }, { AttributeName: "timestamp", KeyType: "RANGE", }, ], Projection: { ProjectionType: "ALL", }, ProvisionedThroughput: { ReadCapacityUnits: 0, WriteCapacityUnits: 0, }, }, ], BillingMode: "PAY_PER_REQUEST", TableName: tableName, }; };
package.json
{ "name": "sample", "version": "1.0.3", "license": "UNLICENSED", "bin": { "preprocess": "bin/preprocess.js" }, "scripts": { "build": "tsc", "watch": "tsc -w", "test": "jest --runInBand --config jest.config.js", "lint": "eslint 'lib/**/*.{js,ts}' 'src/**/*.ts'", "lint:fix": "eslint 'lib/**/*.{js,ts}' --fix" }, "devDependencies": { "@types/jest": "^26.0.10", "@types/node": "10.17.27", "@typescript-eslint/eslint-plugin": "^4.11.0", "@typescript-eslint/parser": "^4.11.0", "@typescript-eslint/types": "^4.11.0", "esbuild": "0", "eslint": "^7.16.0", "eslint-config-prettier": "^7.1.0", "eslint-formatter-friendly": "^7.0.0", "eslint-formatter-rdjson": "^1.0.3", "eslint-plugin-import": "^2.22.1", "eslint-plugin-jest": "^24.1.3", "jest": "^26.4.2", "prettier": "^2.2.1", "ts-jest": "^26.2.0", "ts-node": "^9.0.0", "typescript": "~3.9.7" }, "dependencies": { "@aws-sdk/client-dynamodb": "^3.49.0", "@aws-sdk/client-s3": "^3.49.0", "@aws-sdk/lib-dynamodb": "^3.49.0" } }
解説
import { DynamoDBClient, CreateTableCommand, ListTablesCommand, DeleteTableCommand } from "@aws-sdk/client-dynamodb"; import { DynamoDBDocumentClient, GetCommand, QueryCommand, QueryCommandInput } from "@aws-sdk/lib-dynamodb";
AWS SDKはv3を使用
const dynamoClient = new DynamoDBClient({ region: "ap-northeast-1", endpoint: "http://localhost:4566" });
endopointに「"http://localhost:4566"」と指定することでdockerのlocalstackに通信できる
beforeAll(async () => { // テーブル作成 const createTableInput = createTableInput(tableName); await dynamoClient.send(new CreateTableCommand(createTableInput)); }); afterAll(async () => { // テーブルリスト const listTablesParams = {}; const listTablesResponse = await dynamoClient.send(new ListTablesCommand(listTablesParams)); // テーブル削除 const tableNames = listTablesResponse.TableNames; if (tableNames) { for (const table of tableNames) { await dynamoClient.send(new DeleteTableCommand({ TableName: table })); } } });
beforeAllにてlocalstackでテーブル作成や初期データ投入を行う。 afterAllでテーブル削除などリソースの状態をリセットする。
S3利用サンプル
import { S3Client, CreateBucketCommand, PutObjectCommand, DeleteObjectCommand, DeleteBucketCommand, ListObjectsCommand} from "@aws-sdk/client-s3"; import * as path from "path"; const bucketName = "test-bucket-name"; const s3key = "testkey/Sample.csv"; const testFileName = "Sample.csv"; const s3Client = new S3Client({ region: "ap-northeast-1", forcePathStyle: true, endpoint: "http://localhost:4566", }); // LocalStackのS3リソース作成/削除に時間がかかるため追加 jest.setTimeout(10000); describe("s3 getObject", () => { beforeAll(async () => { try { await s3Client.send(new CreateBucketCommand({ Bucket: bucketName })); await s3Client.send(new PutObjectCommand({ Bucket: bucketName, Key: s3key, Body: fs.readFileSync(path.join(__dirname, "./testSample", testFileName)), })); } catch (e) { console.log(e); } }); afterAll(async () => { const deleteObjects = await s3Client.send(new ListObjectsCommand({ Bucket: bucketName })); if (deleteObjects.Contents) { for (const deleteObject of deleteObjects.Contents) { await s3Client.send(new DeleteObjectCommand({ Bucket: bucketName, Key: deleteObject.Key })); } } await s3Client.send(new DeleteBucketCommand({ Bucket: bucketName })); }); test("testSample", async () => { // ここに好きなテストを書く }); });