SmartHR Tech Blog

SmartHR 開発者ブログ

CircleCI 2.0 に移行しました

こんにちは。SmartHR SRE チームの吉成です。 昨日、CircleCI 2.0 が正式リリースされましたね! 弊社では現在、CI 環境として CircleCI を利用しており、その上で RuboCop や RSpec、npm を利用したフロントエンドのテストから AWS 上の環境に対するデプロイまでを行っています。 β版のため移行をためらっていたのですが、待ちに待った正式版ですので、早速移行しました。

1.0 と 2.0 の違い

はじめに、 2.0 になって何が変わったのかをざっくりと紹介します。

実行環境の違い

1.0 では予め用意された Ubuntu 環境上でビルドが実行されていました。そのため、依存している環境とのバージョン違いや設定ファイルの書き換えが必要でした。 それが 2.0 になり、Docker コンテナの上でテストを実行するようになりました。予め依存関係を解決したコンテナを用意してやれば、バージョン違いの解決や依存関係の再インストールが不要になるので、テスト開始までの時間が短くなります。 弊社でも MySQL のバージョンアップや AWS コマンドのインストールなどが行われていたので、そのあたりの準備時間がなくなりました。

最新の Docker

1.0 でも Docker を使うことは使えましたが、CircleCI 独自にカスタムされた古いバージョンの Docker に固定されていました。そのため、新しい Docker の機能を使おうと思っても使えないなどの面倒がありました。 2.0 では仮想マシン上で純粋な Docker を動かせるようになったため、自分でバージョンアップして最新の Docker を使えるようになりました。 弊社でも 1.0 のままでは Staging 環境や Production 環境の Dockerize に向けて書いていた Dockerfile が動かせなかったのですが、2.0 に移行して実行できるようになりました。

Workflow という概念の登場

2.0 になり、 Workflow という概念が登場しました。 ビルド内での処理を分割して管理でき、実行順序の依存が解決可能になっています。これにより、実行順序が依存しないもの同士を並列実行するなどの設定ができるようになります。 弊社でもまだ導入はできていないのですが、将来的には導入して Bundler や npm による依存関係のインストールや RuboCop と RSpec によるテストなど、並列実行できるものをきちんと管理してビルドの高速化を図りたいと考えています。 他にも様々な使い方ができるようですので、詳しく知りたい方は公式のドキュメントをご確認ください。

移行前

移行の説明にあたり、まず弊社が CircleCI 上で行っていることを紹介します。

  • 依存関係のインストール
  • Rails のテストで利用するため、 Bundler をアップデートします
  • フロントエンドのテストで利用するため、node や npm をインストールしています
  • デプロイで利用するため、 awsebcli をインストールしています
  • MySQL のインストールと起動
  • デフォルトでインストールされている MySQL のバージョンが古いため、再度インストールしています
  • utf8mb4 を利用するために、設定ファイルを外部から取得して再起動しています
  • RuboCop によるコーディングスタイルのチェック
  • npm によるフロントエンドのテスト
  • カバレッジ計測
  • コーディングスタイルのチェック
  • テスト
  • RSpec によるテスト
  • Elastic Beanstalk 環境へのデプロイ
  • テストサマリの保存
  • Coveralls への通知

以上が弊社が CircleCI 上で行っている事になります。

これを設定ファイルとして書き出すと、以下のようになります。

general:
  artifacts:
    - テストサマリの保存

machine:
  - 実行環境の設定

dependencies:
  - 依存関係のインストール
  - RuboCop によるコーディングスタイルのチェック
  - npm によるフロントエンドのテスト

database:
  pre:
  - MySQL のインストールと起動

test:
  override:
    - RSpec によるテスト

notify:
  webhooks:
    - url: YOUR_COVERALLS_WEBHOOK_URL

deployment:
  DEPLOYMENT_NAME:
    branch: YOUR_BRANCH
    commands:
      - ./deploy.sh

移行作業

上述の設定ファイルを元に、2.0 に移行していきます。 移行自体は Migrating from 1.0 to 2.0CircleCI 2.0 Docs を参考にしながら行いました。

