マージボタン1つで本番適用するための仕組み

こんにちは、Yakumoチーム兼コネクト支援チームの@ueokandeです。

本日はYakumoチームで構築した、デプロイパイプラインとその工夫について紹介します。 Yakumoプロジェクトはグローバル市場向けに、kintone.comをAWSから提供することを目指すプロジェクトです。 これまで日本のデータセンターから提供していたkintone.comを、現在AWS上に移行しています。 プロジェクトのもう1つのゴールとして、開発・運用体制を見直してクラウドネイティブなリリースプロセスを確立するというのがあります。

このプロジェクトは、国内向けのcybozu.comと完全に切り離されて開発がスタートしました。 ゼロからリリースフローを作るということで、これまでのインフラの経験や反省点を活かしつつ、チームの理想的なデプロイパイプラインの構築を目指しました。 そして最終的には、マージボタン1つで本番適用できるデプロイパイプラインを構築しました。 この記事ではYakumoチームのデプロイパイプラインと、それに至るまでの経緯を紹介します。

kintone.comのインフラ構成

kintone.comは異なる責務を持ったいくつかのサービスから構成されます。 たとえば、マルチテナントを実現するサービスや、非同期ジョブを処理するサービスなどです。 各サービスはKubernetes (Amazon EKS) を使ってデプロイされます。 それぞれのサービスが利用するデータベースやストレージは、AWSのマネージドサービスを利用します。

kintone.comのインフラ構成
kintone.com on AWSのインフラ構成

理想のデプロイパイプライン

インフラ構成の更新や本番オペレーションでは、運用するチームにとって負担が少ないことが理想です。 これまでの国内インフラの運用経験で、オペレーションを自動化するだけでは管理者の負担が減らないというのがわかっていました。 Yakumoプロジェクトでは、KubernetesやTerraformで採用された宣言的モデルという考えに基づいて、デプロイパイプラインを構築しました。

ソースコードは構築したいインフラの構成を表し、常に本番環境の設定はソースコードを見ればよいという状態を維持します。 そしてインフラの構成やアラートの閾値調整も、ソースコードを変更してマージするというフローにして、本番オペレーションを最小限に抑えました。

kintone.comのデプロイパイプラインの図
kintone.comのデプロイパイプライン
宣言的モデルをインフラに適用するのに必要なのが、差分検知と差分を埋めるオペレーションです。

差分検知とオペレーション

宣言的モデルでは、管理者が定義した状態を元に、実際のインフラを定義された状態に収束させます。 システムは、実際のインフラの状態と定義された状態との差分を検知し、その差分を埋めるオペレーションを実行します。

AWS CloudFormationやKubernetesは、この仕組を備えています。 ユーザーが定義したマニフェストファイルをCloudFormationやKubernetesに与えると、その定義ファイルに基づいたインフラの構築やコンテナをデプロイします。 またマニフェストファイルで何らかのパラメータを変更すると、対象リソースや関連するリソースに新しい設定を反映します。

一方でインフラレイヤーだけでなく、Yakumoチームが開発しているサービス群のデプロイもあります。 こちらもソースコードと本番環境の状態を常に一致させたいです。 サービスのソースコードを更新すると、新しいバージョンのイメージをKubernetes上にデプロイしたいです。 サービスの更新を検知するために、コンテナのイメージタグにソースコードのハッシュ値を採用しました。

ソースコードからイメージタグを生成

それぞれのサービスのイメージタグは、バージョン番号vX.Y.Zなどではなく、ソースコードから計算したハッシュ値を使います。 このソースコードには、サービス本体のコードだけでなく、コンテナに同梱するスクリプトやDockerfileも含みます。 ソースコードやDockerfileが更新されると、新しいイメージタグでコンテナレジストリに登録されます。

Kubernetesに適用するマニフェストは、レポジトリに含まれるソースコードからハッシュ値を計算して、イメージタグを埋め込みます。 ソースコードに更新があったサービスは新しいイメージタグが割り当てられ、 Kubernetesマニフェストの適用 (kubectl apply) でサービスのコンテナが更新されます。 開発者は、現在サービスのどのバージョンがデプロイされているかを考える必要がなくなり、本番環境は常にレポジトリにあるソースコードがデプロイされる事になります。

spec:
  containers:
    - name: my-service-a
      # 575C70E78FE448BC973EB46A46F4AE8Bなどのイメージタグが適用される
      image: quay.io/cybozu/my-service-a:{{ tag "my-service-a" }}

ケーススタディ: コンテナのベースイメージ更新

このしくみにより本番適用の自動化が容易になり、マージボタン1つで本番適用できるしくみができました。 ここで1つ例として、コンテナのベースイメージの更新を考えます。

例えばJVM用にビルドされたサービスの、Javaのバージョンの更新を考えます。 Yakumoチームでは管理の容易化のために、JVMサービスでは同じベースイメージを利用します。 セキュリティパッチなどで新しいJavaのバージョンがリリースされると、そのたびに本番環境のサービスを更新する必要があります。

一般的なベースイメージの更新作業は以下のとおりです。

  1. べースイメージのDockerfileを更新
  2. ベースイメージのリビルド (docker build)
  3. ベースイメージをコンテナレジストリに登録 (docker push)
  4. 各種サービスをリビルド (docker build)
  5. 各種サービスをコンテナレジストリに登録 (docker push)
  6. 各種サービスを本番環境に適用 (kubectl apply)

Yakumoチームのリリースフローでは、手順1.のDockerfileを更新してマージするだけです。

ベースイメージもまた、ハッシュ値のイメージタグを持ちます。 各サービスはベースイメージのタグを参照するため、ベースイメージが更新されると、それを利用するサービスも更新されます。 そして新しいイメージタグを持つサービスが本番環境に適用され、作業は完了です。

コンテナのベースイメージの更新と本番適用
コンテナのベースイメージの更新と本番適用

まとめ

以上がYakumoプロジェクトで取り組んだ、リリースフローとデプロイパイプラインの紹介でした。 宣言的モデルにより、「インフラやサービスの状態を確認し、影響範囲を考慮しながら、適用手順を作成する」という難しいフローも発生しません。 このリリースフローを確立したことで、本番環境への適用オペレーションというのがほぼなくなりました。 masterマージと本番適用が同義となり、切り戻しもgit revertするだけ、というフローです。

この理想のデプロイパイプラインも、一朝一夕で構築できたわけではありません。 これまでのcybozu.comの知見や、チーム内で積み重ねてきた改善活動によって現在の形に至りました。 このリリースフローで退屈な適用オペレーションを省けるだけでなく、製品の価値をより早くユーザーに届けることにも繋がります。

みなさんも、同じオペレーションを繰り返してるなと感じた時は、理想のリリースフローを探求してみてはいかがでしょうか?