ネットワークサービス部のid:uwenoです。
本連載ではこれまで、CI/CDを活用し機械的な文法チェックやテストなどを実行していることを紹介してきました。 今回はさらにCI/CDを活用し、パイプライン中でアプリケーションを検証環境に自動的にデプロイする事例をご紹介します。 MRがマージされてからすぐに検証環境にアプリケーションをデプロイすることで、 開発者が新機能をいち早く利用したり、早い段階で不具合が発見できたりするメリットがあります。
なお、前回の記事は 【CI・CD戦略編 その3】デプロイ後の正常性確認 - FJCT Tech blog でした。
Blue/Greenデプロイ
私たちのアプリケーションは、Docker Swarmを用いてBlue/Greenデプロイを行っています。 Docker SwarmにBlueとGreenの2つのStackを作成し、それぞれのStackで独立してアプリケーションを稼働させています。 アプリケーションへのアクセスはリバースプロキシ(Docker Swarm上に構築したHAProxy)を経由させ、 BlueとGreenのどちらか片側のStackにアクセスを寄せて運転しています。
なお、アプリケーションはBlueとGreenでそれぞれ独立していますが、 2つのアプリケーションが操作しているネットワーク機器やDBは同じものです。
アプリケーションのリリースでは、以下の手順に従い無停止でメンテナンス作業をしています。
- その時点でリバースプロキシからアクセスされない裏側のStackへ新しいバージョンのアプリケーションをデプロイ
- 新しいアプリケーションをデプロイした裏側のStackで正常性確認と動作確認を実施
- 正常性確認と動作確認で問題がなければ、リバースプロキシの設定を変更してBlue/Greenを切り替え
- (切り替え後に問題が発生した場合)Blue/Greenを切り戻し
リリース直前の動作確認
リリース直前の動作確認には、E2Eテストとは別のテストスクリプトを利用しています。
検証環境でのE2Eテストでは、テストクライアントでWeb APIを操作し、想定されるレスポンスが返却されることを確認します。 さらにテスト前後のDBやネットワーク機器のコンフィグの差分をチェックし、インフラ層まで正しく情報が投入されたことを確認しています。
しかし、本番環境ではテスト中にも不特定多数のユーザによる操作が見込まれるため、テスト前後のインフラ層の差分を予測することは困難です。 また、E2Eテストにはネットワーク機器に障害を発生させる異常系のテストなども含まれるため、本番環境では実行できません。 加えてE2Eテストの実行には時間がかかってしまうため、メンテナンス作業中の実施は望ましくないと考えています。
メンテナンスでリリースされるバージョンのソフトウェアは、必ず事前に検証環境でのE2Eテストが実施されています(このことはCIによって保障されます)。 そのためリリース直前の動作確認では、一部の正常系のテストケースを実行し、Web APIのレスポンスが期待通りであることを確認することにしています。 ちなみにこの確認に使うスクリプトは「最低限動くことを確認するスクリプトくん」と呼ばれています。 ちょっと長いですね。
なんでローリングアップデートじゃないの?
無停止リリースをする他の手段として、ローリングアップデートなども考えられます。 Docker SwarmやKubernetesなどのコンテナーオーケストレーターを使っている場合は、むしろその方が主流なのかもしれませんね。
私たちのアプリケーションでは、それぞれ異なる処理を行う複数のワーカーノードによってタスクを処理しています。 WebアプリケーションがHTTPリクエストを受け付けるとワーカーへタスクを発行し、 タスクを受け取ったワーカーはタスクを実行して(あれば)次のワーカーへタスクを発行します。
もしデプロイされているWebアプリケーションと、各種ワーカーのバージョンが違っていた場合、 ワーカーで正しくタスクを実行できない可能性があります。
- 上側が通常時の動作です
- 下側がWebアプリケーションとワーカーのバージョンが異なる場合の動作です
- 簡単のため省略していますが、WebAPPと各種ワーカーはそれぞれ複数のコンテナが動作しています
- 簡単のため省略していますが、WebAPPと各種ワーカー間のタスクのやり取りはメッセージ指向ミドルウェアを経由します
この問題を回避してローリングアップデートを実施するためには、タスクに後方互換性を持たせることが必要です。 互換性を維持しつつバージョンを上げていくことも可能ですが、 ローリングアップデートのメリットが互換性維持に必要なコストと比べて小さいと判断しました。 また構築・デプロイ手順が自動化されており、2環境を同時に稼働した場合でも運用上のオーバーヘッドが小さいことから、 新旧のバージョンが混在しないBlue/Greenデプロイを選択しました。
デプロイ作業自動化スクリプト
私たちのアプリケーションは、各種検証作業や本番でのメンテナンス手順の確認のために、複数の環境へデプロイを行っています。 私たちが管理している環境とその用途は下記の通りです。
- 開発チーム用ナイトリーテスト環境
- 開発メンバーが検証に利用
- 毎朝、その時点での最新の開発版に対してE2Eテストを実行するために利用
- 開発チーム用MRテスト環境
- 開発メンバーが検証に利用
- プッシュされたMRに対して、マージ前にE2Eテストを実行するために利用
- 他チーム用検証環境
- Web APIで連携している他のチームが検証に利用
- ステージング環境
- 本番リリースのメンテナンス手順の確認や動作確認に利用
- 本番環境
- ニフクラのコントロールパネルと接続されている本番の環境
これら以外にも、開発メンバーが各自で管理している環境が複数あります。 これらすべての環境へ、毎回手作業でデプロイするのはとても大変です。 そのため、Pythonのスクリプトを利用してデプロイ作業を自動化しています。
このスクリプトには以下の機能が実装されています。
- Stackの表裏を確認する機能
- リバースプロキシの設定をもとに、現在稼働しているStackと待機中のStackを返却
- StackのアプリケーションのバージョンをUpdateする機能
- デプロイするアプリケーションのバージョン(Dockerイメージのタグ名)とデプロイ先のStackを指定して、 アプリケーションのデプロイを実行
- Blue/Greenを切り替える機能
- リバースプロキシの設定を変更して、Blue/Greenを切り替え
- Zabbixの通知を抑止する機能
- 以前ご紹介した正常性確認くんなどを活用し、Zabbixでは定常的にBlue/Green双方環境の正常性を監視
- デプロイ中はコンテナ数の異常などを検知してアラートが発報されるため、Zabbixを一時的にメンテナンスモードに移行し通知抑止
なお、私たちはDocker Swarmの管理にPortainerを利用しています。 PortainerにはDocker APIに対するゲートウェイとなる機能があり、これを活用してStackの操作をしています。
CIによる自動デプロイ
これまでご紹介してきた「正常性確認くん」「最低限動くことを確認するスクリプトくん」「デプロイ作業自動化スクリプト」 を組み合わせることで、GitLab CI上で自動デプロイが可能になりました。 私たちのプロジェクトでは実際に、前項で紹介した「開発チーム用ナイトリーテスト環境」と「他チーム用検証環境」に対して、 MRマージ時に、アプリケーションをデプロイするパイプライン(以降デプロイパイプライン)を実行しています。
デプロイパイプラインでは、以下の順序でジョブを実行しています。
- Zabbixの通知を抑止(デプロイ自動化スクリプト)
- このパイプライン中にビルドされたアプリケーションを裏側のStackへデプロイ(デプロイ自動化スクリプト)
- アプリケーションの正常性確認(正常性確認くん)
- Web APIの動作テスト(最低限動くことを確認するスクリプトくん)
- Blue/Green切り替え(デプロイ自動化スクリプト)
- Blue/Green切り戻し(デプロイ自動化スクリプト)
- 切り戻しの必要がある場合のみ、トリガー実行
まとめ
今回はGitLabのCI/CDを用いて、MRのマージ後に検証環境へ自動デプロイする事例をご紹介しました。 私たちのシステム構成固有の説明がやや多くなってしまいましたが、 デプロイに必要な手順をプログラムとして実装し、そのプログラムをCI/CDで活用するという戦略は、 他の構成においても十分活用できると思います。
次回は今回に引き続き、CI/CDを活用して本番環境へどのようにデプロイしているかをご紹介します。お楽しみに。
連載バックナンバー
CI戦術編
- 【CI戦術編 その1】alembic check コマンドを活用したマイグレーションスクリプト生成忘れ防止
- 【CI戦術編 その2】コードレビューを充実したものにする方法、あるいは一生残る恥ずかしい履歴を作らないように
- 【CI戦術編 その3】Pythonのimportのことならまかせろ isort
- 【CI戦術編 その4】Blackを利用したコーディングスタイルの統一
- 【CI戦術編 その5】Pythonで明示的に型を書く理由
- 【CI戦術編 その6】Python開発の強い味方 Pylint
- 【CI戦術編 その7】 pyupgradeを使って最新の記法に対応してみた
- 【CI戦術編 その8】OAS(OpenAPI Specification)で仕様書を自動生成しよう
- 【CI戦術編 その9】自動生成しか勝たん openapi-typescript
- 【CI戦術編 その10】 Renovateで依存ライブラリのアップデートに負けない方法
- 【CI戦術編 その11】Trivy: あなたの使ってるライブラリ、大丈夫ですか?
- 【CI戦術編 その12】戦術編総まとめ — いつ何を使うべきか
- CI・CD戦略編