LocalStackを使ったテストをCircleCIのCI/CDで実行してみた

2023.04.20

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

CX事業本部 Delivery 部のアベシです。
先日投稿したこちらのブログでLocalStackを使ったテストを紹介しました。

今回はこちらのテストをCircleCIのCI/CDの過程でできるようにしてみましたので、その際の設定や手順をご紹介したいと思います。

大まかな流れ

以下の流れでやっていきます。

  1. プロジェクトにCircleCIの設定ファイル(.circleci/config.yml)を作成する
  2. リポジトリをCircleCIの管理下に設定する
  3. ダミーのAWSクレデンシャルをCircleCIの環境変数に登録する
  4. 変更をGitHubにプッシュしてCI/CDの結果がSucceessとなればOK

コード

ソースコード

テスト対象のLambda関数のソースコードの処理は、S3にGetObjectを実行し、バケットに保存されているJSONファイルを取得して返却する、です。
今回使用するテスト対象のLambda関数及びテストコードは上に載せたブログで紹介したものと同じですので説明は省きます。折りたたんだものを下に掲載します。

こちら開くとコードが確認できます。

src/lambda/handlers/get-object-to-s3.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: "sample-get-object-to-s3",
        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;
  }
};

テストコード

テストコードも前回のブログで紹介したものと同じですので説明は省きます。ソースコードと同じように折りたたんだものを下に掲載します。

こちら開くとコードが確認できます。

test/unit/handlers/get-object-to-s3.test.ts

process.env.S3_ENDPOINT_LOCALSTACK = 'https://s3.localhost.localstack.cloud:4566';
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/get-object-to-s3';

const s3EndpointLocalStack = 'https://s3.localhost.localstack.cloud:4566';

