Jestを通してCircleCIの基本を確認してみる

2020.05.19

CIツールの利用は現代の開発においては必要不可欠なものであり, とりわけSaaSを利用すると設定ファイルの準備を行う程度の準備でパイプラインが構築できます. そしてCircleCIは一つのCIツールであり非常に便利です.
今回はJestで書いた簡単なテストを通じて設定を書きCircleCIの基本コンポーネントを理解できればと思っています.

基本コンポーネントについて

alt

手を動かす前にCircleCIの基本コンポーネントについて理解を深めていきます.
最新バージョンである2.1では大きくわけて3つのコンポーネントに分かれています. なのでまずは上の図にある3つのコンポーネントを追っていきましょう.

Workflows

CircleCIで実行する一番大きな単位です. ジョブの実行を制御するために利用されます.
例えばジョブを並行で実行したり、実行するジョブの条件を決定したりします.
この段階ではまだわかりづらいかもしれませんが実際に設定を書いて, 試していく中で把握できるとおもうのでまずはジョブの実行を制御するコンポーネントと認識してもらえば大丈夫です.
例えばですが, アプリケーションのパイプラインで下記のような用件があったとします.

  • 依存関係のインストールは一番最初に行いたい
  • テストはユニット、結合、E2Eテストを並行で実行したい
  • デプロイ処理は全てのテストが終わった後に実行したい

この場合は下記の図のように1つのワークフローでジョブの実行を制御することができます.

  • ビルドが終わったら全てのテストを並列で実行する
  • デプロイ処理は全てのテストが成功した場合に実行する

alt

Jobs

ジョブはステップの集まりであり, ジョブは1つのコンテナで実行されます.
そしてジョブでは下記の内容を主に定義します. またステップの実行条件の分岐とか細かい指定もまだまだあります.

  • ジョブの名前
  • 利用するDocker Image
  • ステップ一覧

つまり, 1つのジョブでは1つのコンテナが実行されそのコンテナの中で定義したステップが実行されています.
またジョブごとに利用するDocker Imageの指定を行うので, ジョブとジョブはコンテナのアイソレーションのために分離されています.
そのため先ほどのワークフローの図にあったように, ジョブを逐次で実行することも, 並行で実行することもできるのです.

Steps

大まかに言うとジョブの中で実行されるコマンドのことをステップと呼びます.
任意のシェルスクリプトの実行とコードをチェックアウトするcheckoutやキャッシュ, アーティファクト, デプロイなど様々なビルトインのステップがあります.
ステップについては実行するコマンドを書いていくという認識であればほとんど合っていると思います.

設定の記述とプロジェクトの準備

CircleCIの基本概念を理解したところで実際に設定ファイルを用意してワークフローを動かします.
概念の理解を頭でするだけでなく実際に手を動かした方が理解が捗りますよね. 手を動かして分からないことが出たら再度基本概念に戻ったり公式ドキュメントを眺めてみてください.

今回は非常に簡単にJestでテストを書いてパイプラインを実行していきます.

事前準備

まずはCircleCIでワークフローを実行する前にプロジェクトの設定をしていきます.Jestが動く最低限の設定をしていきます.

$ git int
$ yarn init -y
$ yarn add -D jest
$ curl -L -s https://www.gitignore.io/api/node > .gitignore

設定の記述とプロジェクトの準備

CircleCIの基本概念を理解したところで実際に設定ファイルを用意してワークフローを動かします.
概念の理解を頭でするだけでなく実際に手を動かした方が理解が捗りますよね. 手を動かして分からないことが出たら再度基本概念に戻ったり公式ドキュメントを眺めてみてください.

今回は非常に簡単にJestでテストを書いてパイプラインを実行していきます.

事前準備

まずはCircleCIでワークフローを実行する前にプロジェクトの設定をしていきます.Jestが動く最低限の設定をしていきます.

$ git int
$ yarn init -y
$ yarn add -D jest
$ curl -L -s https://www.gitignore.io/api/node > .gitignore