ビルドの実行用イメージを用意する

2.0 ではテストに利用する Docker イメージを指定し、その中でビルドが行われます。Docker イメージは公式に用意されているものの他にも、DockerHub などで公開されているものを自由に利用できます。 しかし、弊社では ruby のバージョンや node/npm のバージョンのバージョン依存、aws-cli や docker-deploy などのコマンド依存が必要だったため、smarthr/circleci-base としてバージョンを揃え、必要なコマンドをインストールしたイメージを用意しました。 このイメージはベースとして Ruby を利用しており、Rails の実行コンテナとなるような考えのもと用意してあります。

新しい設定ファイルを作成する

1.0 ではプロジェクトのルートディレクトリに circle.yml というファイルを用意していたかと思いますが、2.0 では .circleci/config.yml にファイルを用意する必要があります。

ここで注意が必要なのですが、 1.0 と 2.0 では設定ファイルに互換性がありません。つまり、移行するためには 1.0 の設定ファイルを参考に一から書く必要があります。 また、1.0 では CircleCI が管理している処理の流れを適宜 override していくスタイルで設定ファイルを記述しましたが、 2.0 では全ての処理を自分で書く必要があります

ちなみに、circle.yml.circleci/config.yml は両方共リポジトリに含めてしまっても問題ありません。その場合、2.0 が優先して利用されます。

さて、上述の処理を 2.0 の設定ファイルで記述すると、以下のようになります。 長くなってしまうため省略していますが、もう少し詳細なもの も用意してあります。こちらには circleci globcircleci tests を利用した RuboCop、 RSpec のテスト並列化も記載されていますので、興味があれば参考にしてみてください。

version: 2

jobs:
  build:
    working_directory: /app
  docker:
    - image: smarthr/circleci-base
      environment:
        RAILS_ENV: test
        DB_HOST: 127.0.0.1
        REDIS_HOST: 127.0.0.1
        TZ: /usr/share/zoneinfo/Asia/Tokyo
        CIRCLE_TEST_REPORTS: /tmp/test-results
    - image: mysql:5.7
      command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_bin --innodb-large-prefix=true --innodb-file-format=Barracuda
      environment:
        MYSQL_USER: root
        MYSQL_ALLOW_EMPTY_PASSWORD: yes
    - image: redis
    steps:
      - checkout

      - restore_cache:
          name: 依存関係をキャッシュから取得
          keys:
            - YOUR_CACHED_KEY

      - run:
          name: 依存関係のインストール
          command: YOUR_INSTALL_COMMAND

      - save_cache:
          name: 依存関係をキャッシュに保存
          key: YOUR_CACHED_KEY
          paths:
            - YOUR_CACHED_PATH

      - run:
          name: RuboCop によるコーディングスタイルのチェック
          command: bundle exec rubocop

      - run:
          name: npm によるフロントエンドのテスト
          command: npm run test

      - run:
          name: DB の初期化
          command: |
            bundle exec rake db:create
            bundle exec rake db:schema:load

      - run:
          name: RSpec によるテスト
          command: bundle exec rspec

      - deploy:
          command: ./deploy.sh

      - store_test_results:
          path: /tmp/test-results

notify:
  webhooks:
    - url: YOUR_COVERALLS_WEBHOOK_URL

改善できた点

依存関係の設定

1.0 時代にはフロントエンドテストの実行やデプロイのために npm のバージョンを指定したインストール、aws-cli のインストールが行われていましたが、これを全て用意した Docker イメージを用意しました。 毎回のインストール作業がなくなるので、ビルド開始までの準備時間が短くなりました。

DB の設定

1.0 の時代にはテストで利用するための MySQL を毎回用意していました。これは、MySQL のバージョンを指定したいことと、utf8mb4 に対応するために設定ファイルの書き換えが必要であったためです。 しかし、2.0 になって MySQL も Docker イメージとして指定できるようになったため、インストールが不要になり準備時間が短くなりました。

全体の見通し

