こんにちは!!
8月からZEALSにジョインしたぱんでぃーです!TypeScriptとAureliaを愛するバックエンドエンジニアとして、現在はGolangでMicroservices化を目指したAPIを開発しています!
このAPI開発のプロジェクトについても、近いうちにブログで公開していきたいのですが、今回はKubernetes環境でのCI/CDについてご紹介します。(おもにCD)
このトピックについては、以前、CTO島田のエントリーでも少し紹介していましたが、さらに進化させた内容となっております!
TL;DR
- Kubernetes導入初期で最適なCDパイプラインを構築するハードルが高い・・・
- できるだけミニマムスタックでラーニングコストの小さいCI/CDパイプラインとは!?
- GitOps likeなCIOpsで安全かつ高速なデプロイサイクルを実現!!
KubernetesエコシステムのCDツールの現状
最近ではWeb系、メーカー系、金融系など、業界を問わずKubernetesを採用する事例が増えてきましたね。7月末に開催されたGoogle Cloud Next '19 in Tokyoでも、たくさんの企業がKubernetes導入について発表していました。
そういったセッションを聞いていると、どの企業さんも共通して頭を悩ませている点があるようです。その内の一つがKubernetes環境でのCD(Continuous Delivery)パイプラインの構築です。
CDパイプラインの構築フローは以下のようになると思いますが、特に2つ目が悩みどころです。
- CI/CDパイプラインのジョブフローの設計
- パイプラインを実現するためのCDツールの選定
- CIでのビルドやテストジョブ、CDでのデプロイジョブの作成
1のジョブフローの設計については、GitOpsが現状のベストプラクティスとなっているため、まずはこれに従うのが良いでしょう。
https://www.weave.works/technologies/ci-cd-for-kubernetes/
GitOpsについては、下記のブログエントリーに定義から導入の利点、具体的な構築手順まで、とても分かりやすくまとまっているため、是非一読することをオススメします。
GitOpsで悩むポイントとしては、既に運用中のGitブランチの運用ルール(Git Flow、GitHub Flow、GitLab Flowなど)に、どのように統合するか?という点でしょう。具体的には、どのGitイベント(MergeやTagのプッシュなど)をトリガーとして、各環境(Dev、Staging、Production)にデプロイを行うか?という点です。これについては、エントリー後半でZEALSでの運用例と共にご紹介します。
そして、2のCDツールの選定ですが、現状ではこの分野はまさに群雄割拠といった感じで、日々新しいツールが発表されています。メジャーどころですと、以下が挙げられます。
最近では、Tektonも注目を集めているようですね。
これだけの数があると、一通り各ツールのドキュメントに目を通して、比較検討するだけでもかなりの労力が必要です。また、パイプラインはCRD(Custom Resource Definitions)として実装されているものが多いため、Kubernetes自体の知識もある程度求められます。
このため、Kubernetes導入の初期フェーズでのハードルになりがちです。
とはいえ、CDパイプラインを構築しないままにしておくと、以下のような問題が発生します。
- Kubernetesのコンテキストを間違ってしまい、意図した環境とは異なるマニフェストをデプロイしてしまう。
- Gitでコード管理されていない変更が行われており、意図が分からない。
- デプロイ作業が属人化してしまい、新規メンバーの学習コストが高くなる。
主に、開発者が直接 kubectl
コマンドを実行することに起因する問題ですね。これらを放置しておくと、せっかくKubernetesを導入したのに、かえって運用が煩雑になり、オペレーションミスのリスクが高くなってしまいます。
ここまでを要約すると、
- Kubernetes導入初期で最適なCDパイプラインを構築するハードルが高い。
- 一方、マニュアルでのオペレーションはリスク大・・・
Kubernetesに精通しているメンバーが多いチームや潤沢な導入リソースを得られる環境であれば良いのですが、そうではないケースの方が圧倒的に多いでしょう。
このエントリーの後半では、そのような開発組織の助けになるような、
- ラーニングコストが低く
- 複雑性をできるだけ排除し
- かつ、GitOpsの恩恵を受けられるような
ミニマリストのためのCDパイプラインをご紹介します。
できるだけミニマムスタックでスタートするためには?
まずは前提条件、目指すゴールをお伝えします。
Gitブランチ戦略
GitブランチはGitHub Flowを前提として進めます。
master
ブランチとfeature
ブランチだけのシンプルなモデルです。
(既にGit FlowやGitHub Flowのようなモデルで開発を行っている場合でも、デプロイをトリガーするGitイベントを変更するだけで、これから紹介するCDパイプラインを導入可能です。)
また、アプリケーションのGitレポジトリとKubernetesマニフェストのレポジトリは別で管理します。
アプリケーションの実行環境
アプリケーションのデプロイを行う環境は、Dev、Staging、Productionを対象とします。
ZEALSではKubernetesのマネージドサービスとしてGKEを利用しており、各環境はGCPプロジェクトごとに作成しています。そのため、各プロジェクトごとに同じ構成のKubernetesクラスターを稼働させています。(厳密にはDev環境ではPreemptibleインスタンスを使用するなど、細かな差異はあります)
Kubernetesに限れば、Namespaceで各環境を分けるという手もありますが、その他のGCPサービスも考慮すると、プロジェクトごとに環境を分けた方が構成管理しやすいという利点があります。
目指すゴール
一言で表すと、GitOps LikeなCIOps です。
コンセプトとしてはGitOpsに従いつつ、一部、CIOpsの問題点は許容するいった感じです。CIOpsという言葉は聞き慣れていないかもしれませんが、要するにCIツールでビルドとテストを行ったあと、続けてKubernetesクラスターにデプロイを行うことです。より詳しい解説については下記の記事を御覧ください。
さて、GitOps LikeなCIOpsとは、具体的にどういうことでしょうか?
まず、GitOpsが提唱する以下の原則には必ず従います。
- アプリケーションやミドルウェアの設定は、Gitレポジトリで管理しているものが絶対的な正であり、差異が発生しないようにする (HPAによるPod数の差異などは除く)
kubectl
を直接使用してデプロイを行わない
その一方で、上記の記事でも『アンチパターン』と指摘されていたCIOpsを部分的に採用します。
具体的には、GitOpsが指摘しているCIツールがデプロイを行うためにクレデンシャルを渡してしまうという点を許容します。ここで言うクレデンシャルは、CIツール用に作成したService AccountのJSONファイルです。これにはKubernetesマスターのAPIへ命令を送ることができるとても強力な権限が付与されています。
しかし、そもそも『クレデンシャルをCIツールに渡してセキュアに管理すること』は、Kubernetes以前でも当たり前に行っている運用です。インパクトは大きいですが、発生するリスクは低く抑えられるはずです。
そして、使い慣れたCIツールをKubernetes環境でのCDに利用することで、**初期フェーズでのラーニングコストを最小限に抑えられます。CIジョブの中でkustomize
やkubesec
、kubectl
といったマニュアル時に使用していたコマンドを実行するだけなので、ブラックボックス化もしません。
これらのコンセプトにより、シンプルさをできるだけ保ったまま、安全かつ高速にアプリケーションの開発サイクルを回していくことができるようになります。開発者が行うことはGitのマージ作業や新バージョンのタグを打つだけです。
ZEALSの実際のCI/CDパイプラインを紹介
まずはCI/CDパイプラインのフローを掴んでもらうために、下記のシーケンス図を御覧ください。(矢印の実線は開発者が行う作業、点線は自動実行される処理です)
- Feature: アプリケーション用GitレポジトリのFeatureブランチ
- Master: アプリケーション用GitレポジトリのMasterブランチ
- k8s Manifest: Kubernetesマニフェスト用GitレポジトリのMasterブランチ
フロー自体は特段変わったところは無いと思いますが、各環境へのデプロイは下記のようにGitイベントがトリガーとなっています。
Gitイベント | デプロイ環境 | |
---|---|---|
FeatureブランチのPush | => | Devクラスター |
MasterブランチへのMerge | => | Stagingクラスター |
GitタグをPush | => | Prodクラスター |
言及すべき点としては、トリガーは全てアプリケーションのGitレポジトリへのイベントとなっていることです。そのため、アプリケーションのバージョンアップの際は、開発者はk8sマニフェスト用のレポジトリを意識する必要がありません。このレポジトリを修正するのは、ConfigMapやSecretとして定義している環境変数やIngressなどの設定情報を更新する時だけです。
おや・・・?
すると、Deploymentのマニフェスト内で定義しているはずのコンテナイメージのタグはどこで更新しているのか?という疑問が残りますよね。
答えはSkaffoldです。
SkaffoldはKubernetes環境でのアプリケーション開発を支援するためのツールで、Dockerイメージのタグ打ちやビルド、プッシュ、k8sマニフェストのデプロイといった一連のルーティーンをまとめて実行してくれます。その中の機能の一つにイメージのタグを自動で付与する機能があります。skaffold
コマンド実行時のGit情報を元に、以下のようなタグが無い状態のマニフェストにタグを付与してくれます。(先程のシーケンス図のapp:COMMIT_HASH
やapp:v0.1.2
がそれです)
apiVersion: apps/v1 kind: Deployment metadata: name: app spec: template: spec: containers: - name: app image: gcr.io/${PROJECT_ID}/app
この点に関してはツールに依存した処理となっていますが、数行のShellスクリプトを書けば同じことができるため、『ブラックボックス』という程ではないと考えています。skaffoldが内部でやっている事もdocker
コマンドやkubectl
コマンドを順次実行しているだけで、標準出力もそれぞれのコマンドのものなので、エラー時にも特別なノウハウを必要としません。
こうして、シンプルさと利便性を天秤にかけながら色々と試行錯誤した結果、下記のようなCIジョブでデプロイする形となりました。(例はCircleCIのconfig.yml
です。要点を絞るため『ビルドして、Prod環境へデプロイするジョブ』だけを抜粋しています。install-cli
などはCircleCIのcommands
として定義しています)
version: 2.1 orbs: slack: circleci/slack@3.3.0 gcp-cli: circleci/gcp-cli@1.8.2 executors: gcloud: docker: - image: google/cloud-sdk golang: docker: - image: circleci/golang:1.12 environment: GOCACHE: /go/.cache/build GO111MODULE: "on" jobs: build: executor: golang steps: - checkout - restore_go_modules_cache - run: name: Build Go Artifacts command: | CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \ go build -a -installsuffix cgo -o artifacts/main cmd/app/main.go - persist_to_workspace: root: artifacts paths: [ 'main' ] deploy: executor: gcloud steps: - setup_remote_docker - checkout - gcp-cli/install - gcp-cli/initialize - install-cli: cli: skaffold url: https://storage.googleapis.com/skaffold/releases/${VERSION}/skaffold-linux-amd64 version: latest - install-cli: cli: kustomize url: https://github.com/kubernetes-sigs/kustomize/releases/download/v${VERSION}/kustomize_${VERSION}_linux_amd64 version: "3.1.0" - install-cli: cli: kubesec url: https://github.com/shyiko/kubesec/releases/download/${VERSION}/kubesec-${VERSION}-linux-amd64 version: "0.9.2" - run: name: Clone Kubernetes Manifest command: git clone --depth 1 -b master git@github.com:${USER}/${REPO}.git - attach_workspace: at: artifacts - run: name: Build and Push Docker Image command: | gcloud container clusters get-credentials ${GKE_CLUSTER_NAME} --region ${GCP_REGION} skaffold run -p ${ENV} workflows: version: 2 deploy-on-production: jobs: - build: context: prd filters: branches: ignore: /.*/ tags: only: /^v\d+(\.\d+){2}$/ # SemVer style tag like v0.12.3 - deploy: context: prd requires: - build filters: tags: only: /^v\d+(\.\d+){2}$/ # SemVer style tag like v0.12.3
今後の伸びしろ
上記のCDパイプラインでは、Featureブランチがプッシュされた際にはDev環境の同一のDeploymentが更新されてしまいます。次のフェーズではそれぞれ別のNamespaceにデプロイを行い、Pull Requestごとに動作検証を行えるようにする予定です。
また、頻繁にリリースが発生するプロジェクトでは、リリースタイミングの調整を行うために、GitLab Flow のようなモデルに変更することも検討しています。デプロイのトリガーを変更するだけで、GitHub Flowよりも柔軟なリリース管理が実現できます。
https://docs.gitlab.com/ee/workflow/gitlab_flow.html
そして、今回紹介したミニマリストのためのCD戦略では、カナリアリリースや障害時のロールバック、マニフェストと実際の値の変更検知などの、本格運用に必要な機能が不足しています。そのため、ある程度Kubernetes運用に慣れたタイミングで、より高機能なCDツールの導入も検討したいと思います。
個人的には、必要な機能が備わっており、ドキュメントも充実しているArgoCDが有力候補です。(とはいえ、エコシステムの変化が激しいので、ちょっと目を話すと情勢が変わりそうですね・・・笑)
さいごに
Kubernetesを中心としたCloud Nativeのエコシステムは変化も激しく、まだまだベストプラクティスが出揃ってない領域も多いです。今回ご紹介したCDに関する運用プラクティスも順次アップデートしていく必要があるでしょう。
ZEALSでは、このような混沌とした環境でも目をキラキラさせながら共に成長していける、飽くなき探究心を持ったメンバーを募集しています!!
長くなりましたが、最後まで読んで頂きありがとうございました!!
それでは次回のKubernetes連載でお会いしましょう!