CircleCI 2.1とkubernetesで動作するアプリケーションの CI/CD 事始め

はじめに

この記事は CircleCI Advent Calendar 2019 の 19 日目の記事です。

拙稿となりますが Ruby on Rails, Vue.js によるモダン WEB アプリケーション 実践編 (その2) にて GitHub Action を使って k8s 上で動作する Ruby on Rails アプリケーションを CI/CD する Workflow を作ったので、同じ機能を CircleCI で作ってみることにします。

尚、Rails アプリケーションを前提としてますが、k8s の deployment manifest で定義されたアプリケーションであれば概ね流用できると思います。

なぜ CircleCI?

アドベントカレンダーのネタとして何かないかと探したことがきっかけです。(笑)

Wercker, CircleCI, GitHub Actions, GitLab CI/CD ではどれも YAML を定義することで CI/CD 環境を作ることが出来ます。
(CI/CD をオンプレ環境で構築する場合は Jenkins を使う環境があると思います)

そこで、1つ処理の実例を題材として GitHub Actions, CircleCI のそれぞれで違いを探してみようと思い立ったため CircleCI を使うことにしました。

前提事項

CircleCI が VCS リポジトリと連携できるよう初期設定が既に完了していることを前提としています。

初めて CircleCI の Workflow を追加する場合は、Getting Started を参考にして Workflow を設定してみて下さい。

構築する CI/CD の動作説明

CI では VCS レポジトリの全ブランチを対象に、コミットされた時点のコードに対してテストを実行するものとします。

CD では VCS リポジトリのデプロイ用ブランチを対象に、アプリケーションの Docker container image をビルド及び、k8s へデプロイするものとします。

CI の動作説明

CIの動作図

CI の動作は上図にあるように次のとおりです。

  1. 開発者VCSリポジトリ(ex.GitHub) に git push を実行
  2. VCSリポジトリ(ex.GitHub)CIツール(ex.CircleCI) に PUSHイベントを通知
  3. CIツール(ex.CircleCI) がテストを実行
  4. CIツール(ex.CircleCI)VCSリポジトリ(ex.GitHub) CI結果(テスト結果)を通知
  5. (opt) CIツール(ex.CircleCI)開発者 にCI結果(テスト結果)を通知

※ 今回「5. CIツール(ex.CircleCI)開発者 にCI結果(テスト結果)を通知」は設定しません。

CD の動作説明

CDの動作図

CD の動作は上図にあるように次のとおりです。

  1. 開発者VCSリポジトリ(ex.GitHub) にてPR/MRをマージ
  2. VCSリポジトリ(ex.GitHub)CDツール(ex.CircleCI) にマージ(又はブランチ更新)イベントを通知
  3. CDツール(ex.CircleCI) にてDockerイメージをビルド
  4. CDツール(ex.CircleCI)Dockerイメージレジストリ(ex.Docker Hub) にDockerイメージをプッシュ
  5. CDツール(ex.CircleCI)Kubernetes の k8sマニフェストを更新
  6. Kubernetes にてローリングアップデート
  7. CDツール(ex.CircleCI)Kubernetes にてローリングアップデートの成功を確認
  8. (opt) CIツール(ex.CircleCI)開発者 にCI結果(テスト結果)を通知

CircleCI の Workflow / Job を作成する

GitHub Actions から CircleCI に移行する際に参考になるドキュメントが migrating-from-github にあります。

GitHub Actions では 1 Workflow は 1 ファイルであり、Workflow の中では複数の Job が定義できます。Workflow 設定は .github/workflows 配下に保存します。
一方で CircleCI では複数の Workflow が 1 ファイルに保存され、複数の Job が定義できます。Workflow 設定は .circleci/config.yml に保存します。

CI 環境

まずは CI 環境を構築することにします。

GitHub Actions の CI Workflow

# .github/workflows/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

CircleCI の CI Workflow

# .circleci/config.yml
version: 2.1
jobs:
  test:
    docker:
      - image: ruby:2.5.3

    steps:
      - checkout

      - run:
          name: Initialize
          environment:
            RAILS_ENV: test
            DISABLE_SPRING: 1
          command: |
            # 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

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

workflows:
  ci:
    jobs:
      - test

GitHub Actions と CircleCI の比較

GitHub Actions と CircleCI のそれぞれの Workflow で CI 環境を実現する YAML ファイルを見てみましたが、内容はほぼ同じに記載することが出来ました。

記載内容に違いが出るのは、並列実行、キャッシュ利用、実行ホストのOS/リソース指定などを行おうとした時だと思われます。
CI として紹介した Workflow は非常に単純なものであったため差異は出ませんでした。

CD 環境

次に CD 環境を構築することにします。

GitHub Actions の CD Workflow

# .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

# .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

CircleCI の CD Workflow

Settings >> BUILD SETTINGS >> Environment Variables にて、環境変数を以下のとおり設定する必要があります。

名前 説明
DOCKER_HUB_USERNAME Docker Hub アカウントのユーザ名
DOCKER_HUB_PASSWORD Docker Hub アカウントのパスワード
KUBECONFIG_STR ※1 kubectl config の内容を base64 変換したもの ※2

※1 circleci/kubernetes の install-kubeconfig コマンドでは設定ファイルの内容を KUBECONFIG 環境変数で設定することが default なのですが、kubectl は KUBECONFIG 環境変数を設定ファイルへのパスとして使用するため KUBECONFIG 以外の名前にしましょう。(参考)

※2 echo -n "$KUBECONFIG" | base64 -d により復元できる値にすること

# .circleci/config.yml
version: 2.1

orbs:
  docker-orb: circleci/docker@0.5.19
  kube-orb: circleci/kubernetes@0.10.1

