The Amazon Builders’ Library というページがあります。このページでは、Amazon においてソフトウェア開発をどのように行っているかがブログ記事の形式で公開されているのですが、その中で CI/CD に関する 4 つの記事(3 つは日本語化済み)が公開されていたので、どういうことが記載されているのかまとめてみました。
この記事では、日本語化が済んでいる前半 3 記事についてまとめています。
継続的デリバリーによる高速化 (2019 年公開)
AWS でソフトウェア開発のシニアマネージャーをしていた Mark Mansour 氏による投稿です。
10 年以上前の Amazon では、ビルドツールの Brazil やデプロイツールの Apollo を独自開発していたものの、それらを連携するパイプラインツールは無く、コードのチェックインから本番稼働まで平均で 16 日間かかっていました。
そこで、少数のチームを対象として継続的デリバリーの実践を試行して、先程の時間を 90% も削減することに成功しました。その後、ここで得られた知見をもとに CI/CD パイプライン運用を他チームにも横展開していき、これまで個々のチームに閉じてしまっていた様々なナレッジが、標準化されたパイプラインの利用により共有されるという副次効果も得られました。ただ、自動化を目指すあまりテストをおろそかにしてしまうという行動もみられました。
不具合のリスクを減らすために、いくつかのアクションが実行されました。まずは、デプロイ後に健全性をチェックするフローを導入しました。これは、AWS CodeDeploy (デプロイを自動化する AWS サービス)の、AppSpec (デプロイ時に自動実行するスクリプト定義ファイル)に、デプロイを検証するスクリプトを仕込むことで実現できます。2 つめは、デプロイ前にもテストステージを設け、様々なテストを自動化しました。これには、単体テスト(コードの静的解析等も含む)や統合テスト(障害テストや自動ブラウザテスト等も含む)やセキュリティ診断、本番環境と同じ環境を用意した上での動作テストを含みます。一般的に、リリースパイプラインのなるべく早い段階で不具合を検出するように努めることで、結果的に不具合のリスクを低減できます。3 つめは、そのサービスの一部のコンポーネントのみを一部のユーザーに対してだけ公開するカナリアリリースを実践しました。これにより、パイプライン上でのテストを通過してしまった不具合が本番環境で顕在化しないか確認します。これを一定時間(チームによって数分~数時間)待機して、ネガティブなデータポイントがなくなってから次のリリースに進めるようにする際、徐々に多くのユーザーにまとめて公開するようにしてリリース時間の高速化も図っています。4 つめは、リリース時期を管理可能としました。AWS では不具合発生時にすぐに対応できるように営業時間内にリリースすることを好む一方、Amazon の他のチームは顧客トラフィックが少ない時間帯のリリースを好みました。
このような取り組みのポイントは、まずは現状の痛みを理解して、それに対処して、それを繰り返すことです。小さいところからスタートし、時間の経過とともに徐々にできることを増やしていきます。また、ルールには常に例外が存在するため、オプトアウトの仕組みを取り入れてあらゆるチームで採用できるようにすることも大切です。Amazon では現在でも自動化を推進する道中にいます。
デプロイ時におけるロールバックの安全性の確保 (2019 年公開)
AWS で DynamoDB や SQS や Lex の開発に携わっていた Sandeep Pokkunuri 氏による投稿です。
Amazon では 2-way door (失敗してもすぐに戻れる選択肢を取ろう) という考え方があり、これはサービス開発やデプロイパイプラインにも適用される哲学です。Amazon が提供するサービスは後方互換性を持つようにしており、新しいバージョンのリリースに問題があるとすぐに過去のバージョンにロールバックできるような準備が行われています。
デプロイ対象が分散システムの場合は、あるシステムのある機能の一部がローリングデプロイ等によって徐々に更新されていくことが多いため、コンポーネントごとに新旧のバージョンが混合します。このとき、双方のバージョンでプロトコルが変更されると、異常発生時にロールバックすら不可能になってしまう可能性があります。
たとえば、新しいシステムではデータをストレージに永続化する際に、新たに暗号化を施すような変更が行われたとします。その状態で古いシステムへロールバックしても、古いバージョンのシステムは暗号化されたデータを読み込むことができません。
こういった状況には 2-phase deploy が有効です。上記の例の場合、まずは最初のフェーズ(prepare)で暗号化データと非暗号化データの両方を読み込めるバージョンをデプロイし、その後のフェーズ(aciivate)で、暗号化データを書き込むバージョンをデプロイします。そうすることで、後続のフェーズで新旧のバージョンが混在した状況でも、暗号化データと非暗号化データの両方を読み込めるため、リリースおよびロールバック時に問題は発生しません。
この方法の注意事項は 2 つあります。まず、prepare フェーズが完全に完了したことを確認してから activate フェーズに進む必要がある点です。そうしないと、暗号化データが書き込まれ始めた時点でまだ非暗号化データしか読み取れないホストが存在する可能性があります。DynamoDB の開発時には、全てのサーバーで prepare フェーズが完了したことをはっきりと確認してから activate フェーズに進んだこともありました。2 つめは、prepare と activate の両方のフェーズをロールバックできない点です。activate フェーズが始まった時点で暗号化データが書き込まれ始めてしまうためです。そのため、prepare フェーズのバージョンで問題ないことを綿密に確認する必要があり、おおよそ数日間の待機時間を設けるようにしていました。
上記のような 2-phase deploy が必要かどうかは、実際に本番環境と同様のテスト環境にデプロイして検証します。このとき、3 つの段階でデプロイを検証します。1 段階目では、全体の半分のホストにだけデプロイして、意図的に新旧バージョンを混在させます。2 段階目では、すべてのホストにデプロイして挙動を確認します。そして最後に 3 段階目では、ロールバックを実施して問題なく完了するかを確認します。
安全なハンズオフデプロイメントの自動化 (2020 年公開)
AWS のプリンシパルソフトウェアエンジニアである Clare Liguori による投稿です。
Amazon では前述したようにデプロイパイプラインを自動化しているため、開発者がソースコードリポジトリのメインブランチに変更をプッシュした後は、パイプラインが残りの処理の全てを自動化しています。
一般的な AWS サービスのパイプラインでは、Source, Build, Test, Prod のステージが含まれています。
Source ステージでは、アプリケーションコードやテストコード、IaC コード、静的アセット、依存ライブラリ情報が個々のリポジトリで管理され、個々のパイプラインからデプロイされます。こうすることで、アプリケーションコードに不具合があってパイプラインが止まっていても、インフラの変更や静的アセットの変更は可能であることを意味します。また、依存ライブラリのバージョン等は定期的に自動更新されるようになっています。
各リポジトリの mainline ブランチ(いわゆる main ブランチ)へマージする際はコードレビューが必須となっています。このコードレビューが唯一の手動承認であるため、非常に重要なフェーズです。ここでは、コードの正確性、テストの妥当性、デプロイ監視の設定、ロールバックの安全性などが確認されます。承認をパスすると、その後その変更は自動で本番環境までデプロイされていきます。
Build ステージでは、チームごとに構築したビルド環境でアプリケーションコードのビルドと単体テストが実施されます。単体テストでは他の API 呼び出しをシミュレートしますが、API 呼び出しが予期しないエラーを返却した際の挙動確認などにとどまります。実際の API 呼び出し時の挙動については以降の統合テストで実施されます。
Test ステージは更に細分化され、Alpha では機能テスト、Beta では統合テストを実施します。その後、Gamma One Box では、別の記事で触れたロールバックの安全性を確かめるための新旧バージョンの混在テストが実施されます。そして最後の Gamma では複数リージョンに本番環境と同じようにデプロイされた環境を用意し、ローリングデプロイ、監視とアラーム、カナリアリリース、自動ロールバック、等の挙動が改めてテストされます。負荷テストを実施する場合もあります。
Prod ステージもかなり細分化され、リージョンや AZ、そして更に細かい粒度(セル)ごとに順番にデプロイされるようになっています。これは、不具合が生じた際に顧客影響を最小限に留めるためです。より具体的には、それらを Wave と呼ばれるグループに分割して、Wave ごとにデプロイを実施していきます。そのうえで、たとえば、Wave1 を 1 リージョン、Wave2 を 3 リージョン、Wave3 を 5 リージョン、…、のようにしていくことで、速度とリスクのバランスをとっています。
各 Wave におけるデプロイではまず、前述した One Box ステージから始まります。そのうえで、本番環境でも新しいバージョンが想定通りの挙動になることを監視により確認した後に、残り全てのフリートへのデプロイが開始されます。典型的な設定では、これはローリングデプロイによって行われ、一度に 33% のリソースが更新対象になります。これは、もともとのリソース設計で AZ 障害に耐えられるようになっているため、本番環境で正常稼働するリソースが一時的に 66% になってもユーザーリクエストを捌ききれることが見積もられているためです。もちろん、チームによっては一度に更新する対象を 5% 程度に抑えることもあります。
安全なデプロイを支えるためには監視が必要不可欠ですが、エラー率やレイテンシといった顧客体験に直結するメトリクスのほか、CPU 使用率などのインフラ側のメトリクスにアラームを予め定義しておいて、それらが発報された時点で自動ロールバックが開始されるようになっています。合わせてオンコールエンジニアへの呼び出しも行われますが、多くの場合、手動での対応が開始される頃には自動ロールバックは既に進行中となっています。なお、On Box 環境では新バージョンが捌くリクエスト数がそもそも少ないため、それ用のアラーム(や閾値)を設定しておきます。
また、デプロイしたタイミングでは顕在化しないような不具合もあります。そうした不具合に気付かないまま本番環境の複数リージョンにデプロイを進行させてしまうと、多くのユーザーに不具合の影響を与えてしまう可能性があります。そこで、Prod ステージでは、次のステージに進行するまでの待機時間(Bake Time)を設定しています。多くの場合、スピードとリスクのバランスをとるため、早い段階での Wave での待機時間は長めに設定しておき、後続の Wave での待機時間は短めに設定します。典型的な設定では、One Box ステージでは 1 時間、初期の Wave では 12 時間、後期の Wave では 2-4 時間の待機時間をもたせ、Prod ステージ全体では 4-5 日とします。もちろん、影響度や規模に応じてこれらの値は大きく変動します。なお、Bake Time には「特定の API Call が 100 件以上になる」のような条件も付与できます。また、緊急のセキュリティ対応のような場合、Bake Time を短くしてデプロイスピードを優先することも稀にありますが、その場合はプリンシパルエンジニアによる精査と積極的な監視を伴います。
複数のデプロイが同時に進行することもよくあります。その場合、先行するデプロイで不具合が見つかった場合、後続のデプロイが進行することによってより影響が大きくなる可能性があります。そのため、パイプライン全体でアラーム状態を確認できるようになっており、アラーム状態の場合は他のデプロイも進行しないような仕組みが導入されています。また、障害発生時に素早く手動対応に移れるよう、デプロイを実際に実施するタイムウィンドウを設定することもできます。
通常、AWS の各サービスチーム(開発チーム)は、各マイクロサービスのリソースタイプ(アプリケーションコード、インフラコード、…)に応じた複数のパイプラインを所有しています。そして、新しく増えるリージョンや AZ への対応、セキュリティ対応など、パイプラインそのものの管理コストが懸念されます。そこで、Pipelines as Code を実践しており、すべてのパイプライン設定を共通設定から継承するサブクラスとして定義可能としています。
このように、Amazon のデプロイは、徹底的な事前テスト、自動ロールバック、時間差のあるデプロイなどによって安全性を高めています。これにより、開発者は本番環境へのデプロイを積極的に監視する必要はなくなり、1 日のなかで複数回の本番環境へのデプロイというスピードを手に入れることができています。