はじめに
この記事は 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 の動作は上図にあるように次のとおりです。
- 開発者 が VCSリポジトリ(ex.GitHub) に git push を実行
- VCSリポジトリ(ex.GitHub) が CIツール(ex.CircleCI) に PUSHイベントを通知
- CIツール(ex.CircleCI) がテストを実行
- CIツール(ex.CircleCI) が VCSリポジトリ(ex.GitHub) CI結果(テスト結果)を通知
- (opt) CIツール(ex.CircleCI) が 開発者 にCI結果(テスト結果)を通知
※ 今回「5. CIツール(ex.CircleCI) が 開発者 にCI結果(テスト結果)を通知」は設定しません。
CD の動作説明
CD の動作は上図にあるように次のとおりです。
- 開発者 が VCSリポジトリ(ex.GitHub) にてPR/MRをマージ
- VCSリポジトリ(ex.GitHub) が CDツール(ex.CircleCI) にマージ(又はブランチ更新)イベントを通知
- CDツール(ex.CircleCI) にてDockerイメージをビルド
- CDツール(ex.CircleCI) が Dockerイメージレジストリ(ex.Docker Hub) にDockerイメージをプッシュ
- CDツール(ex.CircleCI) が Kubernetes の k8sマニフェストを更新
- Kubernetes にてローリングアップデート
- CDツール(ex.CircleCI) が Kubernetes にてローリングアップデートの成功を確認
- (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とは?-前編