はじめに
BookLiveでは2020年末にEKSを導入して、既にいくつかのアプリケーションがEKS上で本番稼働しています。
この記事では、そんなBookLiveがEKSへのデプロイをどのようにやっているか、事例を紹介したいと思います。
「既にゴリゴリK8sを運用しています」というような方には参考にならないと思いますが、これから導入しようとされている方には多少なりとも参考になるかもしれません。
基本的に使うもの
- GitHub Actions
- aws-cli
- kubectl
まずはアーキテクチャ
まずはともあれアーキテクチャを紹介します。
はい、書くまでもなかったですね(笑)。
とはいえ、多少なりとも特徴を抽出して紹介します。
- GitHub Enterprise(以下、GHE)を使用しているが、Self hosted版である
- したがって、GitHub Actionsのrunnerインスタンス(EC2)もBookLiveのVPC内にある
- IAMの権限はrunnerインスタンスのIAM Roleを適用するようにActionsを実装している
- GitリポジトリはK8sのマニフェストとアプリケーションコードで分けている
- K8sマニフェスト(インフラ)とアプリケーションでリリースサイクルが違うため、分ける方がCI/CDが組みやすい
- 逆に分けないと、gitのコミットログ・タグがどちらに属するものか分かりづらくなって、リポジトリのメンテナンス性が下がる
言うほど特徴的なものはありませんが、軽く頭に入れておいてもらえると以降の内容がわかりやすいかと思います。
また、インフラとアプリケーションでリポジトリが分かれていると書いていますが、メンテナも分かれており、前者はインフラを管理しているSREチーム、後者はブックライブサービス等を開発しているアプリケーション開発のチームに裁量があります。
僕が所属しているのはプラットフォーム開発チームで後者のチームに該当します。
したがって、今回の記事におけるデプロイとはインフラとしてのK8sリソースの更新ではなく、アプリケーションの更新を意味します。
「インフラリソースの更新」はkubectl apply
、「アプリケーションの更新」はkubectl rollout
という意味で使っています。
なぜGitHub Actionsにした?
まず結論から申し上げますと、**「必要十分だったから」**という理由です。
前提としてEKSへのデプロイは「IAMの権限がある」というのが十分条件で、それさえあればkubectlでデプロイが可能です。
それに加え、やりたかったことをまとめると
- 統合環境はrelease candidate(rc)、staging(stg)、production(prod)の3つがあり、デプロイの方法を環境ごとに変えられる必要がある
- rcとstg環境にはgitのmergeのタイミングで自動でデプロイしたい
- git flowのアプリケーションが多く、developブランチはrc、master(main)ブランチはstgおよびprod
- prod環境には任意のタイミングで手動でジョブを実行するような形式でデプロイしたい
- デプロイの実行ログ(誰がいつ何をしたか)を残したい
この程度のものでした。
最初はArgo CD入れるぞとか、stgへ自動デプロイ後はprodデプロイ前に承認を挟む形でSpinnakerとかでパイプラインを構築してみる?、とか意気込んでたんですが、SREチームと相談して、最初から仰々しくやらずに必要最低限の準備と運用で済むGitHub Actionsでいいっしょと帰結しました。
元々BookLiveではCIやリリースジョブにJenkinsを使用していたので、従来とほぼ同じ思想でジョブを構築できるというのも大きかったです。
Jenkinsは今でも大活躍していて、かつCI/CDはアプリケーションのメンテナに裁量があるので、今ではこの記事で紹介するやり方をJenkinsから実行していたりもします。
後ほど紹介するrollout status
は監視ジョブとしてJenkinsで作られたものを、GitHub Actionsにも組み込んだりしてます。
補足を書いておきますが、Code PipelineはGHE Self-hostedに対応していないのでそもそも候補に入れませんでした。
GHE Self-hostedでも使おうと思えば使えますが(実際BookLiveでも使用している箇所はある)、多少周りくどいやり方をする必要があります。
GitHub Actionsコード紹介
今回は、mainブランチへのmergeが完了後にstgとprodのEKSクラスタにデプロイするためのActionを紹介します。
多少実際のコードとは違いますが、エッセンスが伝わるように編集しています。
Marketplaceからの輸入
コードを紹介する前にMarketplaceから輸入したコンポーネントたちを紹介しておきます。
Actions名 | 内容 |
---|---|
Slack Notify (rtCamp) | 決められたテンプレートに値を入力するだけでSlack通知ができる |
Kubectl tool installer (Azure) | kubectlを使えるようにダウンロードしてくれる |
コード例 (stg)
mainブランチのリビジョンが動いたら自動で動くActionです。
このアクションでは次の内容が実行されます。
追って詳しくジョブの内容は説明します。
- Slackにリリース開始の通知
- コンテナイメージのビルドをした後、ECRにイメージをpushする
- 成功・失敗に関わらずSlack通知(色でステータスを見分けられる)
- stgクラスタに
rollout restart
を実行して、成功したらその旨をSlack通知 -
rollout status
で待機し、4も含め成功・失敗に関わらずSlack通知 - Slackにリリース完了の通知
name: Release stg
on:
push:
branches:
- main
jobs:
start:
runs-on: [ self-hosted, stg ]
steps:
- name: Slack notification start
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: slack_channel_name
SLACK_USERNAME: App CI/CD
SLACK_ICON_EMOJI: ':emoji:'
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: 'START STG RELEASE'
build:
needs: start
runs-on: [ self-hosted, stg ]
steps:
- uses: actions/checkout@v2
- name: Generate tag
id: initialize
run: |
rev=$(git rev-parse HEAD)
echo "::set-output name=TAG::$rev"
- name: Build image
run: ./build.sh -t ${{ steps.initialize.outputs.TAG }}
- name: Push imeage
run: |
./publish.sh -t ${{ steps.initialize.outputs.TAG }}
- name: Slack notification push complete
if: ${{ always() }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: slack_channel_name
SLACK_USERNAME: App CI/CD
SLACK_ICON_EMOJI: ':emoji:'
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: 'PUSHED STG IMAGE'
deploy:
needs: build
runs-on: [ self-hosted, stg ]
steps:
- uses: actions/checkout@v2
- name: Update kubeconfig
run: aws --region ap-northeast-1 eks update-kubeconfig --name stg-cluster-name --kubeconfig ./kubeconfig
- uses: azure/setup-kubectl@v1
- name: Rollout
run: |
kubectl --kubeconfig=./kubeconfig -n app rollout restart deployment app-deployment
- name: Slack notification applied
if: ${{ success() }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: slack_channel_name
SLACK_USERNAME: App CI/CD
SLACK_ICON_EMOJI: ':emoji:'
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: 'APPLIED STG ROLLOUT'
- uses: azure/setup-kubectl@v1
- name: Check status
run: |
kubectl --kubeconfig=./kubeconfig -n app rollout status deployment app-deployment
- name: Slack notification complete
if: ${{ always() }}
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: slack_channel_name
SLACK_USERNAME: App CI/CD
SLACK_ICON_EMOJI: ':emoji:'
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: 'COMPLETE STG ROLLOUT'
finish:
needs: deploy
runs-on: [ self-hosted, stg ]
steps:
- name: Slack notification finish
uses: rtCamp/action-slack-notify@v2
env:
SLACK_WEBHOOK: ${{ secrets.SLACK_WEBHOOK }}
SLACK_CHANNEL: slack_channel_name
SLACK_USERNAME: App CI/CD
SLACK_ICON_EMOJI: ':emoji:'
SLACK_COLOR: ${{ job.status }}
SLACK_MESSAGE: 'FINISHED STG RELEASE'
それでは各jobの説明を書いていきます。
start
ここで重要なのは2つあります。
一つ目はruns-on
の設定でこの設定は全てのjobに記載する必要があります。
重要なのはprodの部分で、stg環境用のIAMロールが適用されているrunnerで実行することを指定しています。
もう一つは${{ secrets.SLACK_WEBHOOK }}
です。
これはSlackのincoming webhookのurlをあらかじめGitHubのSecretsに登録しておいて、それを利用するようにしています。
build
以降のjobにも共通しますが、まずなんといっても大事なのはneeds
です。
今回のjobは全て直列に実行する必要があるので、needsに前のjob名を指定することで、そのjobが完了した後にjobを開始するよう制御できます。
逆に、指定しなければ並列で動きます。
このjobで重要なものは特にないのですが、プラクティスのひとつになればいいかなと思い、内容だけ軽く共有します。
まず、build.sh
とpublish.sh
はBookLiveではよく使うのですが、前者はスクリプト内でdocker imageをビルドし、後者はECRへのpushを行います。
特に前者はローカルでビルドするときも使えるように実装しておくと便利です。
次に特徴的なのは
rev=$(git rev-parse HEAD)
echo "::set-output name=TAG::$rev"
と、それを引数としてスクリプトに渡す
-t ${{ steps.initialize.outputs.TAG }}
かなと思います。
ここでやりたいのは、ECRにgitのリビジョンのタグをpushするというものです。
これをやっておくことで、latestやその他のエイリアスタグがどのリビジョンを指しているのかをECRを見ることですぐ分かるようになります。
そのためにGitHub Actions内で対象コードのHEADが指しているリビジョン番号を抽出し、スクリプトにそのタグを生成するようにタグ名を指定しているというような感じです。
deploy
ここでようやくEKSの話です。
このjobのエッセンスは2つあります。
一つ目はaws eks update-config
を使ってカレントディレクトリにEKSの.kube/config
相当のファイルを生成し、それを使ってkubectl rollout
を実行するというものです。
カレントディレクトリに出力するというのが重要で、グローバルな.kube/config
にしないことで、リポジトリ間やAction間で影響を与えることなくkubectlを実行できます。
もう一つは、rollout status
です。
rollout restart
はそれを実行した時点で即時にステータスが返ってくるので、コマンドの終了 = restartの完了
ではありません。
このrestartが完了しないとPodの入れ替えは完了していないので、本当の完了時にSlack通知をするためにはPodの入れ替えを監視してあげる必要があります。
これを実現してくれるのがrollout status
で、rollout実行中の場合、それが完了するまで待機し、成功すればステータスコード0を返してくれます。
その後にSlack通知をすることで、本当のPod入れ替え完了をただ待っておくだけで知ることができます。
finish
特に重要なことはなく、Slackに全てのjobが完了したことを通知しているだけです。
コード例 (prod)
ほぼstgと一緒ですが、冒頭で紹介したやりたいことの中に、本番環境では任意のタイミングでリリースをしたい(デプロイをしたい)と書いていました。
これを実現してくれるのが、workflow_dispatch
です。
workflow_dispatch
を使うとActionを任意のタイミングで実行できるようになります。
name: Release prod
on:
workflow_dispatch:
その他大事なことは、
- prod環境用のIAMロールが適用されているrunnerでActionを実行すること
-
eks update-kubeconfig
でprod環境用のクラスタ名を指定すること
くらいです。
今後の展望
aws-cliのeks update-kubeconfig
はEKSの利用を汎用的にしてくれます。
そのため、kubectlがあればコマンドラインからEKSにデプロイできるようになります。
例えば、docker-compose run
で実行しているものをkubectl run
に置き換えるだけで良いと考えると、K8sの利用を促進できると思いませんか?
実際、BookLiveではバッチのスケジューラにdigdag、実行環境はDockerがインストールされたEC2群、そこに対して、docker-compose -H ${host ip} run
でバッチを実行しています。
スケジューラを移行するのは相当な手間や移行時のリスクが伴うので、スケジュール設定はそのまま、実行環境をK8sに移行してクラスタ化できると考えると非常に魅力的に感じています。
おわりに
予告にはIAM関連の記事を書くと書いておりましたが、当初予定していた題材の準備が間に合わず、急遽今回の内容にしました。
すみません、、
どなたかひとりでも参考になったのであれば幸いです。
良いお年を。