次にnpm scriptsに対してtestを追加します.

package.json

{
  "name": "testing_with_jest",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "jest"
  },
  "license": "MIT",
  "devDependencies": {
    "jest": "^26.0.1"
  }
}

とりあえず通るテストを書きます. 今回はCircleCIの動作がメインなのでテスト内容については踏み込みません. 何もしてないので踏み込めませんが...

test('it works because nothing i do', () => {})

最後にこのテストが問題なく実行されることを確認します. 先ほど書いたスクリプトでテストが対象とされ, 実行されるかを確認します.

yarn test
yarn run v1.22.4
$ jest
PASS  __tests__/sample.test.js
  ✓ it works because nothing i do (1 ms)

Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        1.365 s
Ran all test suites.
✨  Done in 4.39s.

問題なさそうですね. これでプロジェクトの準備ができたのでCircleCIの設定ファイルを書いていきます.

CircleCIの設定ファイルの作成

設定ファイルはプロジェクトルートに「.circleci/config.yml」を作成して記載していきます.
一番先頭に利用するCircleCIのバージョン, 今回は2.1を利用したいので2.1を指定します. その次からが実際の設定になっていきます. 設定の順序が先ほどの説明順序と異なりジョブを定義した後にワークフローを定義していきます. すこしこんがらがりそうですが頑張りましょう.
ジョブに対して名前を付けたら, 利用するDocker Imageとステップを定義していきます.
buildジョブのステップの中身はコードベースをチェックアウトして, 依存関係を取得するだけで, testジョブではそれに追加して最後にテストを実行しています. またステップの中のrunにはインデントが2つ必要です. ちょっと深めのネストでYAMLでのインデントエラーで稀によくあることなので注意しましょう.

.circleci/config.yml

version: 2.1
jobs:
  build:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: install dependencies
          command: yarn install
  test:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - run:
          name: install dependencies
          command: yarn install
      - run:
          name: unit test
          command: yarn test
workflows:
  version: 2
  build_and_test:
    jobs:
      - build
      - test

今回のような記載ではコンテナの同時実行数の制限がかからない限りはワークフローの中でジョブは並行実行されます. Freeプランの場合は同時実行数が1のため, 一つづつジョブが実行されます.

またCircleCIのセットアップが済んでいないため手順が逆転してしまいますが, buildジョブで「yarn install」を実行しているから, testジョブから「yarn install」を抜いてみたりしてください.
これをした場合にはtestジョブが失敗します. 理由は単純で, buildジョブとtestジョブで別のコンテナを利用しているため, testジョブのコンテナにnode_modulesが存在しないため失敗します.
非常に小さなワークフローではありますが, CircleCIの基本概要を理解するためには十分役立つと思っています.

CircleCI のセットアップ

次にCircleCI側の設定を行っていきます. この先記載することは以下のことを前提として書いています. 実施できていない場合は先に対応してください.

  • GitHub 側でリポジトリを作成している
  • CircleCIのアカウントを取得している

前提を伝えたところで, CircleCIにアクセスしてプロジェクト一覧を表示させてみましょう.
下記のような画面で, GitHubのリポジトリ一覧が表示されていると思います. ですので「Set Up Project」をクリックしてプロジェクトを作成していきます.

alt

次の画面に遷移したら, 「Start Building」をクリックし, ポップアップに出てくる「Add Manualliy」をクリックしてセットアップは完了です.

alt

ここまでできたら, GitHubに変更をpushしてワークフローの実行を眺めてみましょう.

alt

だいぶ実行に時間がかかっていますが, 無事ワークフローが実行できましたね.

キャッシュの活用

先ほどのワークフローではbuiltとtestの2つのジョブを実行していました.
またワークフローの中でジョブは別のコンテナで実行されることもお伝えしました.
実行環境が分かれているためbuildジョブとtestジョブで依存関係を都度取得していますね.
今回は軽い処理が2つだけのため特段遅くはないですジョブが増えたらCIの実行時間がいたずらに増えてしまいます.

