Ruby on Rails, Vue.js によるモダン WEB アプリケーション 実践編 (その2)

# はじめに

アプリケーションは Rails と Vue.js を想定して、実戦的なアプリケーション動作環境を構築するための方法を段階的に紹介していきます。

Rails と Vue を使ったアプリケーションを初めて開発する場合は [Ruby on Rails, Vue.js で始めるモダン WEB アプリケーション入門](https://qiita.com/tatsurou313/items/4f18c0d4d231e2fb55f4) も参考にしてみて下さい。

## アプリケーションの機能

* [(その1) アプリケーションは冗長構成の Postgres に接続できる](https://qiita.com/tatsurou313/items/b7b0bbabd1d61baccbb3)
* [(その1) アプリケーション、DB は k8s にデプロイされる](https://qiita.com/tatsurou313/items/b7b0bbabd1d61baccbb3)
* [(その1) LE証明書を使う(定期更新できる)](https://qiita.com/tatsurou313/items/b7b0bbabd1d61baccbb3)
* (その2) CI/CD 環境がある
* [(その3) OAuth によりログイン認証できる](https://qiita.com/tatsurou313/items/c7a3bd31bce1ffb82eec)


# CD環境を構築する

[前回](https://qiita.com/tatsurou313/items/b7b0bbabd1d61baccbb3)は本番環境を初期構築しました。

今後、アプリケーションが新しくなった時に(ほぼ)自動でデプロイできるようにしてみます。
Git リポジトリの stable ブランチが本番環境用なので、stable ブランチが更新されたことを起因としてデプロイ処理が行われるようにしましょう。

インテグレーションツールには Jenkins, CircleCI や Werkrer, GitHub Action 等があり、どのツールでもよいと思います。
今回は GitHub でアプリケーションを管理することを想定しているため、GitHub Action を使うことにします。

デプロイに必要な処理の概要は次のとおりです。

1. コンテナイメージをビルドして Docker hub に push する
    - イメージのタグ名は `stabe` (1-a.), `<Git SHA-1ハッシュ>` (1-b.) の2種類がある
2. DB のマイグレーションを実行する
3. Rails アプリケーションを新しいコンテナイメージで動作させる


## Docker hub の automated build を設定する(1-a.)

Docker hub ではリポジトリを GitHub と連携して Automated build を行う設定が出来ます。
しかしタグ名に Git SHA1 ハッシュ値を設定する等の細かい設定は GitHub Action にて行うこととして、stable ブランチを stable タグで build する設定だけすることにします。

automated build を設定することで副産物として Docker hub の repository に GitHub の README.md が同期されます。

automated build を設定するには、リポジトリの設定から [Builds] タブから設定します。[参考](https://docs.docker.com/docker-hub/builds/#configure-automated-build-settings)

|Source Type|Source|Docker Tag|Dockerfile location|Build Context|Autobuild|Build Caching|
| --- | --- | --- | --- | --- | --- | --- | --- |
|Branch|stable|stable|Dockerfile|/|`ON`|`ON`|

設定が終わったら一度 Trigger ボタンを押してビルドを実行してみるとよいでしょう。
ビルドが成功して General タブに README.md の内容が表示され、Tags に stable タグのイメージが作成されていれば成功です。

## GitHub Action を設定する(1-b., 2., 3.)

次に GitHub Action を設定します。


### stable ブランチのコミット値をタグ名に持つ Docker コンテナイメージをビルドしてプッシュする(1-b.)

まずは stable ブランチにコミットが push されたときにコミットハッシュ値をタグ名に持つ Docker コンテナイメージをビルドしてプッシュするアクションを追加します。 

docker で実行するコマンドを考え、そのコマンドを GitHub Action にすることにします。

```bash
$ docker build . --file Dockerfile --tag ＜Dockerユーザ名＞/vue_practice_app:＜Gitコミットハッシュ＞
$ docker login -u ＜Docker hubユーザ名＞ -p ＜Docker hubパスワード＞
$ docker push ＜Dockerユーザ名＞/vue_practice_app:＜Gitコミットハッシュ＞
```

カッコ内の値は GitHub Action から参照できるよう、Secrets 又は [GitHub Action のコンテキスト](https://help.github.com/ja/articles/contexts-and-expression-syntax-for-github-actions#contexts)を使います。

|項目|種別|参照方法|
| --- | --- | --- |
|＜Docker hubユーザ名＞|Secrets|${{ secrets.DOCKER_HUB_USERNAME }}|
|＜Docker hubパスワード＞|Secrets|${{ secrets.DOCKER_HUB_PASSWORD }}|
|＜Gitコミットハッシュ＞|GitHub Action変数|${{ github.sha }}|

※ `${{}}` で括られた内容は GitHub Action の[コンテキスト](https://help.github.com/ja/articles/contexts-and-expression-syntax-for-github-actions#contexts)です

Secrets を登録した後、Docker コマンドを GitHub Action にします。

```.github/workflows/docker_build_and_push.yml
name: Docker Image CI

on:
  push:
    branches:
    - stable

jobs:

  docker_build_and_push:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@v1
    - name: Build the Docker image
      env:
        # [TODO] ${{ github.repository }} から repository 名だけ抽出する
        IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
      run: docker build . --file Dockerfile --tag $IMAGE_NAME

    - name: Login to Docker hub
      run: docker login -u ${{ secrets.DOCKER_HUB_USERNAME }} -p ${{ secrets.DOCKER_HUB_PASSWORD }}

    - name: Push the Docker image to Docker hub
      env:
        # [TODO] ${{ github.repository }} から repository 名だけ抽出する
        IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
      run: docker push $IMAGE_NAME
```


### DB のマイグレーションを実行する(2.)

次に DB のマイグレーションを実行するアクションを追加します。

マイグレーションファイルが追加された場合は、DB マイグレーションを実行する必要があります。
マイグレーションファイルが変更されてない時にコマンドを実行しても影響はないため、リリーズ前には必ず DB マイグレーションを実行することにします。

kubectl で実行するコマンドを考え、そのコマンドを GitHub Action にすることにします。

```bash
$ kubectl run tmp-migrate -i --generator=run-pod/v1 --rm \
    --image ＜Docker hubユーザ名＞/vue_practice_app:＜Gitコミットハッシュ＞ \
    --overrides='{"spec":{"containers":[{"name":"app","image":"＜Docker hubユーザ名＞/vue_practice_app:＜Gitコミットハッシュ＞","command":["bash"],"args":["-c","rails db:migrate SECRET_KEY_BASE=$(rails secret)"],"envFrom":[{"secretRef":{"name":"vue-practice"}}]}]}}'
```

アクションを実行する条件ですが、先に作成したコンテナビルドアクションが成功した場合に実行すればよいのですが、Workflow を複数数珠つなぎにさせることは出来ません。[参考](https://help.github.com/ja/articles/events-that-trigger-workflows)

> 実行しているワークフローのアクションからは、新しいワークフローの実行をトリガーできません。 たとえば、リポジトリの GITHUB_TOKEN を使ってアクションがコードをプッシュしている場合、push イベントの発生時には、そのリポジトリが、新しいワークフローを実行するよう設定されているワークフローを含んでいる場合であっても、新しいワークフローは実行できません。

そこで Release を作成したタイミングでデプロイを行うように設定します。

尚、kubectl コマンドを実行する GitHub Action として [kubernetes-cli-kubectl](https://github.com/marketplace/actions/kubernetes-cli-kubectl) を使ってみました。

※ --overridesに指定するJSONパスを渡すときの文字列がそのまま使えずにダブルクォート `"` をエスケープする必要がありハマり使いづらさを感じました。本家の改善案や kubectl コマンドを実行できる Docker イメージを使うことで改善できる可能性があります

```.github/workflows/deploy.yml
name: Deploy docker container to kubernetes

on:
  release:
    types: [published]

jobs:

  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@master

    - name: migrate on cluster
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
      with:
        args: >
          run tmp-migrate -i --generator=run-pod/v1 --rm
          --image ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
          --overrides='{
            \"spec\":{
              \"containers\":[{
                \"name\":\"app\",
                \"image\":\"${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}\",
                \"command\":[\"bash\"],
                \"args\":[\"-c\",\"rails db:migrate SECRET_KEY_BASE=$(rails secret)\"],
                \"envFrom\":[{\"secretRef\":{\"name\":\"vue-practice\"}}]
              }]
            }
          }'
```


### Rails アプリケーションを新しいコンテナイメージで動作させる(3.)

次にアプリケーションを新しいコンテナイメージで動作させるアクションを追加します。

kubectl で実行するコマンドを考え、そのコマンドを GitHub Action にすることにします。

```bash
$ kubectl set image --namespace vue-practice --record \
    deployment/vue-practice app=＜Docker hubユーザ名＞/vue_practice_app:＜Gitコミットハッシュ＞
$ kubectl rollout status --namespace vue-practice deployment/vue-practice
```

これらをアクションにします。

```.github/workflows/deploy.yml
name: Deploy docker container to kubernetes

on:
  release:
    types: [published]

jobs:

  deploy:

    runs-on: ubuntu-latest

    steps:
    - uses: actions/checkout@master

    - name: migrate on cluster
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
      with:
        args: >
          run tmp-migrate -i --generator=run-pod/v1 --rm
          --image ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
          --overrides='{
            \"spec\":{
              \"containers\":[{
                \"name\":\"app\",
                \"image\":\"${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}\",
                \"command\":[\"bash\"],
                \"args\":[\"-c\",\"rails db:migrate SECRET_KEY_BASE=$(rails secret)\"],
                \"envFrom\":[{\"secretRef\":{\"name\":\"vue-practice\"}}]
              }]
            }
          }'

    - name: deploy to cluster
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
        # [TODO] ${{ github.repository }} から repository 名だけ抽出する
        IMAGE_NAME: ${{ secrets.DOCKER_HUB_USERNAME }}/vue_practice_app:${{ github.sha }}
        K8S_NAMESPACE: vue-practice
        K8S_DEPLOYMENT_NAME: vue-practice
        K8S_CONTAINER_NAME: app
      with:
        args: set image --namespace $K8S_NAMESPACE --record deployment/$K8S_DEPLOYMENT_NAME $K8S_CONTAINER_NAME=$IMAGE_NAME

    - name: verify deployment
      uses: steebchen/kubectl@master
      env:
        KUBE_CONFIG_DATA: ${{ secrets.KUBE_CONFIG_DATA }}
        K8S_NAMESPACE: vue-practice
        K8S_DEPLOYMENT_NAME: vue-practice
      with:
        args: rollout status --namespace $K8S_NAMESPACE deployment/$K8S_DEPLOYMENT_NAME
```


# CD環境の動作テストをする

次の機能を持つ CD 環境が構築できました。

* stable ブランチが更新(push) されると Docker コンテナイメージがビルドされる
    * タグ `stable` のコンテナイメージが Docker hub の autobuild 設定により自動でビルドされる
    * タグ `＜Gitコミットハッシュ値＞` のコンテナイメージが GitHub Action の Workflow によりビルドされる
* GitHub の Release が作成(published) されると GitHub Action の Workflow によりアプリケーションがデプロイされる

それぞれの機能について動作を確認してみましょう。

1. master ブランチを stable ブランチにマージする PR を作成してマージする
    * Docker hub を確認して `stable` タグと `＜Gitコミットハッシュ値＞` のイメージが追加されれば成功です
2. stable ブランチをリリースする
    * GitHub の Release タブから stable ブランチを指定してリリースします
    * タグは新規作成又は既存から選択します


# CI環境を構築する

CI環境を構築していきます。

CI環境で行う処理の概要は次のとおりです。

1. テスト DB のマイグレーションを実行する
2. minitest を実行する

コマンドは次のとおりです。

```bash
$ bundle install
$ bundle exec rails db:migrate
$ bundle exec rails test
```

アクションは次のようになります。

どのブランチでも実行するように `on: push` を指定します。

```.github/workflows/build_and_test.yml
name: Test

on: push

jobs:

  test:

    runs-on: ubuntu-latest
    container: ruby:2.5.3

    steps:
    - uses: actions/checkout@v1

    - name: Initialize
      env:
        RAILS_ENV: test
        DISABLE_SPRING: 1
      run: |
        # install tools
        curl -sL https://deb.nodesource.com/setup_10.x | bash -
        apt-get install -y nodejs
        npm install yarn@1.13.0
        gem install bundler -v 1.17.3
        # initialize DB
        bundle install
        bundle exec rails db:migrate

    - name: Test
      run: bundle exec rails test
```

アクションが出来たら master ブランチに PR をマージできる条件として、アクションがパスすることを追加しておきましょう。


# さいごに

これで Rails アプリケーションを開発するための CI/CD 構築は終わりです。

次はアプリケーションに OAuth 認証が出来るよう機能を追加することにします。