commands:
  migrate_vue_practice:
    parameters:
      image_name:
        type: string
    steps:
      - run: |
          kubectl run tmp-migrate -i --generator=run-pod/v1 --rm -n vue-practice --image << parameters.image_name >> \
            --overrides="'{ \
              \"spec\":{ \
                \"containers\":[{ \
                  \"name\":\"app\", \
                  \"image\":\"<< parameters.image_name >>\", \
                  \"command\":[\"bash\"], \
                  \"args\":[\"-c\",\"rails db:migrate SECRET_KEY_BASE=\$(rails secret)\"], \
                  \"envFrom\":[{ \
                    \"secretRef\":{ \
                      \"name\":\"vue-practice\" \
                    } \
                  }] \
                }] \
              } \
            }'"

jobs:
  test:
    docker:
      - image: ruby:2.5.3

    steps:
      - checkout

      - run:
          name: Initialize
          environment:
            RAILS_ENV: test
            DISABLE_SPRING: 1
          command: |
            # 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

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

  docker_build_and_push:
    machine:
      image: ubuntu-1604:201903-01

    steps:
      - checkout

      - run:
          name: Initialize
          command: |
            GIT_HASH=$(git rev-parse HEAD)
            echo "export GIT_HASH=${GIT_HASH}" >> $BASH_ENV
            echo "export IMAGE_NAME=${DOCKER_HUB_USERNAME}/vue_practice_app" >> $BASH_ENV
            source $BASH_ENV

      - run:
          name: docker login
          command: echo "${DOCKER_HUB_PASSWORD}" | docker login -u ${DOCKER_HUB_USERNAME} --password-stdin

      - docker-orb/build:
          image: $IMAGE_NAME
          tag: $GIT_HASH

      - docker-orb/push:
          image: $IMAGE_NAME
          tag: $GIT_HASH

  deploy:
    machine:
      image: ubuntu-1604:201903-01

    steps:
      - checkout

      - run:
          name: Initialize
          command: |
            GIT_HASH=$(git rev-parse HEAD)
            echo "export IMAGE_NAME=${DOCKER_HUB_USERNAME}/vue_practice_app:${GIT_HASH}" >> $BASH_ENV
            source $BASH_ENV

      - kube-orb/install-kubectl

      - kube-orb/install-kubeconfig:
          kubeconfig: KUBECONFIG_STR

      - migrate_vue_practice:
          image_name: $IMAGE_NAME

      - kube-orb/update-container-image:
          namespace: vue-practice
          resource-name: deployment/vue-practice
          container-image-updates: app=$IMAGE_NAME
          record: true

      - kube-orb/get-rollout-status:
          namespace: vue-practice
          resource-name: deployment/vue-practice

workflows:
  ci:
    jobs:
      - test

  cd:
    jobs:
      - test:
          filters:
            branches:
              only: stable
      - docker_build_and_push:
          requires:
            - test
      - deploy:
          requires:
            - docker_build_and_push

CircleCI Orbs を使う方法には .circleci/config.yml にて以下のようにします。

  • version: 2.1 以上にする
  • orbs の値として使用したい Orb を Hash 形式で指定する

    • Hash の key は job 内で使用する名前となる

    • Hash の value は公開された Orb のパスを指定する

      • @ をつけると Orb のバージョンを指定できる
    • 例) docker-orb: circleci/docker@0.5.19 を指定すると circleci/docker の version 0.5.19 が定義したコマンドが使えるようになり、Job の中で docker-orb/build のようにコマンドが指定できる

また、commands も version: 2.1 から使えるようになった設定です。
Job の中で run 等の代わりに使えるコマンドを定義できます。
今回 commands を使った理由は、kubectl run の override させる JSON パスに $IMAGE_NAME を展開しつつ生成できなかったため使うことにしました。

GitHub Actions と CircleCI の比較

GitHub Actions と CircleCI のそれぞれの Workflow で CD 環境を実現する YAML ファイルを見てみましたが、CI と同様に内容はほぼ同じに記載することが出来ました。

GitHub Actions も CircleCI も外部の Workflow/Job を流用できるため、同様の機能であれば、それらの使い方の違いはあるもののほぼ同じに記載することが出来ました。

作成した CI/CD の改善ポイント

  • npm, gem パッケージインストール後にキャッシュを有効にする
  • Docker イメージのビルドキャッシュを有効にする
  • CI/CD の実行結果をメールの代わりに Slack 通知する
  • CD にてデプロイする前に継続して実行するかどうかを人に尋ねる

キャッシュを有効にすることで Workflow を実行する時間が短縮できるようになることが期待できます。

最後に - GitHub Actions と CircleCI の違いについての所感

今回はせっかくなので version 2.1 から使えるようになった機能である CircleCI Orbs, Commands を使ってみました。

CircleCI の Marketplace と同様の仕組みとして CircleCI Orbs なるものがあると記事を執筆していて知りました。

CircleCI Orbs を知る前は GitHub Actions との大きな差異として、外部の Workflow が利用できる点だと思っていたので、version 2.1 の新機能を知ったことで GitHub Actions とはほぼ差異はなく移行できる印象を持ちました。

敢えて違いとなることを挙げると次のとおりかと感じてます。

GitHub Actions の Marketplace の手軽さ、GitHub Actions では JavaScript を使ってアクションを実装することが出来るため自由度やメンテナンス製が高いように感じます。

一方で CircleCI には CLI があるため、開発をする際にはわざわざ VCS リポジトリに commit/push せずに開発・デバッグ出来るメリットがあると思います。

関連記事

Kubernetes時代のCI/CD Jenkins Xとは?-前編

DockerfileとKubernetesHelmChartを入れる

Kubernetes+Let’s Encryptでワイルドカード証明書の自動発行基盤を作る