Gunosy Tech Blog

Gunosy Tech Blogは株式会社Gunosyのエンジニアが知見を共有する技術ブログです。

CircleCI 2.0/2.1の機能をフル活用してCI/CDワークフローを改善してみた

ワークフロー改善

広告技術部のtoshimaruです。この記事はGunosy Advent Calendar 2018、10日目の記事です。昨日の記事はふそやんさんのサーバーレスプッシュ管理画面のコンセプト【グノスポ連載第六回】でした。

昨年2017年にCircleCI 2.0にCircleCIがアップグレードされたことは記憶に新しいかと思いますが、今年2018年末にCircleCI 2.1のアップデートが降ってきました。今日はこのCircleCI 2.0+2.1の機能をフル活用して弊社のRuby on RailsアプリケーションのCI/CDワークフローを改善させた話を書いてみます。

TL;DR

  • CircleCIのexecutors, commandsの定義を利用してCircleCIのconfigをDRYに書けるようになったよ
  • CircleCI workflowを使って自動レビューやデプロイのワークフローを整備したよ
  • Circle CI Orbsを使ってレポジトリを跨ぐCircleCIの処理を共通化できるようになったよ

前提事項

  • CircleCI 2.1およびCircleCI Orbsを使うための設定を済ませてあること

なおCircleCI 2.1, CircleCI Orb導入にあたっては下記の記事を参考にさせていただきました。

CircleCI Orbs 入門 | tsub's blog

改善前: 素のCircleCI 2.0

あくまで擬似コードですが、改善前の .circleci/config.yml は下記のようなイメージです。1つのbuild stepの中に一連の処理内容が上から順に並べてあるだけでした。

version: 2
jobs:
  build:
    docker:
      - image: circleci/ruby
    steps:
      - checkout
      - run: bundle install
      - run: bundle exec rspec
      - deploy:
          name: Deploy to Staging
          command: "deploy operation"

改善ステップ

上記のCircleCIの設定をCircleCI 2.0/2.1の機能をフル活用して改善させていきたいと思います。

改善第1弾: executors, commandsを使ってみる

CircleCI 2.1からはexecutors, commandsがyamlに定義できるようになりました。それぞれ下記の定義が可能です。

  • executors: 実行環境の定義が可能(dockerイメージや環境変数の定義) → これにより複数のジョブを跨ぐ実行環境を共通化できるようになりました
  • commands: 一連の処理をコマンドとして定義可能。→ 関数切り出しのようなイメージ。これによりジョブを跨ぐ共通部分の処理をコマンドとして切り出せるようになりました

ではこれで上記のyamlをリファクタリングしてみましょう。

version: 2.1
executors:
  default:
    docker:
      - image: circleci/ruby
commands:
  bundle_install:
    steps:
      # 実際はここにCircleCIのキャッシュの処理も入る
      - run: bundle install --path vendor/bundle
jobs:
  build:
    executor: default
    steps:
      - checkout
      - bundle_install
      - run: bundle exec rspec
      - deploy:
          name: Deploy to Staging
          command: "deploy operation"

いい感じですね。これによりさらなるワークフロー改善のためのお膳立てが整いました。どんどん改善していきましょう。

改善第2弾: workflowsを使ってみる

次にworkflowsを使ってデプロイ周りの処理をリファクタリングしていきます(ちなみにworkflowはCircleCI 2.0から使える機能です)。

以降のyamlから既に引用してあるyamlと同じ記述は ... として省略しています。

version: 2.1
executors:
  default:
    ...
commands:
  bundle_install:
    ...
jobs:
  rspec:
    executor: default
    steps:
      - checkout
      - bundle_install
      - run: bundle exec rspec
  deploy_staging:
    executor: default
    steps:
      - run:
          name: Deploy to Staging
          command: "=== deploy operation ==="
workflows:
  test:
    jobs:
      - rspec
      - deploy_staging:
          requires:
            - rspec
          context: deploy
          filters:
            branches:
              only: develop

ポイントは下記の通り。

  • develop ブランチのビルド時のみdeployコマンドを走らせるのでそのようなfilterを適応
  • deployはrspecのテストがうまくいったときのみ実行(requires: rspec)
  • context の機能を使い、デプロイのために必要な環境変数はオーガナイゼーショングローバルで管理可能なように切り出し

結果として、下記のようなRSpec→Deploymentというワークフローが仕上がりました。

さぁワークフローっぽくなってきましたね。まだ改善できそうなのでさらに改善させていきます。

改善第3弾: rubocop自動レビューをワークフローに導入(with reviewdog)

私たちのプロジェクトではrubocopを導入しています。rubocopは途中から導入したためrubocopルールを強制すると大変なrubocop diffが出てしまうので今まで各個人の努力でrubocopルールを遵守する・レビューで気づいたrubocopルール違反があれば指摘するという人間による運用を続けていました。

しかしrubocop導入が一年ほどたちrubocopルール遵守の意識もチームに根付いてきたので、このワークフロー整備がよいきっかけだと思い自動レビューの仕組みをワークフローに組み入れてみることにしました。

自動レビューの仕組みはいくつかありますが今回はhaya14busa/reviewdogを使わせてもらうことにしました。なおrubocop+reviewdogを用いた自動レビューの仕組みは私の個人ブログにまとめてありますので気になる方はこちらを参照してみてください。

blog.toshimaru.net

改善した結果仕上がったconfig.ymlはこちら。

version: 2.1
executors:
  default:
    ...
commands:
  bundle_install:
    ...
