SmartHR Tech Blog

SmartHR 開発者ブログ

CircleCI 2.0 移行に潜む闇

こんにちは。SmartHR SRE チームの吉成です。

先日 CircleCI 2.0 移行の記事を書きましたが、CircleCI 2.0 に移行するのは一筋縄ではいきませんでした。 本日は設定の変更や Docker 利用の影響で行き詰まったことを紹介し、その解決方法を共有したいと思います。幾つかは前回の記事でも軽く触れたものになりますが、参考になれば幸いです。

カスタムイメージ置き場の闇

2.0 から自身で CI に利用したい Docker イメージをあらかじめ作ることで、ビルド中の動作環境を用意する時間を短くできるようになりました。 弊社では Ruby on Rails をベースにしつつも、フロントエンドのコードは npm を使って管理するように分割されているため、Ruby と npm の両方を扱える環境が必要でした。 そのため、今回は Ruby をベースイメージとして npm をインストールしたカスタムイメージを用意し、その上でビルドさせようと考えていました。

カスタムイメージはパブリックイメージのみ

弊社では ECS を利用して Dockerize された環境を用意する前提で開発を進めています。ECS では ECR からイメージを取得するようにしていたため、同じように ECR を利用する予定でした。 しかし、 CircleCI でプライベートイメージを利用するのは若干面倒でした。また、ECR ではパブリックイメージを作成できない…

今回は仕方がないので、Docekr Hub 上にパブリック・イメージを置き、それを利用するようにしました。 ベースとなるイメージの Dockerfile を非公開前提で書いていたため、公開したくない部分を修正するなど、ちょっとした修正の手間が発生しました。「Dockerize してるから移行カンタンそうだ〜」と思っている方は、公開するにあたって修正が入る可能性があるということにご注意ください。

ビルド中の挙動の闇

1.0 と 2.0 で実行環境が CircleCI が用意してくれるマシン上から自分で用意した Docker イメージ上へと移りました。 それに伴い、いくつかのコマンドや前提条件が変更されており、そのままでは動作しない箇所が存在しました。

AWS コマンドと認証

弊社では現在、 ElasticBeanstalk を利用しており、デプロイ時には CI 上から eb deploy を実行して環境をデプロイしています。 1.0 では環境変数に AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY を設定することで aws-cli を利用できましたが、2.0 では自分で設定ファイルを出力しておく必要があります。

アクセスキーを出力しなければならないので、カスタムイメージ作成時に設定ファイルを含めておくことができず、以下のような対応を行いました。

      - run:
          name: Setup AWS credentials
          command: |
            mkdir -p ~/.aws
            printf "[default]\nregion = ${AWS_DEFAULT_REGION}\naws_access_key_id = ${AWS_ACCESS_KEY_ID}\naws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" > ~/.aws/config
            printf "[default]\naws_access_key_id = ${AWS_ACCESS_KEY_ID}\naws_secret_access_key = ${AWS_SECRET_ACCESS_KEY}" > ~/.aws/credentials
            chmod 600 ~/.aws/*

カスタムイメージに対して AWS_ACCESS_KEY_IDAWS_SECRET_ACCESS_KEY の環境変数を設定すれば aws configure コマンドのオプション指定でもいけそうですが、そちらは試していませんので動作の保証はできません。

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 コンソールから設定してやる必要があったのです。

テスト結果の保存先に CIRCLE_TEST_REPORTS を利用している方はご注意ください。

parallelism の指定方法

1.0 の時代は、同時に実行するコンテナ数は Web コンソール上から指定していたかと思います。 2.0 でも同様の指定かと思っていたのですが、2.0 では設定ファイルに明記する必要があります。

version: 2

jobs:
  build:
    parallelism: 5

このようにして、設定ファイルの上部に並列数を明記する必要がありますので注意しましょう。

また、CircleCI 上の環境変数に PARALLELISM_COUNT を設定し、以下のようにしてみたのですが動作しませんでした。

version: 2

jobs:
  build:
    parallelism: ${PARALLELISM_COUNT}

このように書ければ並列数を変更しようとした際に、毎回設定ファイルを更新する必要がなくなるのですが…

deployment のブランチ指定

1.0 では以下のようにしてデプロイ処理に名前をつけるとともに、実行ブランチを指定できました。

deployment:
  staging:
    branch: staging
    commands:
      - ./deploy_staging.sh
  production:
    branch: master
    commands:
      - ./create_release_tag.sh

しかし、このようなブランチ指定は 2.0 ではできないようになりました。厳密には、弊社の移行時には実装されておらず、以下のようにしてコマンド実行時に分岐を入れる対応が必要でした。

version: 2

jobs:
  build:
    working_directory: /app
    docker:
      - image: smarthr/circleci-base
    steps:
      - checkout

      - deploy:
          command: |
            if [ "${CIRCLE_BRANCH}" == "staging" ]; then
              ./deploy_staging.sh
            fi
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              ./create_release_tag.sh
            fi

この機能は現在実装中のようでしたので、同じように簡単にかけるようになることを期待して待ちましょう。

GitHub の Tag 付けベースの起動

1.0 では特に何もせずとも GitHub のブランチに対するタグをトリガーにビルドを開始できました。 弊社でも以下のように設定ファイルを書くことでリリースタグの作成時に本番環境をデプロイしていました。

deployment:
  release:
    tag: /release-.*/
    commands:
      - ./deploy_production.sh