const awsConfig = {
  endpoint: s3EndpointLocalStack,
  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-to-s3' }),
    );
    await s3Client.send(
      new PutObjectCommand({
        Body: fs.readFileSync(path.join(__dirname, 'test-data.json')),
        Bucket: 'sample-get-object-to-s3',
        Key: 'sample/value1',
      }),
    );
  });
  afterAll(async () => {
    await s3Client.send(
      new DeleteObjectCommand({
        Bucket: 'sample-get-object-to-s3',
        Key: 'sample/value1',
      }),
    );
    await s3Client.send(
      new DeleteBucketCommand({ Bucket: 'sample-get-object-to-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('北海道');
  });
});

.circleci/config.ymlファイルの作成

config.ymlファイルの内容は以下のようになります。
CircleCIでのCI/CDの内容や環境設定などはこのファイルに記載します。

.circleci/config.yml

version: 2.1

jobs:
  test_and_build:
    docker:
      - image: cimg/node:18.16.0
      - image: localstack/localstack:2.0.1
    steps:
      - checkout
      - restore_cache:
          key: v1-dependencies-{{ .Branch }}-{{ checksum "package.json" }}-{{ checksum "package-lock.json" }}
      - run:
          name: Install npm dependencies
          command: |
            set -x
            [[ -d node_modules ]] || npm ci
      - save_cache:
          paths:
            - node_modules
          key: v1-dependencies-{{ .Branch }}-{{ checksum "package.json" }}-{{ checksum "package-lock.json" }}
      - run:
          name: Test unit
          command: |
            set -x
            npm run unit-test

workflows:
  test_workflows:
    jobs:
      - test_and_build:
          filters:
            branches:
              only:
                - /.*/

config.ymlファイルの解説

config.ymlのバージョンは今使える最新の2.1を使用しています。

jobs:

jobs:にビルドとテストの内容を記載しています。

docker:

jobs:配下のdocker:にはテストとビルドに使用する実行環境イメージを設定しています。

  • Node.JS実行用のイメージ
    • cimg/node:18.16.0を使用しています。
      cimg/nodeCircleCIが提供する公式のDockerイメージでNode.jsを実行するためのイメージです。 今回LambdaのランタイムがNode.js18なので、イメージもNode.js18が使える18.16.0を指定しています。
  • LocalStack実行用のイメージ
    • localstack/localstack:2.0.1を使用しています。localstack/localstacklocalstackが提供する公式のDockerイメージです。今回なるべく新しいものを使用しています。

steps:

steps:には依存関係のインストールやテストとビルドのコマンドを記載します。

checkout:

checkoutではGitHubからコードをgit cloneで取得し、対象のブランチにスイッチしています。

restore_cache:

restore_cacheでは前回のCI/CDでキャッシュしたnode_moduleを復元する工程になります。
保存されているkey:と比べて値が変化していれば復元します。
key:にはpackage.jsonpackage-lock.jsonのハッシュ値を指定しています。 インストールしたパッケージに変更が有るとこのハッシュ値が変化します。変化が有る場合はnode_moduleも変化しているのでキャッシュしたnode_moduleは復元せずに次のステップでパッケージをnode_moduleにインストールします。
restoreの工程を入れることで、新たなパッケージのインストールが無い場合はパッケージのインストールがスキップされてCI/CDの実行にかかる時間を短縮できます。

Install npm dependencies

次のrun:ではnpm ciコマンドを実行しています。npm cipackage-lock.jsonを元にパッケージをnode_moduleにインストールします。
もしnode_moduleを復元している場合は、[[ -d node_modules ]] || npm ciの評価によってnpm ciは実行されません。[[ -d node_modules ]]はnode_modulesディレクトリが存在する場合はtrueを返し、存在しない場合はfalseを返して ||の後のnpm ciが実行されます。

save_cache:

save_cache:node_moduleをキャッシュする工程です。 もしkey:のハッシュ値に変化がなければこの工程はスキップされます。

workflows:

workflows:には実行するjobを記載します。先程説明したtest_and_buildを指定します

filters:

filters:にはjobを実行する対象のブランチを指定します。指定したブランチに対してプッシュやマージがあった場合にjobが実行されます。
今回はすべてのブランチを対象にする、という意味で/.*/を指定しています。

リポジトリをCircleCIの管理下に設定する

CircleCIのダッシュボードからGitHubのリポジトリをFollowします。
まずは左ページのProjectsを開きます。
CircleCIの管理下に置きたいリポジトリのSet Up Projectをクリックします。
出てきたモーダルでconfig.ymlを選択します。今回は既にconfig.ymlを作成してプッシュしている状況を想定しますので、 一番上のuse the .circleci/config.ymlを選択します。
最後にSet Up Projectをクリックして完了です。

ダミーのAWSクレデンシャルをCircleCIの環境変数に登録する

LocalStackではダミーの値で良いのですがAWSのクレデンシャルの情報が必要なので、CircleCIの環境変数に登録します。
左ペインのprojectをクリックして、対象のリポジトリの3点メニューを開きます。
メニューのproject settingsをクリックします。
リポジトリの設定画面に遷移するのでEnvironment Variablesをクリックします。
切り替わった画面でAdd Environment Variable から変数を登録します。
以下の2つを登録します。

  • AWS_ACCESS_KEY_ID : dummy
  • AWS_SECRET_ACCESS_KEY : dummy これらのクレデンシャルを登録しないでCI/CDを実行すると以下のようなエラーが発生します。
● Test suite failed to run
    CredentialsProviderError: Could not load credentials from any providers

動作確認

適当に変更加えてプッシュします。 以下のようにSuccessというステータスが表示されていばjobがすべて成功となります。

補足

Container localstack/localstack:2.0.1のjobが灰色丸に横棒のステータスとなり、エラーでも成功でもない結果となって、ログにBuild was canceledと出力されています。こちらの記事のCircleCIからの解答によると、テストjobの完了までこのjobは動き続け、テスト完了と共にBuild was canceledと出力されるとの事で、特に問題のある動きではないようです。

以上