CircleCI→herokuのCI/CD構築手順(デプロイ先をstagingとprodcutionで切替）

##達成したいもの

master、staging、developブランチがあるとして
masaterへマージ・・・・・**自動テスト実行→本番環境へ自動デプロイ**
staginへマージ・・・・・・**自動テスト実行→検証環境へ自動デプロイ**
develpへプッシュ・・・・・**自動テスト実行**

CircleCIでのCI/CDパイプライン化というものを行いたい。

##公式リファレンス
https://circleci.com/docs/2.0/configuration-reference/#checkout
こちらにあります。

##サンプルのconfig.yml
プロジェクト追加時に示されるsample.yml
基本的にこれにDBのimageを追加すればすぐさま自動テストが利用できます。
(ruby versionなどは変更してください。）

```yml:sample.yml
# Ruby CircleCI 2.0 configuration file
#
# Check https://circleci.com/docs/2.0/language-ruby/ for more details
#
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/ruby:2.4.1-node-browsers

      # Specify service dependencies here if necessary
      # CircleCI maintains a library of pre-built images
      # documented at https://circleci.com/docs/2.0/circleci-images/
      # - image: circleci/postgres:9.4

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

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

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

      # run tests!
      - run:
          name: run tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"

            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      # collect reports
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results
```

ですが今回は設定を変えていくので基本的な構造から確認します。

##設定ファイルの基本的な構造


```yml：超簡略版congif.yml

version: 2
jobs:
  #job名
  build:
    #dockerイメージや実行コマンドを書いていく
workflows:
  version: 2
  #workflow名
  build-deploy: 
    jobs:
      #実行するジョブとスケジュールを書いていく
```
こちら超簡略化したconfig.yml
jobsでテストの実行内容を書いて
workflowsでテストの実行条件（スケジュール）を書きます。

##full exampleが参考になる

公式リファレンス内にfull exampleというものがあります。
これはステージング環境と本番環境へのデプロイの設定が想定されているので
これを元にRailsプロジェクトへと書き換えるのが良さそうです。
下記はfull exampleを簡略化したものです。

```yml:full_example.yml簡略版


version: 2
jobs:
  build:
    docker:
      - image: dockerイメージ名

    environment:
      FOO: bar 
    working_directory: ~/my-project
    steps:
      - checkout

      - run:
          name: ステップ名
          command: 実行コマンド

  deploy-stage:
    docker:
      - image: dockerイメージ名
    working_directory: /tmp/my-project
    steps:
      - run:
          name: ステップ名
          command: 実行コマンド

  deploy-prod:
    docker:
      - image: dockerイメージ名
    working_directory: /tmp/my-project
    steps:
      - run:
          name: 
          command: 

workflows:
  version: 2
  build-deploy:
    jobs:
      - build:
          filters:
            branches:
              ignore:
                - develop
                - /feature-.*/
      - deploy-stage:
          requires:
            - build
          filters:
            branches:
              only: staging
      - deploy-prod:
          requires:
            - build
          filters:
            branches:
              only: master
```

##テスト実行の流れ

jobs以下で実行内容を定義し、
workflows以下で実行タイミングを定義します。
またworkflowsを定義していない場合はデフォルトでjob -> buildが
エントリーポイントとなり、push時に実行されます。

またworkflowsには filters -> branches -> onlyまたはignoreで
トリガーbranchを設定します。

上記のfull exampleでは
**・develop, featureブランチを除いたブランチへのpush時にbuildジョブを実行**
**・stagingブランチへのpush時にはbuildジョブ成功後にdeploy-stageジョブを実行**
**・masterブランチへのpush時にはbuildジョブ成功後にdeploy-prodジョブを実行**

という設定になっています。
ステージング環境と本番環境でデプロイ先を変えたい場合は
deploy-stageジョブとdeploy-prodジョブの中で設定を行えば可能となります。

##cacheを活用する

テスト実行を効率化するためにcacheを使いましょう。
（デフォルトの設定ファイルですでに設定されています）
公式ページはhttps://circleci.com/docs/2.0/caching/

パッケージの依存関係が更新されていない限りはrestoreされたキャッシュを使用します。

**設定方法**

```yml
    steps:
      - restore_cache:
         keys:
           - v1-dependencies-{{ checksum "Gemfile.lock" }}
           - v1-dependencies-
      - run:
          name: install dependencies
          command: |
            bundle install --jobs=4 --retry=3 --path vendor/bundle
      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}
```

**restore_cache**で利用できるキャッシュを探索
**save_cache**でキャッシュを保存します。
依存関係が書かれているGemfile.loc:kのチェックサムが合致すればそれを利用し、
無ければ最新のキャッシュを利用します。
上記はgemをvendor/bundleにキャッシュしてGemfile.lockの変更をトリガーにして
キャッシュを保存していく、といった設定になります。

pathを.gitに変更してソースコード自体をキャッシュする方法もあるので
checkoutに時間がかかる場合はこちらを検討すると良いみたい。

##CircleCIからherokuへデプロイする

公式のリファレンスは
https://circleci.com/docs/2.0/deployment-integrations/#heroku
こちらです。

herokuでデプロイする場合には、heroku側のUIから設定可能ですが
パイプラインとしてcircleCIに組み込む場合は
config.ymlにgit pushコマンドを記載します。
また、CircleCIのUIから環境変数にAPIキーとAppNameを設定しておきましょう。

```terminal:herokuへのpushコマンド
git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME.git master
```
このコマンドをconfig.ymlのjobに組み込めば自動デプロイが可能ですね。
ステージングと本番環境を分けられるように環境変数も
**HEROKU_APP_NAME_STAGE**
**HEROKU_APP_NAME_PROD**
のようにして二つに分けておきましょう


##full_exampleを完成版にする

必要な情報は揃いましたがfull_exmapleがrubyとherokuでは無いため
CircleCIを使って
・Githubへプッシュすると自動テストが実行される
・stagingブランチへpushされた場合は検証環境(heroku)へデプロイ
・masterブランチへpushされた場合は本番環境(heroku)へデプロイ
書き換えて行きましょう


```yml:.circleci/config.yml
version: 2
jobs:
  build:
    docker:
      # specify the version you desire here
      - image: circleci/ruby:2.6.1-node-browsers
      - image: circleci/postgres

    working_directory: ~/repo

    steps:
      - checkout

      # Download and cache dependencies
      - restore_cache:
          keys:
            - v1-dependencies-{{ checksum "Gemfile.lock" }}
            # fallback to using the latest cache if no exact match is found
            - v1-dependencies-

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

      - save_cache:
          paths:
            - ./vendor/bundle
          key: v1-dependencies-{{ checksum "Gemfile.lock" }}

      # Database setup
      - run: bundle exec rake db:create
      - run: bundle exec rake db:schema:load

      # run tests!
      - run:
          name: run tests
          command: |
            mkdir /tmp/test-results
            TEST_FILES="$(circleci tests glob "spec/**/*_spec.rb" | \
              circleci tests split --split-by=timings)"

            bundle exec rspec \
              --format progress \
              --format RspecJunitFormatter \
              --out /tmp/test-results/rspec.xml \
              --format progress \
              $TEST_FILES

      # collect reports
      - store_test_results:
          path: /tmp/test-results
      - store_artifacts:
          path: /tmp/test-results
          destination: test-results

  deploy_stage:
    docker:
      - image: circleci/ruby:2.6.1-node-browsers
    working_directory: /tmp/repo
    steps:
      - checkout
      - run:
          name: 'Install Heroku CLI, if necessary'
          command: |
            if [[ $(command -v heroku) == "" ]]; then
              curl https://cli-assets.heroku.com/install.sh | sh
            else
              echo "Heroku is already installed. No operation was performed."
            fi
      - run:
          name: heroku maintenance on
          command: heroku maintenance:on --app ${HEROKU_APP_NAME_STAGE}
      - run:
          name: Deploy to Heroku_Staging
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME_STAGE.git master
      - run:
          name: heroku maintenance off
          command: heroku maintenance:on --app ${HEROKU_APP_NAME_STAGE}

  deploy_prod:
    docker:
      - image: circleci/ruby:2.6.1-node-browsers
    working_directory: /tmp/repo
    steps:
      - checkout
      - run:
          name: 'Install Heroku CLI, if necessary'
          command: |
            if [[ $(command -v heroku) == "" ]]; then
              curl https://cli-assets.heroku.com/install.sh | sh
            else
              echo "Heroku is already installed. No operation was performed."
            fi
      - run:
          name: heroku maintenance on
          command: heroku maintenance:on --app ${HEROKU_APP_NAME_PROD}
      - run:
          name: Deploy to Heroku_Production
          command: |
            git push https://heroku:$HEROKU_API_KEY@git.heroku.com/$HEROKU_APP_NAME_PROD.git master
      - run:
          name: heroku maintenance off
          command: heroku maintenance:off --app ${HEROKU_APP_NAME_PROD}


workflows:
  version: 2
  build-deploy:
    jobs:
      - build
      - deploy_stage:
          requires:
            - build
          filters:
            branches:
              only:
                - staging
      - deploy_prod:
          requires:
            - build
          filters:
            branches:
              only:
                - master

```

herokuメンテナンスモードを変えるためherokuCLIが必要なので
インストールコマンドだけ追加しています。
herokuとgithubを連携させてテスト完了後にデプロイという設定もできますが
CircleCI上でパイプライン化するとどこで異常が発生したか
一目瞭然で便利ですね。


##master,stagingへのプッシュを禁止する

このCI/CDパイプラインは
**feature-* → develop → staging(デプロイ) → master(デプロイ）**
という想定なのでこれらのブランチには直接プッシュできない状態にするのが安全。
そのため
https://qiita.com/starfish/items/f55b7cadeff5df5cca46
https://qiita.com/january108/items/05ab9e2f1d713f6efae3
こちらの記事にあるようにpre-pushを設定しておくと心理的にも安心安全、、、！