もうリリースは怖くない ― 大きな変更を安全に本番適用するTips

こんにちは、AWS版kintoneのDevOpsエンジニアをしている@ueokandeです。 AWS版kintoneは2019年9月のローンチから現在まで、幾度となく機能改善をしてきました。 ローンチ当時よりも利用者が増え、スケーラビリティのために内部設計を大きく変更することもあります。 先日公開したメール送信の設計変更もその1つです。

blog.cybozu.io

安定運用のために必要なリリースではありますが、実装を大きく変えることで不具合混入のリスクもあります。 それだけではなく、パフォーマンス改善のつもりが、本番環境に投入して逆にパフォーマンス低下が発覚するというケースもあります。 この記事では、大きな変更を安全にリリースするためのTipsを紹介します。 記事の最後ではSpring Bootの実装例と、Kubernetesでの実現方法も紹介します。

切り戻し戦略

大きな変更を安全にリリースする方法として、Canary ReleaseBlue/Green Deploymentが有名です。 The Site Reliability WorkbookのChapter 8には、障害が発生したとき原因を取り除くいくつかのプラクティスが紹介されています。

  • ロールバック: 直近の変更で問題が発生した場合はロールバックが安全かつ適切です。 新たな不具合修正では、新しい実装のテスト、ビルド、ロールアウトが必要となります。 障害によりエラーバジェットを消費し続けているのなら、ロールバックによる迅速な対応が必要です。
  • 機能の独立: 機能フラグなどを使って機能単位でオン・オフを切り替える方法です。 機能フラグを使った切り替えの方が、ロールバックよりもさらに迅速に問題を取り除くことができます。

AWS版kintoneでも、適用後に問題を発見してロールバックを実施することもあります。 しかし大きな変更をリリースする場合や、リリース後しばらく様子を見たい場合は、機能フラグを採用することが多いです。 機能フラグを採用する理由は以下の通りです。

バージョン(コンテナイメージ)の管理が不要

AWS版kintoneの開発環境と本番環境は対照的な構成になっています。 開発環境でしばらく寝かせるということはせず、開発環境の適用後に自動テストが通過すれば、本番環境に同じコンテナイメージを適用します。 開発環境では、本番同等のリクエストを再現するのは難しいことが経験的にわかっていました。 そのため入念な試験だけでなく、問題が発生したら即座に切り戻せる仕組みに注力しました。

各環境でコンテナイメージを統一することで管理が容易になり、Gitのソースコードがそのまま今の本番環境を表します。 現在チーム内で開発しているサービス群だけで20近く、Lambdaなどの細かい部品を含めると50近くあります。 それぞれの環境ごとに異なるイメージを使用するのは、管理コスト的にもあまり現実的ではありません。

そのため適用するコンテナイメージ統一して、機能フラグによって利用する機能を切り替えることにしています。

所望の機能だけ切り戻せる

機能をリリースした翌週に不具合を発見した場合を考えてみましょう。 ロールバックではうまく対応できないことがあります。

大きな機能変更Aの後に、別の機能Bがすでにリリースされたとします。 コンテナイメージを機能変更Aより前まで巻き戻してしまうと、 機能Bもロールバックされてしまいます。 機能Aだけをロールバックするには git revertして再びCI/CDパイプラインを流す必要があります。 もちろんgit revertが常に成功するとも限りません。

機能フラグを使うと、後から取り入れた機能Bに一切触れることなく、機能Aだけを切り戻すことができます。

Spring BootとKubernetesの例

AWS版kintoneではチームが開発してるサービス群をKubernetes上にデプロイしています。 それぞれのサービスはSpring Bootで実装しているものがほとんどです。 ここからはSpring Bootでの機能フラグの実装例と、Kubernetes上での切り戻し例を紹介します。

Spring Bootの実装例

Spring Bootでは、JVMシステムプロパティや環境変数の定義に応じて、DI(Dependency injection)やロードするクラスを切り替える機能があります。 これを使うことでアプリケーション起動時のJVMシステムプロパティ・環境変数から、サービスのロジックを容易に切り替えることができます。

以下の例では @ConditionalOnProperty アノテーションによって、DIするクラスを切り替える例です。 チームではKotlinを採用しているので、Kotlinのコードを使って説明します(サーバーサイドKotlinはいいぞ!)。 V1StrategyV2Strategy は同じStrategy interfaceを実装しているとします。 プロパティ mycomponent.useV2Strategy の値を true に設定した場合のみ V2Strategy の実装を使用します。

@Configuration
class Configuration {
    @Bean
    @ConditionalOnProperty(name = ["mycomponent.useV2Strategy"], havingValue = "false", matchIfMissing = true)
    fun v1Strategy(): Strategy {
        return V1Strategy()
    }

    @Bean
    @ConditionalOnProperty(name = ["mycomponent.useV2Strategy"], havingValue = "true")
    fun v2Strategy(): Strategy {
        return V2Strategy()
    }
}

クラス全体にアノテーションを付けることもできます。 これは @Scheduled アノテーションを持つメソッドを切り替えるときによく使います。

@Component
@ConditionalOnProperty(name = ["mycomponent.useV2Strategy"], havingValue = "false", matchIfMissing = true)
class V1Strategy {
    @Scheduled(fixedDelay = 60 * 1000)
    fun doSomething() {
        // do something
    }
)

@Component
@ConditionalOnProperty(name = ["mycomponent.useV2Strategy"], havingValue = "true")
class V2Strategy {
    @Scheduled(fixedDelay = 60 * 1000)
    fun doSomething() {
        // do something
    }
}

Kubernetesによる切り替え例

プロパティ mycomponent.useV2Strategy は環境変数 MYCOMPONENT_USEV2STRATEGY から値を渡せます。 環境変数はKubernetesマニフェストから設定できます。

env:
- name: MYCOMPONENT_USEV2STRATEGY
  value: "true"

もし仮に開発環境だけを先に適用したい場合は、開発環境だけ true に設定します。 そして本番環境で問題が見つかった時に機能を切り戻す場合も、 kubectl コマンドで実装をV1/V2を切り替えることができます。

# V1Strategyを利用する
kubectl set env deployment/my-component MYCOMPONENT_USEV2STRATEGY=false

# V2Strategyを利用する
kubectl set env deployment/my-component MYCOMPONENT_USEV2STRATEGY=true

まとめ

本番リリースはいつもドキドキしますよね。 トラブルがあった時の対応方法を事前に用意しておけば、障害の影響を最小限に抑えることができます。 今回のTipsはアプリケーションコードだけで実装できるのも魅力的で、Canary ReleaseやBlue/Green Deploymentのような壮大な仕組みも必要ありません。 Kotlin+Spring Bootの例を紹介しましたが、もちろん他の言語でも同じTipsを使えます。

AWS版kintoneは将来のサービス成長を見据えて、これからも継続的に機能改善を続ける予定です。 そんなAWS版kintoneのDevOpsエンジニアは一緒に働ける人を募集しています。

cybozu.co.jp

この記事の内容やDevOpsチームについて聞いてみたい方は、@ueokandeまでお気軽にDMをどうぞ!