デプロイ失敗時にCI/CDで再デプロイしたいので、CircleCIのワークフローを見直して並列にした話

CircleCIを利用してCI/CDを行っています。今までは一本道のワークフローを作成してデプロイしていましたが、課題に対処するために並列に変更してみました。
2020.05.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

今のプロジェクトでは、リリース作業を毎週行っています。たまにデプロイやE2Eテストが失敗してやり直すことがあるのですが、CI/CDのワークフローが一本道なので手動デプロイせざるを得ない事象が発生しました。 そのため、対策としてCircleCIのワークフローを見直して並列にしてみました。

環境について

  • 本番環境
    • エンドユーザーが利用している
  • プレ本番環境
    • 関係者が本番さながらに利用している
    • 本番環境リリース前の最後の砦
  • ステージング環境
    • いわゆる検証環境
    • 動作確認を行う
  • 開発環境
    • 開発者が自由に使える

リリースについて

v1.2.3のようなタグを付けてPushすれば、CircleCIのワークフローが動いてステージング環境・プレ本番環境・本番環境へのリリースが行われます。リリース作業自体はミスを防ぐために複数人で確認しながら行っています。

今のワークフロー

特徴としては、ステージング環境→プレ本番環境→本番環境と一本道になっており、各環境の間にapproveボタン(承認ボタン)を仕込んでいます。 これによってジョブの実行を一時停止させており、人間が承認すると次の環境にデプロイするジョブが動くようになっています。

  1. ステージング環境にリリースする(タグを付けてPushする)
  2. 1週間待つ
  3. プレ本番環境にリリースする(承認ボタンを押す)
  4. 1週間待つ
  5. 本番環境にリリースする(承認ボタンを押す)

なお、デプロイ環境の準備ではPython仮想環境の構築や必要なライブラリのインストールを行い、それをCacheに保存しています。以降のジョブではCacheから拾って利用しています。

一本道の課題

プレ本番環境や本番環境でデプロイに失敗した場合やE2Eテストが失敗したとき、手動デプロイ(CircleCIを使わず、開発者がデプロイコマンドを実行)せざるを得ない課題があります。 しかし、手動デプロイは地味に手間ですし、普段はしない操作のためミスをする可能性があります。タグを付けてPushすれば終わりではありません。

  • 環境変数の名前と値を調べて適応する
  • 使用するデプロイコマンドを調べたり実行順序に気をつける

手動デプロイをしている理由は、CircleCIのワークフローが一本道なので、デプロイ不要な環境(ステージング・プレ本番環境)へのデプロイが行われてしまうからです。

一本道がダメなら、並列にすればいいじゃない

そんなわけでCircleCIのワークフローを見直して並列にしてみました。

新しいワークフロー

これによって、特定環境だけにデプロイできるようになりました。 ただし、通常のリリース時に「あ! ステージング環境じゃなくていきなり本番デプロイしちゃった!!!」となる可能性が生まれます。 このあたりは承認時に複数人でチェックするなどで対処します。 今回の変更は手動デプロイせざるを得ない状況を回避する事が目的だからです。まずは試してみて、不都合があればまた見直していきます。

CircleCIの画面(参考)

承認待ちの様子

ステージング環境にデプロイした様子

ステージング環境にデプロイした様子

いきなり本番環境にリリースした様子

いきなり本番環境にリリースした様子

すべての環境にリリースした様子

すべての環境にリリースした様子

CircleCIのConfigファイル(参考)

あくまでも参考ですが、下記がCircleCIのConfigファイルです。

.circleci/config.yml

version: 2.1
executors:
  my-executor:
    docker:
      - image: circleci/python:3.7.2
        environment:
          PIPENV_VENV_IN_PROJECT: true
    working_directory: ~/work

commands:
  restore:
    steps:
      - restore_cache:
          key: work-v1-{{ .Branch }}-{{ .Revision }}

  save:
    steps:
      - save_cache:
          paths:
            - ".venv"
          key: work-v1-{{ .Branch }}-{{ .Revision }}

  deploy:
    parameters:
      env:
        type: enum
        enum: ["prod", "pre_prod", "staging"]
    steps:
      - checkout
      - restore
      - run:
          name: deploy
          command: |
            source .venv/bin/activate
            echo << parameters.env >>
            echo deploy-command

jobs:
  setup:
    executor: my-executor
    steps:
      - checkout
      - restore
      - run:
          name: install
          command: |
            sudo pip install pipenv
            pipenv install
      - save

  lint_and_unittest:
    executor: my-executor
    steps:
      - checkout
      - restore
      - run:
          name: lint_and_unittest
          command: |
            source .venv/bin/activate
            echo lint-command
            echo unittest-command

  deploy_staging:
    executor: my-executor
    steps:
      - checkout
      - restore
      - deploy:
          env: staging

  deploy_pre_prod:
    executor: my-executor
    steps:
      - checkout
      - restore
      - deploy:
          env: pre_prod

  deploy_prod:
    executor: my-executor
    steps:
      - checkout
      - restore
      - deploy:
          env: prod

workflows:
  version: 2.1
  release-workflow:
    jobs:
      - setup:
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - lint_and_unittest:
          requires:
            - setup
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - approve_for_staging:
          type: approval
          requires:
            - lint_and_unittest
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - approve_for_pre_prod:
          type: approval
          requires:
            - lint_and_unittest
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - approve_for_prod:
          type: approval
          requires:
            - lint_and_unittest
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - deploy_staging:
          requires:
            - approve_for_staging
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - deploy_pre_prod:
          requires:
            - approve_for_pre_prod
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/
      - deploy_prod:
          requires:
            - approve_for_prod
          filters:
            branches:
              ignore: /.*/
            tags:
              only: /v([0-9]+\.){2}[0-9]+/

さいごに

CircleCIのワークフローを見直して、一本道から並列に変更してみました。たまにはCI/CDのフローを見直してみると良いかもしれませんね。

参考