一つ前の設定ファイルに似ているのですが、これを移行時に下記のように書き換えました。

version: 2

jobs:
  build:
    working_directory: /app
    docker:
      - image: smarthr/circleci-base
    steps:
      - checkout

      - deploy:
          command: |
            if [[ "${CIRCLE_TAG}" =~ release-.* ]]; then
              ./deploy_production.sh
            fi

なんとなく動きそうですよね?しかし、 2.0 ではまだタグ付けをトリガーとした実行ができないようで、 なんとデプロイ作業中にデプロイ処理が実行されない という不具合をおこしてしまいました…

色々と調べたのですが、今現在では以下のようにして 1.0 の記法を併記することで動作することが確認できたようです(参考)

version: 2

jobs:
  build:
    working_directory: /app
    docker:
      - image: smarthr/circleci-base
    steps:
      - checkout

      - deploy:
          command: |
            if [[ "${CIRCLE_TAG}" =~ release-.* ]]; then
              ./deploy_production.sh
            fi

deployment:
  release:
    tag: /release-.*/

ただし、 deployment の中に commands を書くと動作しない とのことですので、ご注意ください。

Local Build に潜む闇

2.0 から CircleCI Command Line Interface というものが用意され、ローカルマシン上でビルド処理を実行可能になりました。 1.0 の時代のように設定ファイルを書き換えてリポジトリにプッシュし、ビルド完了を Web 上で確認する必要がなくなり、一見開発効率爆上げのように見えます。 しかし、この CircleCI CLI には多くの闇が含まれており、何度も何度も悩まされました。

なお、同じ日の朝と夜にアップデートされるなど非常に活発な開発が勧められているようですので、この記事が読まれる頃には解決しているかもしれません。

validate コマンド

CircleCI CLI では以下のようにして設定ファイルが正しく書かれているか確認できます。

$ circleci config validate -c .circleci/config.yml

しかしこれ、結構曖昧です… config file is valid と言われたのに、実際にビルドするとシンタックスエラーになることが何度かありました。例を下記に示します

$ cat .circleci/config.yml
version: 2

jobs:
  build:
    working_directory: /
    docker:
      - image: busybox
    steps:
      - checkout

      - run:
        name: echo hoge
        command: echo "hoge"

$ circleci config validate -c .circleci/config.yml

  config file is valid

$ circleci build

====>> Spin up Environment
Build-agent version 0.0.3600-d284d10 (2017-07-13T14:46:40+0000)
Error: Configuration errors: 1 error occurred:

* In step 2 definition: Step

  - run:
    command: echo "hoge"
    name: echo hoge

has incorrect indentation, it should be formatted like:

  - step:
      option1: value
      option2: value
Step failed
Task failed

これは、 run の後のインデントが足りていないためにエラーとなっています。なお、正しい設定ファイルは以下の通りです。

version: 2

jobs:
  build:
    working_directory: /
    docker:
      - image: busybox
    steps:
      - checkout

      - run:
          name: echo hoge
          command: echo "hoge"