CircleCIではキャッシュをサポートしておりジョブ間でデータを受け渡すことが可能です.
つまり依存関係つまりnode_modulesをキャッシュに投入し, 各ジョブの先頭でキャッシュを取り出してから処理を開始することにします.
なのでキャッシュを利用した処理が下記のようになります.

  • キャッシュにnode_modulesがない場合は通常通り依存関係をダウンロードする
  • キャッシュにnode_modulesああり, 取得できる場合はキャッシュから取得する(npmからのダウンロードが発生しない)

やることが把握できたところで実際に設定を書いていきましょう. 変更部分はキャッシュのところだけです.

version: 2.1
jobs:
  build:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{ checksum "yarn.lock" }}
      - run:
          name: install dependencies
          command: yarn install
      - save_cache:
          key:  dependency-cache-{{ checksum "yarn.lock" }}
          paths:
            - ./node_modules
  test:
    docker:
      - image: circleci/node:12
    steps:
      - checkout
      - restore_cache:
          key: dependency-cache-{{ checksum "yarn.lock" }}
      - run:
          name: install dependencies
          command: yarn install
      - run:
          name: unit test
          command: yarn test
workflows:
  version: 2
  build_and_test:
    jobs:
      - build
      - test

この記事を執筆する前段階でいろいろと試しておりbuildジョブでもキャッシュが利用されている状況ではあるのですが, だいぶ処理が高速になります. キャッシュを利用する前が45秒かかっているのに対して今回は25秒で処理が完了しています. 依存関係のインストールが大半の処理を占めているのでだいぶ良いスコアではありますが, キャッシュの重要性について理解していただけましたでしょうか.

alt

CommandsとExecutorで冗長な処理を省く

キャッシュの設定を記載したおかげでだいぶビルド時間の高速化ができました.
ですがキャッシュの設定を書いたために大幅に可読性が落ちまた同じステップを複数書いているため冗長になっています.
そこで最後にExecutorsとCommandsを指定していきたいと思います.

Executorsは実行環境を定義してくれるようなモノです.
今までは各々のジョブで実行環境をDockerでかつNode.jsのイメージを利用するように指定していましたが, Executorsではそういった環境をExecutorとして定義することができます.
Executorsを定義した上で, 各々ジョブからExecutorを指定すればDRYな設定が実現できます.

次にCommandsはステップの中身を独自定義してCircleCIのビルトインである, 「checkin」や「restore_cache」のような形で呼び出せるようにしてくれます.
やはり言葉で伝わらない思いはコードにのせるのが一番なので実際の設定ファイルをみてみましょう.

ジョブで定義していた部分がexecutorsとcommands部分に切り離すことができ, 冗長な記述がなくなりかつ可読性もあがりましたね.

version: 2.1
executors:
  node:
    docker:
      - image: circleci/node:12
commands:
  restore_modules:
    steps:
      - restore_cache:
          key: dependency-cache-{{ checksum "yarn.lock" }}
  save_modules:
    steps:
      - save_cache:
          key:  dependency-cache-{{ checksum "yarn.lock" }}
          paths:
            - ./node_modules

jobs:
  build:
    executor: node
    steps:
      - checkout
      - restore_modules
      - run:
          name: install dependencies
          command: yarn install
      - save_modules
  test:
    executor: node
    steps:
      - checkout
      - restore_modules
      - run:
          name: install dependencies
          command: yarn install
      - run:
          name: unit test
          command: yarn test
workflows:
  version: 2
  build_and_test:
    jobs:
      - build
      - test

ワークフローの結果自体は何も代わり映えしないので今回はのせないです.

さいごに

今回の記事では下記のことを確認してきました.

  • CircleCIの基本コンポーネントと設定ファイルの書き方について
  • キャッシュを利用してビルドを高速にする
  • ExecutorsとCommandsでDRYな設定をかく

もし理解しきれなかったところは実際に手を動かしつつ, ドキュメントを読むのがおすすめですので是非ご確認ください.
この記事が役に立ちましたら幸いです.

参考資料