jobs:
  rubocop:
    executor: default
    steps:
      - checkout
      - bundle_install
      - run:
          name: Install reviewdog
          command: |
            curl -fSL https://github.com/haya14busa/reviewdog/releases/download/$REVIEWDOG_VERSION/reviewdog_linux_amd64 -o reviewdog && chmod +x ./reviewdog
      - run: bundle exec rubocop | ./reviewdog -f=rubocop -reporter=github-pr-review
  rspec:
    ...
  deploy_staging:
    ...
workflows:
  test:
    jobs:
      - rubocop:
          context: reviewdog
      - rspec:
          requires:
            - rubocop
      - deploy_staging:
          context: deploy
          requires:
            - rspec
          filters:
            branches:
              only: develop

どんなワークフローになったかみてみましょう。

ポイントは下記です。

  • rubocopの修正を強要するためrubocopが成功したときのみ後続の処理を走らせる
  • reviewdog はGitHub Pull Requestにコメントする権限を持っている必要があるのでTokenはreviewdog contextで切り出し

しかしまだ終わりではありません。さらに改善させていきましょう。

改善第4弾: CircleCI Orbsでプロジェクト間の処理を共通化

次はCircleCI Orbsを使ってプロジェクト間の処理を共通化していきましょう。

どこが共通化できそうか考えた結果、下記の2つができそうということでCircleCI Orb化しました。

ちなみにCircleCI Orbの公開手順も私のブログにまとめてありますので、興味がある方はどうぞ。

blog.toshimaru.net

仕上がった config.yml はこちら。

version: 2.1
orbs:
  bundle-install: toshimaru/bundle-install@0.1.1
  aws-opsworks-deploy: toshimaru/aws-opsworks-deploy@0.0.1
executors:
  default:
    ...
jobs:
  rubocop:
    executor: default
    steps:
      - checkout
      - bundle-install/bundle-install
      ...
  rspec:
    executor: default
    steps:
      - checkout
      - bundle-install/bundle-install
      ...
workflows:
  test:
    jobs:
      - rubocop:
          context: reviewdog
      - rspec:
          context: deploy
          requires:
            - rubocop
      - aws-opsworks-deploy/deploy:
          name: "deploy-to-staging"
          requires:
            - rspec
          context: deploy
          aws-region: ap-northeast-1
          stack-id: STACK_ID
          app-id: APP_ID
          command: '{"Name": "deploy", "Args":{"migrate":["true"]}}'
          filters:
            branches:
              only: develop

これで完成!…と言いたいところですがもう一声!production環境のdeployもワークフローに組み込んでみましょう。

改善第5弾: Production Deployを人間の手をかまして行う

上述した設定の通り、stagingのデプロイはCircleCIより自動で行っていましたが、productionの自動デプロイは本プロジェクトでは行っておりませんでした。これは本番のデプロイはリリースタイミングの調整が必要だったりDBマイグレーションを気をつけて行う必要であったりすることが主な理由です。

しかし人の手をかますようにしたらどうでしょうか。幸い、CircleCIにはapproveの機構があります。これを使って手動のApproveのステップを挟んで安全なProductionデプロイを実現しましょう2

version: 2.1
orbs:
  bundle-install: toshimaru/bundle-install@0.1.1
  aws-opsworks-deploy: toshimaru/aws-opsworks-deploy@0.0.1
  slack: circleci/slack@0.1.10
executors:
  default:
    ...
jobs:
  rubocop:
    ...
  rspec:
    ...
  send-approval-link:
    executor: default
    steps:
      - slack/notify:
          message: |
            Please check and approve Job to deploy.
            https://circleci.com/workflow-run/${CIRCLE_WORKFLOW_ID}
workflows:
  test:
    jobs:
      - rubocop:
          ...
      - rspec:
          ...
      - aws-opsworks-deploy/deploy:
          ...
      - pending-approval:
          type: approval
          requires:
            - rspec
          filters:
            branches:
              only: master
      - send-approval-link:
          requires:
            - rspec
          filters:
            branches:
              only: master
      - aws-opsworks-deploy/deploy:
          name: "deploy-to-production"
          requires:
            - pending-approval
          context: deploy
          aws-region: ap-northeast-1
          stack-id: STACK_ID
          app-id: APP_ID
          filters:
            branches:
              only: master

結果できたフローはこちらになります。

ポイントは下記です。

  • masterビルド成功時にpending-approvalの処理を挟み込む
  • もしpending-approvalがApproveされれば後続のProduction Deployのステップへと進む
  • pending-approval時にSlackでapproveのためのリンクを送付

`pending-approval`のApproveボタン

Slackのデプロイ準備完了OKの通知

これにて今回のワークフロー改善は終了です!!!

最後に

以上、ステップバイステップでCI/CDワークフローの改善の歩みを紹介してきました(実際はこれを一ヶ月半くらいかけてちょこちょこと進めてきていました)。たかがCI/CDワークフロー改善、されどCI/CDワークフロー改善。このような改善を積み重ねることが開発者のDeveloper Experienceの向上に繋がります。

CircleCI 2.1とCircle Orbsを組み合わせることでCI/CD改善の幅は圧倒的に広がったと思っています。みなさんの開発の現場でもワークフロー改善にチャレンジしてみてはいかがでしょうか?


  1. sue445さんが作成されたsue445/ruby-orbsbundle-installコマンドが細かなオプションを設定できるリッチなbundle installコマンドに仕上がっているので、必要に応じてそちらも使用してみてください
  2. ただ本設定は各ビルド毎にデプロイリソースを作成するようなワークフローにおいては、デプロイ事故が発生する可能性があるので気をつけたほうがよいです。事故の例としては例えば、誤って過去のワークフローのApproveをしてしまい1週間前のコードベースがデプロイされるなどが考えられます。