移行時に 1.0 の設定ファイルを少しずつ構文チェックしながら書き写しても、実際に動作させると動かないこともあるので気をつけましょう。

キャッシュ

Bundler や npm による依存関係では、以下のような設定ファイルでキャッシュを保持し、2回目移行のビルドを高速化できると公式のドキュメントにも記載があります。

version: 2

jobs:
  build:
    working_directory: /app
    docker:
      - image: smarthr/circleci-base
    steps:
      - checkout

      - restore_cache:
          name: Restore bundle cache
          keys:
            - bundle-{{ checksum "Gemfile.lock" }}

      - run:
          name: Run bundle install
          command: bundle install --path=vendor/bundle

      - save_cache:
          name: Store bundle cache
          key: bundle-{{ checksum "Gemfile.lock" }}
          paths:
            - vendor/bundle

      - run:
          name: Run RSpec
          command: bundle exec rspec

      - run:
          name: Run npm install
          command: |
            npm cache clean
            npm install

      - save_cache:
          name: Store npm cache
          key: npm-{{ checksum "package.json" }}
          paths:
            - ./node_modules

      - run:
          name: Run npm test
          command: npm run test

キャッシュを保持するために、 Bundler ではパスを指定し、 npm では node_modules などを対象にすることが多いかと思います。 しかし、開発マシンでも同様に bundle install --path=vendor/bundlenpm install を行っていると、ローカルビルド時にキャッシュとして開発マシン上の vendor/bundlenode_modules を参照してしまいます。

依存関係に Mac や Windows などのプラットフォームによってインストールするものが異なる Gem や Package が含まれていた場合、CircleCI CLI の実行環境(コンテナなので、殆どの場合が Linux 想定)では必要な依存関係が解決していないことなります。そのため、RSpec や Gulp 実行時にエラーが起きることがあります。

移行時には開発マシンから一度依存関係を削除し、CircleCI CLI 経由でインストールしながら動作確認を行うようにしましょう。

複数コンテナ接続の闇

弊社では Ruby on Rails を利用した開発を行っている関係上、 RSpec の実行時にも DB を用意する必要があります。 2.0 では以下のようにして複数コンテナを立て、各コンテナ間で接続することで MySQL を用意したり Redis を用意したりできます。

version: 2

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

ここで注目していただきたいのは、Rails を動かすコンテナ内で MySQL や Redis のホストを 127.0.0.1 として指定していることです。 Docker コンテナ間の接続になるため、 localhost という指定では動作しないようです。 環境変数の設定のほか、Rails の config/databases.yml 内などでテスト環境のホストを DB_HOST を参照するように修正する必要がありますのでご注意ください。

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

さて、Rails のテストステップは以下のように設定しました。

    steps:
      - checkout

      - run:
          name: Run bundle install
          command: bundle install --jobs=4 --retry=3 --path=vendor/bundle

      - run:
          name: Create DB
          command: |
            RAILS_ENV=test bundle exec rake db:create
            bundle exec rake db:schema:load

      - run:
          name: Run Test
          command: bundle exec rspec

この動作を確認するために、 CircleCI CLI で動作確認を行うと、Create DB で動作が止まり、DB Connection Error でタイムアウトしてしまいました。 しかし、公式が用意している Rails のサンプル は正常に動作するので、困ってしまいました… これはなんと、CircleCI CLI が開発マシン上の MySQL に接続を試みていたことが原因のようでした。公式のサンプルは PostgreSQL を利用しており、私の開発マシン上では動作していないために正常に接続できたようです。

開発マシン上の MySQL を止めるか、諦めてコードをプッシュし、 CircleCI 上で実行させましょう。

最後に

全体を通したサンプルを こちら に用意しました。 弊社では Ruby on Rails、MySQL、Redis そして npm 経由のフロントエンドテストまでを行っています。 Rails からフロントエンドを切り出し、 CircleCI 上で CI を回している方の参考になればと思います。 なお、Rails からフロントエンドを切り出したい方は、手前味噌ではありますが、Rails Sprocketsとのお別れの仕方 – 最初の一歩 – が参考になるかと思いますので、あわせてご確認ください。