上述の通り、1.0 のような逐次 override する方式とは異なり、2.0 では全ての処理を一から記述する必要があります。 はじめは runnamecommand などの同じような記述が増えるので、ちょっと面倒だなぁと感じていました。 しかし、最終的に書きあがった設定ファイルを眺めていると、CircleCI 上で行っていることが明確になり、全体の処理に対する見通しが良くなったように感じます。

気をつけたいこと

移行するにあたり、いくつか気をつけたほうがよいことがあります。これの解決に結構ハマってしまったりしましたので紹介します。

複数のイメージを連携する

SmartHR は Ruby on Rails を利用して開発されています。そのため、テストには DB と Redis の用意が必要でした。 幸い 2.0 では以下のようにして複数のイメージを指定できます。

jobs:
  build:
    working_directory: /app
  docker:
    - image: smarthr/circleci-base
      environment:
        RAILS_ENV: test
        DB_HOST: 127.0.0.1
        REDIS_HOST: 127.0.0.1
    - image: mysql:5.7
    - image: redis

ここで注意したいのが、イメージの記載順と各コンテナの連携です。 2.0 では一番最初に書かれているイメージをビルドの実行コンテナとして利用するため、mysql や redis などを最初に書いてしまうと bundle install などのコマンドが用意されていないと言われてしまいます。 AtoZ の昇順で書きたい気持ちをグッと堪え、ベースとなるイメージを最初に書きましょう。

また、各コンテナ間の連携には localhost を使うことはできません。 127.0.0.1 を指定するようにすることと、Rails であれば config/database.yml で以下のような記述を追加することを忘れないようにしましょう。

test:
  adapter: mysql2
  ...
  host: <%= ENV['DB_HOST'] || 'localhost' %>

CIRCLE_TEST_REPORTS がなくなっている…

1.0 までは CIRCLE_TEST_REPORTS という環境変数が用意されており、そこに対してテスト結果を保存するのが一般的でしたが、 2.0 になってこの変数は使えなくなっています。 弊社では各テストの中でこの変数を呼び出してテスト結果の保存先を指定していましたので、下記のようにしてベースコンテナ内で変数を宣言して使うことにしました。

jobs:
  build:
    docker:
      - image: smarthr/circleci-base
        environment:
        CIRCLE_TEST_REPORTS: /tmp/test-results

そして注意が必要なのですが、このような書き方をした場合にテストの保存先として

jobs:
  build:
    steps:
      - store_test_results:
          path: $CIRCLE_TEST_REPORTS

とは書けないことです。これは、環境変数を宣言しているスコープが異なるためです。 上述の設定では CIRCLE_TEST_REPORTS はベースコンテナ内で宣言されていますが、設定ファイルの yml を読んで実行しているのはそのコンテナを実行するためのマシンになります。そのため、config.yml 内で環境変数を使いたい場合は、CircleCI の Web コンソールから設定してやる必要があったのです。 私はこれに気づかず、「CircleCI 2.0 にアップデートしました(ドヤア)」と報告した直後に、「いやテストサマリーが出力されていないんだけど」と社内から指摘を受けることになりました。

ローカルビルドにはあまり頼らないほうがよい

2.0 になり、 CircleCI は CLI 上からローカルビルドが可能になりました。 はじめはめっちゃ便利じゃん〜と思ったのですが、色々な闇を抱えており本環境上とは挙動が異なることもあるため、あまり頼らない方がよいです。こいつはまだまだ発展途上だという印象です。

移行結果

せっかくですので、ビルドの実行時間を比較したいと思います。

1.0 ではテスト完了に 50分弱かかっていたのが、2.0 になって 40分弱くらいまで短くなっています。 詳細を確認したところ、そのほとんどが依存環境のインストール時間でした。 今後 Workflow の導入で並列化の効率を上げて、さらなる高速化を図りたいと考えています。

次回予告

2.0 はまだ正式リリースされたばかりです 。ググっても古い情報や細かい記法/動作の違いなどがあり、ハマってしまうことがありましたので、そのあたりを紹介したいと思います。

次回、 2.0 移行に潜む闇 乞うご期待。