CircleCI2.0でデプロイステップを別のビルドに分離する

CircleCI2.0がベータ版として公開されている. 個人のプロジェクトではCircleCIにべったりで(無料枠だけど),Docker buildとかさせまくっているので,早速使ってみた.

自分でdocker imageを用意する

1.0では「あれーnodeって入ってたっけ,バージョンいくつだっけ」みたいに思ったら,CircleCIのドキュメントを読んで調べたりしていた.

elixirのプロジェクトをやったときなんかは,「入ってないじゃん,これ自分でインストールするのかよ」と思った. 結局自分でインストールしてたけど,そもそもインストールに10分くらいかかってたので,キャッシュが効かないと非常に時間のかかるテストになっていた.

2.0ではDockerを自分で用意すればいいので,elixirはインストール済みのものを用意するだけで,非常に楽だった.感動的. CI2分切ったぞ!

CIでしか使わないパッケージが足らない問題

反面ちょっと困ることもある.

本番用のコンテナとしてalpineを用意していたりする. こういうコンテナをCIで使いまわそうとすると,tar, gzipすらなくて,キャッシュのリストアができないwww

restore_cach のところで,

Error untarring cache: exit status 1

と言われる. これはtar, gzipを入れよう.

$ apk add --no-cache tar gzip

とかするとか,docker imageに詰めておくとか.

このくらいならdocker imageに詰めておいてもいいかもしれない. だけど,たとえばCIでawsコマンドを使いたくなったらどうしよう? インストールのために,python-pipと,awscliをdocker imageに入れるか?

これはあまり入れたくない.

リーズナブルな解決策としては,CI用のimageはやはり本番や開発とは分けて,CI専用ということで必要なものを全部詰め込んで用意するのがいいだろう.

デプロイステップを分離する

さて,本題.

上記でも少し触れたけど,例えば,imageに入ってないパッケージを使うのが,デプロイステップだけだとしたら? そしてそのデプロイ,実はmasterだけで実行できればいいとしたら?

特定のブランチだけで実行したい

2.0では,branches という項目で,ビルド対象を絞れる. だが,これはビルドステップ全体の実行判断に過ぎない.

ステップ単体で,ブランチごとに実行判断を分けたい場合は,

- run:
  name: deploy
  command: |
    if [ "${CIRCLE_BRANCH}" == "master" ]; then
      deploy.sh
    fi

とかしてあげないといけない.

じゃぁsetup_remote_dockerはどうなる?

デプロイステップで,docker buildをしたいとしよう. コンテナ内ではあるが,CircleCIはdockerコマンドの実行方法を用意してくれている.

それがこの,setup_remote_dockerだ.

しかし,こいつには多少時間がかかる.

デプロイステップ,すなわちmasterでだけ必要な処理だが,setup_remote_docker の設定項目に branches は存在しない. 即ち,全てのブランチで実行されてしまう

時間がかかるというのに!

これが大問題だった.

master以外ではデプロイをしないので,docker buildする必要はない. だけど,全てに共通して setup_remote_docker しないといけなくて,全てのブランチのCI時間が無駄に長くなる.

別のビルドに分けよう

とりあえず今の段階でCircleCIがこれに対する解決策は出してくれていない.

しかし,いい質問が飛んでいて, https://discuss.circleci.com/t/specify-a-config-yml-file-per-branch-with-config-yml-as-the-fallback/11191/6

非常に参考になる.

設定ファイルに新たなjobを定義する

version: 2
jobs:
  build:
    docker:
      - image: h3poteto/phoenix
    working_directory: /var/opt/app
    steps:
      - checkout

      - run:
          name: Test
          command: mix espec

      - deploy:
          name: conditionally run a deploy job
          command: |
            if [ "${CIRCLE_BRANCH}" == "master" ]; then
              curl --user ${CIRCLE_API_TOKEN}: \
                --data build_parameters[CIRCLE_JOB]=deploy \
                --data revision=$CIRCLE_SHA1 \
                https://circleci.com/api/v1.1/project/github/$CIRCLE_PROJECT_USERNAME/$CIRCLE_PROJECT_REPONAME/tree/$CIRCLE_BRANCH
            fi

  deploy:
    docker:
      - image: h3poteto/phenix
    working_directory: /var/opt/app
    branches:
      only:
        - master
    steps:
      - checkout
      - setup_remote_docker:
      - deploy:
          name: docker build
          command: docker build ...

このような定義にしておく.

CircleCI側にAPI_TOKENを準備

CircleCIのプロジェクト設定から API Permissionを選び,新たにAPI TOKENを発行する. 権限が,デフォルトだとStatusになっているので,一応Allをいただく.

こいつを環境変数 CIRCLE_API_TOKEN として設定しておく.

あとは,これをpushすると,buildに記述されたものだけがまず走る.

そしてテストの最後,deployステップで,CircleCIに新たなビルドリクエストを投げ直す. このときに,対象をdeployに絞っているため,deploy jobだけが動き出す.

心配ごと

とりあえず公式のやり方ではないが,いいやり方だとは言われているので,しばらくは使えるんじゃないかな.

おそらくだけど,jobsという定義にしているあたり,もっとワークフロー的な,少なくともdeployステップを分離するようなことはCircleCIも考えているのだとは思う.

困ることがあるとすれば,今回はデプロイはmasterだけ!と決めていたからあまり問題にはならないけど,複数ブランチでdeployを走らせる前提だとAuto-cancel redundant buildsとのかみ合わせは最悪だと思う.