こんにちは。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_ID
や AWS_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_ID
や AWS_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/bundle
や npm install
を行っていると、ローカルビルド時にキャッシュとして開発マシン上の vendor/bundle
や node_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とのお別れの仕方 – 最初の一歩 – が参考になるかと思いますので、あわせてご確認ください。