好きな技術を布教したい 😗

〜 沼のまわりに餌をまく 〜

【Istio⛵️】Istioによって抽象化されるEnvoyのHTTPSリクエスト処理の仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

  • Istioのサイドカーメッシュを題材にしたEnvoyの設定の抽象化について
  • 様々なサービスメッシュツール (特に、Istio、Consul、Cilium、など) でも流用できるEnvoyの知識について



01. はじめに

どうも、俺 (REMIX) feat. Istioニキ a.k.a. いすてぃ男です。

istio-icon


Istioは、Envoyを使用したサービスメッシュを実装します。

IstioがKubernetesリソースやIstioカスタムリソースに基づいてEnvoyの設定を抽象化してくれるため、開発者はEnvoyをより簡単に設定できます。

istio_envoy


Envoyの設定の抽象化は、Envoyを使用したサービスメッシュ (例:Istioサイドカーメッシュ/アンビエントメッシュ、Consul、Istioから得られた学びを土台に登場したCiliumサイドカーフリーメッシュ、など) に共通しています。

つまり、次々に登場するEnvoyによるサービスメッシュツールに振り回されないようにするためには、ツールがどのようにEnvoyを抽象化するのかを理解しておく必要があります。

そこで今回は、IstioサイドカーメッシュがEnvoyのHTTPSリクエストの処理をどのように抽象化するのかを解説します。

また、抽象化されたEnvoyがHTTPSリクエストを処理する仕組みも一緒に解説します。

これらの知識は、様々なサービスメッシュツールで流用できるはずです。

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. 様々なリソースによるEnvoy設定の抽象化

まずは、どのようなリソースがHTTPSリクエストの処理に関係しているのかを、HTTPSリクエストの方向に分けて解説していきます。

istio-proxyコンテナやEnvoyについては、次章以降で解説します。


サービスメッシュ外からのHTTPS

サービスメッシュ外から内にHTTPSリクエストを送信する場合、リソースが以下の順で紐付き、Envoyの設定を抽象化します。

istio_envoy_istio_resource_ingress_mermaid.png


各リソースは、以下の仕組みで、HTTPSリクエストを送信元から宛先まで届けます。

図中の番号に沿って、通信の仕組みを解説します。

  1. クライアントは、サービスメッシュ外からL7ロードバランサーHTTPSリクエストを送信します。
  2. L7ロードバランサーは、Istio IngressGateway PodにHTTPSリクエストを送信します。
  3. Istio IngressGateway Podは、宛先Podとの間で相互TLS認証を実施します。
  4. Istio IngressGateway Podは、Kubernetesリソース (Service、Endpoints) やIstioカスタムリソース (VirtualService、DestinationRule) に応じて、HTTPSリクエストを宛先PodにL7ロードバランシングします。

istio_envoy_istio_resource_ingress


マイクロサービス間のHTTPS

サービスメッシュ内のPodから別のPodにHTTPSリクエストを送信する場合、リソースが以下の順で紐付き、Envoyの設定を抽象化します。

istio_envoy_istio_resource_service-to-service_mermaid.png


各リソースは、以下の仕組みで、HTTPSリクエストを送信元から宛先まで届けます。

図中の番号に沿って、通信の仕組みを解説します。

  1. 送信元Podは、宛先Podとの間で相互TLS認証を実施します。
  2. 送信元Podは、Kubernetesリソース (Service、Endpoints) やIstioカスタムリソース (VirtualService、DestinationRule) の設定に応じて、HTTPSリクエストを宛先PodにL7ロードバランシングします。

istio_envoy_istio_resource_service-to-service


サービスメッシュ外へのHTTPS

サービスメッシュ内のPodから外のシステム (例:データベース、ドメインレイヤー委譲先の外部API) にHTTPSリクエストを送信する場合、リソースが以下の順で紐付き、Envoyの設定を抽象化します。

複数のVirtualServiceとDestinationが登場するため、これらには便宜上 XY をつけています。

istio_envoy_istio_resource_egress_mermaid.png


各リソースは、以下の仕組みで、HTTPSリクエストを送信元から宛先まで届けます。

図中の番号に沿って、通信の仕組みを解説します。

  1. 送信元Podは、HTTPSリクエストの宛先がServiceEntryでエントリ済みか否かの設定に応じて、HTTPSリクエストの宛先を切り替えます。
    • 宛先がエントリ済みであれば、送信元PodはHTTPSリクエストの宛先にIstio EgressGateway Podを選択します。
    • 宛先が未エントリであれば、送信元PodはHTTPSリクエストの宛先に外のシステムを選択します。
  2. 送信元Podは、Istio EgressGateway Podとの間で相互TLS認証を実施します。
  3. (1) で宛先がエントリ済であったとします。送信元Podは、HTTPSリクエストの向き先をIstio EgressGateway Podに変更します。
  4. 送信元Podは、Kubernetesリソース (Service、Endpoints) やIstioカスタムリソース (VirtualService、DestinationRule) の設定に応じて、Istio EgressGateway PodにL7ロードバランシングします。
  5. Istio EgressGateway Podは、HTTPSリクエストをエントリ済システムにL7ロードバランシングします。

istio_envoy_istio_resource_egress


▶︎ Istio EgressGatewayの必要性について

実は、Istio EgressGatewayを使用しなくとも、ServiceEntryだけでサービスメッシュ外の登録済みシステムにHTTPSリクエストを送信できます。

しかし、Istio EgressGatewayを使わないと、マイクロサービスからistio-proxyコンテナを経由せずに外部システムに直接HTTPSリクエストを送信できるようになってしまい、システムの安全性が低くなります。


03. istio-proxyコンテナによるHTTPS処理

前章では、KubernetesリソースやIstioカスタムリソースによって抽象化されたEnvoyまで言及しませんでした。

本章では、解説をもう少し具体化します。

Istioは、Envoyプロセスを持つistio-proxyコンテナを作成します。

このistio-proxyコンテナを使用してどのようにHTTPSリクエストを処理しているのかを、HTTPSリクエストの方向に分けて解説します。

Envoyの設定については、次章以降で解説します。


Istioコントロールプレーンの仕組み

Envoyの設定を抽象化する責務を担うのは、Istioコントロールプレーン (discoveryコンテナ) です。

Istioコントロールプレーンは異なる責務を担う複数のレイヤーから構成されています。

レイヤー名 責務
Config ingestion
レイヤー
kube-apiserverからKubernetesリソースやIstioカスタムリソースの設定を取得します。
Istioの初期から名前は変わっていません。
Config translation
レイヤー
リソースの設定をEnvoy設定に変換します。
Istioの初期ではConfig Data Modelレイヤーという名前で、執筆時点 (2024/01/16) で名前が変わっています。
Config serving
レイヤー
Envoyの設定や証明書をPod内のistio-proxyコンテナに配布します。
Istioの初期では、Proxy Servingレイヤーという名前で、執筆時点 (2024/01/16) で名前が変わっています。


図中の番号に沿って、Istioコントロールプレーンの仕組みを解説します。

  1. Config ingestionレイヤーにて、 Istioコントロールプレーンはkube-apiserverにHTTPSリクエストを送信します。ここで、KubernetesリソースやIstioカスタムリソースの設定を取得します。
  2. Config translationレイヤーにて、取得したリソースの設定をEnvoyの設定に変換します。
  3. Config servingレイヤーにて、Envoyの設定や証明書をPod内のistio-proxyコンテナに配布します。双方向ストリーミングRPCのため、istio-proxyコンテナがConfig servingレイヤーにリクエストを送信し、これらを取得することもあります。

istio_envoy_istio-proxy_resource_control-plane


▶︎ Config servingレイヤーにあるXDS-APIについて

Config servingレイヤーには、XDS-APIがあります。

このXDS-APIは、Envoyの設定に関するエンドポイント (LDS-API、RDS-APICDS-API、EDS-API、ADS-API、など) や、証明書配布のエンドポイント (例:SDS-API) を持ちます。

istio_sidecar-mesh_architecture

以下の記事で解説していますため、もし気になる方はよろしくどうぞ🙇🏻‍


▶︎ Istioカスタムリソースのコントローラーについて

Istioコントロールプレーンは、前述の責務以外にカスタムコントローラーとしての責務も担います。

以前は、IstioOperatorがカスタムコントローラーの責務を担っていましたが、執筆時点 (2024/01/16) ではIstioOperatorが非推奨となりました。

IstioOperatorの代わりに、Istioコントロールプレーンがこれを担うようになりました👍🏻


サービスメッシュ外からのHTTPS

サービスメッシュ外から内にHTTPSリクエストを送信する場合のistio-proxyコンテナです。

各リソースは、以下の仕組みで、HTTPSリクエストを送信元から宛先まで届けます。

図中の番号に沿って、通信の仕組みを解説します。

  1. Istioコントロールプレーンは、翻訳されたEnvoyの設定をPod内のistio-proxyコンテナに提供します。
  2. クライアントは、サービスメッシュ外からL7ロードバランサーHTTPSリクエストを送信します。
  3. L7ロードバランサーは、Istio IngressGateway PodにHTTPSリクエストを送信します。
  4. Istio IngressGateway Pod内のiptablesは、HTTPSリクエストをistio-proxyコンテナに送信します (リダイレクトは不要)。
  5. Istio IngressGateway Pod内のistio-proxyコンテナは、宛先Podを決定し、またこのPodに対して相互TLS認証を実施します。
  6. Istio IngressGateway Pod内のistio-proxyコンテナは、HTTPSリクエストを宛先PodにL7ロードバランシングします。
  7. 宛先Pod内のiptablesは、HTTPSリクエストをistio-proxyコンテナにリダイレクトします。
  8. 宛先Pod内のistio-proxyコンテナは、HTTPSリクエストを宛先マイクロサービスに送信します。

istio_envoy_istio_ingress


▶︎ Pod内のiptablesについて

Pod内のiptablesは、リクエストが必ずistio-proxyコンテナを経由するように、istio-proxyコンテナにリクエストをリダイレクトします。

iptablesのルールを書き換えるのはistio-initコンテナです。

Istioは、istio-proxyコンテナと同じタイミングで、istio-initコンテナをPodにインジェクションします (Istio IngressGatewayとIstio EgressGatewayのPodは除きます)。

istio_istio-init
画像引用元:SoByte


istio-initコンテナは、istio-iptablesコマンドを実行し、iptablesのルールを書き換えます。

また、istio-initコンテナはルールを書き換えた後に終了するため、Podの起動後にPod内に残りません👍🏻
$ istio-iptables \
    -p 15001 \
    -z 15006 \
    -u 1337 \
    -m REDIRECT \
    -i * \
    -x \
    -b * \
    -d 15090,15020


▶︎ Istio IngressGateway Pod内のiptablesについて

Istio IngressGateway Podでは、マイクロサービスがないため、istio-proxyコンテナにリクエストをリダイレクトする必要がありません。

そのため、Istioはiptablesのルールを書き換えるistio-initコンテナをIstio IngressGateway Podにインジェクションしません。

つまり、Istio IngressGateway Pod内のiptablesのルールはデフォルトのままになっています👍🏻


マイクロサービス間のHTTPS

サービスメッシュ内のPodから別のPodにHTTPSリクエストを送信する場合のistio-proxyコンテナです。

各リソースは、以下の仕組みで、HTTPSリクエストを送信元から宛先まで届けます。

図中の番号に沿って、通信の仕組みを解説します。

  1. Istioコントロールプレーンは、翻訳されたEnvoyの設定をPod内のistio-proxyコンテナに提供します。
  2. 送信元Pod内のiptablesは、HTTPSリクエストをistio-proxyコンテナにリダイレクトします。
  3. 送信元Pod内のistio-proxyコンテナは、宛先Podを決定し、またこのPodに対して相互TLS認証を実施します。
  4. 送信元Pod内のistio-proxyコンテナは、HTTPSリクエストを宛先PodにL7ロードバランシングします。
  5. 宛先Pod内のiptablesは、HTTPSリクエストをistio-proxyコンテナにリダイレクトします。
  6. 宛先Pod内のistio-proxyコンテナは、HTTPSリクエストを宛先マイクロサービスに送信します。

istio_envoy_istio_service-to-service


サービスメッシュ外へのHTTPS

サービスメッシュ内のPodから外のシステム (例:データベース、ドメインレイヤー委譲先の外部API) にHTTPSリクエストを送信する場合のistio-proxyコンテナです。

各リソースは、以下の仕組みで、HTTPSリクエストを送信元から宛先まで届けます。

図中の番号に沿って、通信の仕組みを解説します。

  1. Istioコントロールプレーンは、翻訳されたEnvoyの設定をPod内のistio-proxyコンテナに提供します。
  2. 送信元Pod内のiptablesは、HTTPSリクエストをistio-proxyコンテナにリダイレクトします。
  3. 送信元Pod内のistio-proxyコンテナは、宛先Podを決定し、またこのPodに対して相互TLS認証を実施します。この時、ServiceEntryで宛先がエントリ済みか否かに応じて、HTTPSリクエストの宛先を切り替えます。
    • 宛先がエントリ済みであれば、istio-proxyコンテナはHTTPSリクエストの宛先にIstio EgressGateway Podを選択します。
    • 宛先が未エントリであれば、istio-proxyコンテナはHTTPSリクエストの宛先に外のシステムを選択します。
  4. ここでは、宛先がエントリ済であったとします。送信元Pod内のistio-proxyコンテナは、HTTPSリクエストをIstio EgressGateway PodにL7ロードバランシングします。
  5. Istio EgressGateway Pod内のiptablesは、HTTPSリクエストをistio-proxyコンテナに送信します (リダイレクトは不要)。
  6. Istio EgressGateway Pod内のistio-proxyコンテナは、HTTPSリクエストをエントリ済システムにL7ロードバランシングします。

istio_envoy_istio_egress

▶︎ Istio EgressGateway Pod内のiptablesについて

Istio EgressGateway Podでは、マイクロサービスがないため、istio-proxyコンテナにリクエストをリダイレクトする必要がありません。

そのため、Istioはiptablesのルールを書き換えるistio-initコンテナをIstio EgressGateway Podにインジェクションしません。

つまり、Istio EgressGateway Pod内のiptablesのルールはデフォルトのままになっています👍🏻


04. EnvoyによるHTTPS処理

前章では、istio-proxyコンテナ内のEnvoyの設定まで、言及しませんでした。

本章では、もっと具体化します。

EnvoyがHTTPSリクエストを処理する仕組みを解説します。


Envoyの設定の種類

HTTPSリクエストを処理する場合、Envoyの設定が以下の順で紐付き、HTTPSリクエストを送信元から宛先まで届けます。

istio_envoy_envoy-flow_mermaid_http.png


各処理がどのような責務を担っているのかをもう少し詳しく見てみましょう。

図中の番号に沿って、EnvoyがHTTPSリクエストを処理する仕組みを解説します。

  1. 送信元からのHTTPSリクエストの宛先ポートで、リスナーを絞り込みます。
  2. 通信の種類 (例:HTTP、HTTPSTCPUDPUnixドメインソケット、など) に応じてフィルターを選び、各フィルターがパケットのヘッダーを処理します。もしHTTPSであれば、送信元との間でTLS接続を確立し、パケットのL7のアプリケーションデータを復号化します。
  3. フィルターを使用して、HTTPSリクエストの宛先ポートで、ルートを絞り込みます。
  4. フィルターを使用して、HTTPSリクエストの宛先ホストやパスで、クラスターを絞り込みます。
  5. 設定した負荷分散方式 (例:ラウンドロビン、など) に応じて、クラスター配下のエンドポイントを選びます。
  6. 宛先との間でTLS接続を確立し、パケットのL7のアプリケーションデータを暗号化します。そして、エンドポイントにL7ロードバランシングします。

istio_envoy_envoy-flow


TCPリクエストを処理する場合について

HTTPリクエストを処理する場合、フィルターに紐づくのはルートですが、TCPリクエストの場合はそうではありません。

TCPリクエストを処理する場合、フィルターにクラスターが紐づきます👍🏻

istio_envoy_envoy-flow_mermaid_tcp


フィルター

フィルターの一覧

Envoyのフィルターは、Envoyの機能を拡張するための設定です。

HTTPSリクエストを処理するためには、リスナーフィルター、ネットワークフィルター、HTTPフィルター、といったフィルターが必要になります。

全ては解説しきれないため、HTTPSリクエストを処理するための代表的なフィルターをいくつか抜粋しました。

ただ、 Istioはこれらのフィルターをデフォルトで有効にしてくれている ため、開発者がEnvoyのフィルターを設定する場面は少ないです。

逆をいえば、Istioを介さずにEnvoyをそのまま使用する場合、開発者がEnvoyのフィルターを自前で設定する必要があります👍🏻

フィルターの種類 HTTPSリクエストの処理に必要なフィルター
(一部抜粋)
説明
リスナー
フィルター
Original Destination istio-proxyコンテナへのリダイレクト前の宛先情報をEnvoyが取得できるようにします。
Pod内のiptablesHTTPSリクエストをistio-proxyコンテナにリダイレクトすると、HTTPSリクエストの宛先がistio-proxyコンテナに変わってしまいます。
ただし、iptablesはリダイレクト前の宛先をカーネル上のSO_ORIGINAL_DSTという定数に格納してくれています。
Envoyは、カーネル上のSO_ORIGINAL_DSTから本来の宛先を取得し、プロキシします。
HTTP Inspector EnvoyがHTTPを検知できるようにします。
TLS Inspector EnvoyがTLSを検知できるようにします。
TLSを検知した場合、EnvoyはTLSに関する処理を実行します。
例えば、DownstreamTlsContextは、リスナーフィルター直後に、送信元との間でTLS接続を確立し、パケットのL7のアプリケーションデータを復号化します。
また、UpstreamTlsContextは、クラスターの処理時に、宛先との間でTLS接続を確立し、L7のアプリケーションデータを暗号化します。
ネットワーク
フィルター
HTTP connection manager Envoyが、L7のアプリケーションデータを読み取り、また後続のHTTPフィルターを制御できるようにします。
HTTP
フィルター
Router Envoyがポート番号でルート、ホストやパスでクラスターを絞り込めるようにします。
gRPC-Web EnvoyがHTTP/1.1で受信したHTTPSリクエストをHTTP/2に変換し、gRPCサーバーにプロキシできるようにします。


▶︎ Istioがデフォルトで有効にするEnvoyの設定について

istio-proxyコンテナは、イメージのビルド時に、あらかじめ用意しておいたEnvoyの設定ファイルを組み込みます。

そのため、istio-proxyコンテナ内のEnvoyは、多くの設定をデフォルトで有効にできます。

Istioを利用する開発者が、EnvoyがHTTPSリクエストを処理するために必要なフィルターを有効にしなくてよいのも、Istioのおかげです。

Istioほんまにありがとな🙏🙏🙏


フィルターチェーンの仕組み

Envoyは、複数のフィルターからなるフィルターチェーンを実行し、HTTPSを処理します。

図中の番号に沿って、Envoyのフィルターチェーンの仕組みを解説します。

各フィルターの機能は、前述したフィルターの一覧を参考にしてください🙇🏻

  1. リスナーフィルター (Original Destination、HTTP Inspector、TLS Inspector、など) を実行します。
  2. (1) でTLS InspectorがTLSを検知した場合、DownstreamTlsContextで宛先とTLSハンドシェイクを実行し、パケットのL7のアプリケーションデータを復号化します。
  3. ネットワークフィルター (HTTP connection manager、など) を実行します。
  4. HTTPフィルター (Router、gRPC-Web、など) を実行します。

istio_envoy_envoy-filter


TCPリクエストを処理する場合について

HTTPフィルターはHTTP/HTTPSリクエストを処理する場合にのみ使用します。

それ以外の通信の種類 (例:TCPUDPUnixドメインソケット、など) の場合は、HTTPフィルターを使用しません。

例えば、TCPリクエストの場合、ネットワークフィルターのTCP proxyフィルターを使用します👍🏻


05. リソースの設定からEnvoy設定への翻訳

いよいよです🔥

Istioが各リソースをいずれのEnvoyの設定に翻訳しているのかを解説します。

表で対応関係の一覧を示した後、istio-proxyコンテナ内のEnvoyに当てはめました。


各リソースとEnvoyの設定の関係一覧

Istioコントロールプレーンは、KubernetesリソースやIstioカスタムリソースの設定をEnvoyの設定に翻訳し、処理の流れに当てはめます。

以下の通り、各リソースがいずれのEnvoyの設定を抽象化するのかを整理しました。

リソースによっては、Envoyの複数の設定を抽象化します。

なお、Istioの用意したEnvoyのフィルターのデフォルト値を変更するユースケースが少ないため、これを抽象化するEnvoyFilterについては言及しません。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Gateway Virtual
Service
Destination
Rule
Service
Entry
Peer
Authentication
リスナー
ルート
クラスタ
エンドポイント


サービスメッシュ外からのHTTPS

Envoyの設定を抽象化するリソース一覧

サービスメッシュ外からのHTTPSリクエストを処理する場合に関係するリソースを抜粋しました。

Gatewayは、Istio IngressGatewayの一部として使用します。

ServiceEntryは、使用しないリソースのため、×としています。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Gateway Virtual
Service
Destination
Rule
Service
Entry
Peer
Authentication
リスナー ×
ルート ×
クラスタ ×
エンドポイント ×


リソースとEnvoyの設定の対応関係

送信元または宛先Envoyに分けると、各リソースは以下のようにEnvoyの設定を抽象化します。

話を簡単にするために、送信元と宛先は同じNamespaceにあると仮定します。

送信元EnvoyでHTTPSリクエストの宛先を決める設定、または宛先EnvoyでHTTPSリクエストを受信する設定を、同じリソースが抽象化します。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Gateway Virtual
Service
Destination
Rule
Peer
Authentication
送信元 リスナー
ルート
クラスタ
エンドポイント
宛先 リスナー
ルート
クラスタ
エンドポイント


▶︎ 送信元と宛先のNamespaceについて

今回、送信元のIstio IngressGatewayと宛先は同じNamespaceにあると仮定しています。

しかし、もししっかり設計するのであれば、Istio IngressGatewayは専用のNamespace (例:istio-ingress) においた方が良いです。

マイクロサービスとは異なるNamespaceにIstio IngressGatewayを置くことで、Istio IngressGatewayをアップグレードしやすくなったり、他から障害の影響を受けにくくなります🙆🏻‍♂️

istio-proxyコンテナ内のEnvoyに当てはめる

この表を、HTTPSリクエストの仕組みの中に当てはめると、以下になります。

送信元EnvoyでHTTPSリクエストの宛先を決める設定、または宛先EnvoyでHTTPSリクエストを受信する設定を、同じリソースが抽象化します。


引用した前述の解説のイメージが掴めるかと思います。

送信元または宛先Envoyでほとんど同じリソースが登場しますが、 Gatewayは送信元Envoyだけで登場します。

istio_envoy_envoy-flow_resource_ingress


リソースの種類だけに着目すると、以下になります。

Gatewayが送信元Envoyだけで登場することがわかりやすくなりました。

istio_envoy_istio-proxy_resource_ingress


マイクロサービス間のHTTPS

Envoyの設定を抽象化するリソース一覧

サービスメッシュ内のPodから別のPodへのHTTPSリクエストを処理する場合に関係するリソースを抜粋しました。

GatewayとServiceEntryは、使用しないリソースのため、×としています。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Gateway Virtual
Service
Destination
Rule
Service
Entry
Peer
Authentication
リスナー × ×
ルート × ×
クラスタ × ×
エンドポイント × ×


リソースとEnvoyの設定の対応関係

送信元または宛先Envoyに分けると、各リソースは以下のようにEnvoyの設定を抽象化します。

話を簡単にするために、送信元と宛先は同じNamespaceにあると仮定します。

送信元EnvoyでHTTPSリクエストの宛先を決める設定、または宛先EnvoyでHTTPSリクエストを受信する設定を、同じリソースが抽象化します。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Virtual
Service
Destination
Rule
Peer
Authentication
送信元 リスナー
ルート
クラスタ
エンドポイント
宛先 リスナー
ルート
クラスタ
エンドポイント

istio-proxyコンテナ内のEnvoyに当てはめる

この表を、HTTPSリクエストの仕組みの中に当てはめると、以下になります。

送信元EnvoyでHTTPSリクエストの宛先を決める設定、または宛先EnvoyでHTTPSリクエストを受信する設定を、同じリソースが抽象化します。


引用した前述の解説のイメージが掴めるかと思います。

送信元または宛先Envoyで、同じリソースが登場します。

istio_envoy_envoy-flow_resource_service-to-service


リソースの種類だけに着目すると、以下になります。

送信元または宛先Envoyで同じリソースが登場することがわかりやすくなりました。

istio_envoy_istio-proxy_resource_service-to-service


サービスメッシュ外へのHTTPS

Envoyの設定を抽象化するリソース一覧

サービスメッシュ内のPodから外のシステム (例:データベース、ドメインレイヤー委譲先の外部API) へのHTTPSリクエストを処理する場合に関係するリソースを抜粋しました。

Gatewayは、Istio EgressGatewayの一部として使用します。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Gateway Virtual
Service
Destination
Rule
Service
Entry
Peer
Authentication
リスナー
ルート
クラスタ
エンドポイント

リソースとEnvoyの設定の対応関係

送信元または宛先Envoyに分けると、各リソースは以下のようにEnvoyの設定を抽象化します。

話を簡単にするために、送信元と宛先は同じNamespaceにあると仮定します。

他の場合とは異なり、送信元EnvoyでHTTPSリクエストの宛先を決める設定、または宛先EnvoyでHTTPSリクエストを受信する設定を、異なるリソースが抽象化します。

PeerAuthenticationだけは、話を簡単にするために送信元と宛先が同じNamespaceであると仮定しているので、同じリソースが抽象化します。

送信元Envoyの設定の抽象化で登場するリソースが宛先では登場せず、逆も然りです。

Kubernetes ☸️
リソース
Istio ⛵️
カスタムリソース
Envoyの設定 Service Endpoints Gateway Virtual
Service
X

Y
Destination
Rule
X

Y
Service
Entry
Peer
Authentication
送信元 リスナー
ルート
クラスタ
エンドポイント
宛先 リスナー
ルート
クラスタ
エンドポイント


▶︎ 送信元と宛先のNamespaceについて

今回、送信元と宛先のIstio EgressGatewayは同じNamespaceにあると仮定しています。

しかし、もししっかり設計するのであれば、Istio EgressGatewayは専用のNamespace (例:istio-egress) においた方が良いです。

マイクロサービスとは異なるNamespaceにIstio EgressGatewayを置くことで、Istio EgressGatewayをアップグレードしやすくなったり、他から障害の影響を受けにくくなります🙆🏻‍♂️

istio-proxyコンテナ内のEnvoyに当てはめる

この表を、HTTPSリクエストの仕組みの中に当てはめると、以下になります。

送信元EnvoyでHTTPSリクエストの宛先を決める設定、または宛先EnvoyでHTTPSリクエストを受信する設定を、異なるリソースが抽象化します。

PeerAuthenticationだけは、話を簡単にするために送信元と宛先が同じNamespaceであると仮定しているので、同じリソースが抽象化します。


引用した前述の解説のイメージが掴めるかと思います。

送信元または宛先Envoyで同じリソースが登場しません 。

istio_envoy_envoy-flow_resource_egress


リソースの種類だけに着目すると、以下になります。

送信元または宛先Envoyで同じリソースが登場しないことがわかりやすくなりました。

istio_envoy_istio-proxy_resource_egress


06. 翻訳されたEnvoy設定値を見てみる

前章では、Envoyの具体的な設定値まで、言及しませんでした。

本章では、さらに具体化します。

各リソースの設定の翻訳によって、Envoyの具体的にどのような設定値になっているのかを解説します。


Envoyの現在の設定を出力する

Envoyは、現在の設定を確認するためのエンドポイント (/config_dump) を公開しています。

これにHTTPSリクエストを送信し、具体的な設定値を出力してみましょう👍🏻

リスナーを出力する

/config_dumpのクエリストリングにresource={dynamic_listeners}をつけると、Envoyのリスナーを出力できます。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_listeners}" | yq -P


▶ 宛先情報を見やすくするyqコマンドについて

Envoyは、JSON形式で設定を出力します。

JSON形式だと見にくいため、yqコマンドでYAMLに変換すると見やすくなります👍

ルートを出力する

/config_dumpのクエリストリングにresource={dynamic_route_configs}をつけると、Envoyのルートを出力できます。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_route_configs}" | yq -P

クラスターを出力する

/config_dumpのクエリストリングにresource={dynamic_active_clusters}をつけると、Envoyのクラスターを出力できます。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_active_clusters}" | yq -P

エンドポイントを出力する

/config_dumpのクエリストリングにinclude_edsをつけると、Envoyのエンドポイントを出力できます。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?include_eds" | yq -P

証明書を出力する

/config_dumpのクエリストリングにresource={dynamic_active_secrets}をつけると、証明書を出力できます。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_active_secrets}" | yq -P


サービスメッシュ外からのHTTPS

ここでは、istio-proxyコンテナはHTTPSリクエストを処理するとします。

図中の番号に沿って、通信の仕組みを解説します。

istio_envoy_envoy-flow_ingress

送信元Pod側のistio-proxyコンテナ

  1. 送信元マイクロサービスからのHTTPSリクエストの宛先ポート (例:50000) で、リスナーを絞り込みます。Envoyは、リスナーを宛先ポートで管理しています (例:0.0.0.0_50000) 。
  2. HTTPSリクエストを処理するための各種フィルターを選びます。また、宛先とTLSハンドシェイクを実行し、パケットのL7のアプリケーションデータを復号化します。
  3. HTTPフィルターにより、HTTPSリクエストの宛先ポート (例:50000) で、ルートを絞り込みます。Envoyは、ルートを宛先ポートで管理しています (例:50000) 。
  4. HTTPフィルターにより、HTTPSリクエストの宛先ホスト (例:foo-service.foo-namespace.svc.cluster.local) やパス (例:/) で、クラスターを絞り込みます。Envoyは、クラスターを宛先ポートやホストで管理しています (例:outbound|50010|foo-service.foo-namespace.svc.cluster.local) 。
  5. 設定した負荷分散方式 (例:ラウンドロビン、など) に応じて、Service配下のPodを選びます。Envoyは、エンドポイントをPodのIPアドレスや宛先ポートで管理しています (例:<PodのIPアドレス>:50000) 。
  6. 宛先との間でTLS接続を確立し、パケットのL7のアプリケーションデータを暗号化します。そして、HTTPSリクエストを宛先PodにL7ロードバランシングします。

宛先Pod側のistio-proxyコンテナ

  • L7ロードバランシングされたHTTPSリクエストの宛先ポート (例:50000) で、リスナーを絞り込みます。Envoyは、リスナーを宛先ポートで管理しています (例:0.0.0.0_50000)
  • HTTPSリクエストを処理するための各種フィルターを選びます。
  • HTTPフィルターにより、HTTPSリクエストの宛先ポート (例:50000) で、ルートを絞り込みます。Envoyは、ルートを宛先ポートで管理しています (例:inbound|50000||) 。
  • HTTPフィルターにより、HTTPSリクエストの宛先ホスト (例:example.com) やパス (例:/) で、クラスターを絞り込みます。Envoyは、クラスターを宛先ポートで管理しています (例:inbound|50000||)
  • エンドポイントを選びます。Envoyは、エンドポイントをローカルホストや宛先ポートで管理しています (例:127.0.0.6:50000) 。
  • ローカルホストにHTTPSリクエストを送信します。結果的に、宛先マイクロサービスにHTTPSリクエストが届きます。


▶︎ istio-proxyコンテナのプロキシ先のIPアドレスについて

istio-proxyコンテナは、ローカルホストを127.0.0.6とし、HTTPSリクエストをマイクロサービスに送信します。

これは、127.0.0.1を指定してしまうと、istio-proxyコンテナからマイクロサービスへの通信がiptables上でループしてしまうためです。

istio-proxyコンテナからマイクロサービスへの通信では、正しくはiptables上でISTIO_OUTPUTからPOSTROUTINGに通信を渡します。

一方で、もしローカルホストが127.0.0.1であると、ISTIO_OUTPUTからISTIO_IN_REDIRECTに通信を渡すことになり、istio-proxyコンテナに再びリダイレクトしてしまいます。

hatappi1225さんの解説が鬼わかりやすかったです🙏🙏🙏
127-0-0-1_iptables_loop
画像引用元:mercari engineering


マイクロサービス間のHTTPS

ここでは、istio-proxyコンテナはHTTPSリクエストを処理するとします。

図中の番号に沿って、通信の仕組みを解説します。

istio_envoy_envoy-flow_service-to-service

送信元Pod側のistio-proxyコンテナ

  1. 送信元マイクロサービスからのHTTPSリクエストの宛先ポート (例:50010) で、リスナーを絞り込みます。Envoyは、リスナーを宛先ポートで管理しています (例:0.0.0.0_50010) 。
  2. HTTPSリクエストを処理するための各種フィルターを選びます。また、宛先とTLSハンドシェイクを実行し、パケットのL7のアプリケーションデータを復号化します。
  3. HTTPフィルターにより、HTTPSリクエストの宛先ポート (例:50010) で、ルートを絞り込みます。Envoyは、ルートを宛先ポートで管理しています (例:50010) 。
  4. HTTPフィルターにより、HTTPSリクエストの宛先ホスト (例:foo-service.foo-namespace.svc.cluster.local) やパス (例:/) で、クラスターを絞り込みます。Envoyは、クラスターを宛先ポートやホストで管理しています (例:outbound|50010|foo-service.foo-namespace.svc.cluster.local) 。
  5. 設定した負荷分散方式 (例:ラウンドロビン、など) に応じて、Service配下のPodを選びます。Envoyは、エンドポイントをPodのIPアドレスや宛先ポートで管理しています (例:<PodのIPアドレス>:50010) 。
  6. 宛先との間でTLS接続を確立し、パケットのL7のアプリケーションデータを暗号化します。そして、HTTPSリクエストを宛先PodにL7ロードバランシングします。

宛先Pod側のistio-proxyコンテナ

  • L7ロードバランシングされたHTTPSリクエストの宛先ポート (例:50010) で、リスナーを絞り込みます。Envoyは、リスナーを宛先ポートで管理しています (例:0.0.0.0_50010)
  • HTTPSリクエストを処理するための各種フィルターを選びます。
  • HTTPフィルターにより、HTTPSリクエストの宛先ポート (例:50010) で、ルートを絞り込みます。Envoyは、ルートを宛先ポートで管理しています (例:inbound|50010||) 。
  • HTTPフィルターにより、HTTPSリクエストの宛先ホスト (例:example.com) やパス (例:/) で、クラスターを絞り込みます。Envoyは、クラスターを宛先ポートで管理しています (例:inbound|50010||)
  • エンドポイントを選びます。Envoyは、エンドポイントをローカルホストや宛先ポートで管理しています (例:127.0.0.6:50010) 。
  • ローカルホストにHTTPSリクエストを送信します。結果的に、宛先マイクロサービスにHTTPSリクエストが届きます。


サービスメッシュ外へのHTTPS

ここでは、istio-proxyコンテナはHTTPSリクエストを処理するとします。

図中の番号に沿って、通信の仕組みを解説します。

istio_envoy_envoy-flow_egress

送信元Pod側のistio-proxyコンテナ

  1. 送信元マイクロサービスからのHTTPSリクエストの宛先ポート (例:443) で、リスナーを絞り込みます。Envoyは、リスナーを宛先ポートで管理しています (例:0.0.0.0_443) 。
  2. HTTPSリクエストを処理するための各種フィルターを選びます。また、宛先とTLSハンドシェイクを実行し、パケットのL7のアプリケーションデータを復号化します。
  3. HTTPフィルターにより、HTTPSリクエストの宛先ポート (例:443) で、ルートを絞り込みます。Envoyは、ルートを宛先ポートで管理しています (例:443) 。
  4. HTTPフィルターにより、HTTPSリクエストの宛先ホスト (例:istio-egressgateway-service.foo-namespace.svc.cluster.local) やパス (例:/) で、クラスターを絞り込みます。Envoyは、クラスターをIstio EgressGateway 宛先ポートやホストで管理しています (例:outbound|443|istio-egressgateway-service.foo-namespace.svc.cluster.local) 。
  5. 設定した負荷分散方式 (例:ラウンドロビン、など) に応じて、Istio EgressGateway Service配下のPodを選びます。Envoyは、エンドポイントをPodのIPアドレスや宛先ポートで管理しています (例:<PodのIPアドレス>:443) 。
  6. 宛先との間でTLS接続を確立し、パケットのL7のアプリケーションデータを暗号化します。そして、Istio EgressGateway PodにL7ロードバランシングします。

宛先Pod (Istio EgressGateway Pod) 側のistio-proxyコンテナ

  • L7ロードバランシングされたHTTPSリクエストの宛先ポート (例:443) で、リスナーを絞り込みます。Envoyは、リスナーを宛先ポートで管理しています (例:0.0.0.0_443)
  • HTTPSリクエストを処理するための各種フィルターを選びます。
  • HTTPフィルターにより、HTTPSリクエストの宛先ポート (例:443) で、ルートを絞り込みます。Envoyは、ルートを宛先ポートで管理しています (例:inbound|50010||) 。
  • HTTPフィルターにより、HTTPSリクエストの宛先ホスト (例:external.com) やパス (例:/) で、クラスターを絞り込みます。Envoyは、クラスターを宛先ポートやホストで管理しています (例:outbound|443|external.com) 。
  • エンドポイントを選びます。Envoyは、エンドポイントをエントリ済システムのIPアドレスや宛先ポートで管理しています (例:<エントリ済システムのIPアドレス>:50010) 。エントリ済システムのIPアドレスは、開発者が設定する必要はなく、EnvoyがDNSから動的に取得します。
  • エントリ済システムにHTTPSリクエストを送信します。


07. おわりに

IstioサイドカーメッシュがEnvoyのHTTPSリクエストの処理をどのように抽象化するのか、またEnvoyがどのようにHTTPSリクエストを処理するのかを解説しました。

次々とサービスメッシュツールが登場したとしても、それがEnvoyを使用したサービスメッシュである限り、最終的にはEnvoyの設定値に行き着きます。

そのため、抽象化されたEnvoyがどのように通信を扱うのかを一度でも理解すれば、様々なサービスメッシュツールで知識を流用できると思います。

Istioはもちろん、他のEnvoyによるサービスメッシュツール (Consul、Cilium、など) を使っている方の参考にもなれば幸いです👍🏻


謝辞

今回、Kubernetesのネットワークを調査するにあたり、以下の方に知見をご教授いただきました。

この場で感謝申し上げます🙇🏻‍


記事関連のおすすめ書籍


【ArgoCD🐙️】KubernetesのマルチテナントパターンとArgoCDの実践テナント設計


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

  • Kubernetesのマルチテナントパターンの種類
  • マルチテナントパターンをArgoCDで実践する場合にオススメのパターン (★)
  • ArgoCDのNamespacedスコープモードとClusterスコープモード
  • ArgoCDのテナントが防いでくれる誤った操作の具体例

記事のざっくりした内容は、以下のスライドからキャッチアップできちゃいます!



01. はじめに


どうも、熟成アルトバイエルンです。

argocd_community-board
画像引用元:Argo Project


さて最近の業務で、全プロダクトの技術基盤開発チームに携わっており、全プロダクト共有のArgoCD🐙のマルチテナント化を担当しました。

プロダクトが稼働するKubernetes Clusterが数十個あり、Clusterによっては複数のチームが合計100個以上のマイクロサービスを動かしています。

このような大規模なマイクロサービスシステムがいくつもある状況下で、ArgoCDのマルチテナント設計の知見を深められたため、記事で解説しました。

書きたいことを全部書いたところ、情報量がエグいことになってしまったため、気になる章だけでも拾って帰っていただけるとハッピーです🙏

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. なぜマルチテナントが必要なのか

シングルテナントの場合

そもそも、なぜArgoCDにマルチテナントが必要なのでしょうか。

例えば、マニフェストのデプロイ先となるプロダクト用Cluster (例:foobarbaz) があると仮定します。

ArgoCDをシングルテナントにする場合、各プロダクトチームの操作するApplicationを同じテナントに共存させることになります。

この場合、単一のargocd-server (ダッシュボード) から全てのApplicationを操作できて便利です。

しかし、プロダクト用Cluster数が増えていくにつれて、問題が起こり始めます。

例えば、いずれかのプロダクトチームが誤ったApplicationを操作し、結果的に誤ったプロダクト用Clusterにマニフェストをデプロイしてしまう可能性があります。

もちろん、システムでインシデントを起こしてやろうという悪意を持った人が、誤ったプロダクト用Clusterを意図的に選ぶ可能性もあります😈

argocd_single-tenant


マルチテナントの場合

その一方で、いい感じのマルチテナントにしたとします。

プロダクトチームは、認可されたテナントに所属するApplicationにのみを操作でき、反対に無認可のテナントのApplicationは操作できません。

これにより、誤ったプロダクト用Clusterにマニフェストをデプロイすることを防げます。

argocd_multi-tenant


03. Kubernetesのマルチテナントパターン

マルチテナントパターンの一覧

ArgoCDのテナント設計を実践する前に、Kubernetesにはどんなマルチテナントパターンがあるのでしょうか。

Kubernetesのマルチテナントパターンは、以下に大別できます。

Clusters
as-a-Service
Control Planes
as-a-Service
Namespaces
as-a-Service
カスタムリソース
テナント
テナント単位 実Cluster 仮想Cluster Namespace ツール固有の論理空間
テナント間でKubernetesリソースを分離できるか Cluster
スコープ
リソース
ツールによる
Namespaced
スコープ
リソース
ツールによる
ツール
  • AWS EKS
  • GCP GKE
  • Azure AKE
  • Kubeadm
など
  • Kcp
  • tensile-kube
  • vcluster
  • VirtualCluster
など
Namespaceを増やすだけなので特別なツール不要
  • ArgoCDのAppProject
  • CapsuleのTenant
  • kioskのAccount
  • KubeZooのTenant
など


▶ 他のマルチテナントの分類方法について

本記事では言及しませんが、"ソフトマルチテナンシー""ハードマルチテナンシー" といった分類方法もあります。

この分類方法では、テナント間の分離度の観点で各マルチテナントを種別します。

ソフトマルチテナンシーは、互いに信頼できる前提の上で、テナント間を弱く分離します。

その一方で、ハードマルチテナンシーは、互いに信頼できない前提の上でテナント間を強く分離します。

分離度がソフトとハードのいずれであるかに客観的な指標がなく、やや曖昧な種別になってしまうため、本記事の X as-a-Service の方が個人的には好みです♡♡♡


Clusters as-a-Service

Clusters as-a-Serviceは、テナントごとに独立したClusterを提供します。

ツールとして、AWS EKS、GCP GKE、Azure AKE、Kubeadm、などがあります。

argocd_multi-tenant_k8s_clusters-as-a-service


Control Planes as-a-Service

Control Planes as-a-Serviceは、テナントごとに独立したコントロールプレーン (言い換えば仮想Cluster) を提供します。

ツールとして、Kcptensile-kubevclusterVirtualCluster、などがあります。

argocd_multi-tenant_k8s_control-planes-as-a-service


Namespaces as-a-Service

Namespaces as-a-Serviceは、テナントごとに独立したNamespaceを提供します。

Namespaceを増やすだけなため、ツールは不要です。

argocd_multi-tenant_k8s_namespaces-as-a-service


カスタムリソーステナント

カスタムリソーステナントは、テナントごとにツール固有の論理空間 (例:ArgoCDのAppProject、CapsuleのTenant、kioskのAccount、KubeZooのTenant、など) を提供します。

ツールによっては、X as-a-Service も兼ねている場合があります。

今回紹介するAppProjectはNamespaceテナントを兼ねており、カスタムリソーステナント で解説しています。

argocd_multi-tenant_k8s_tool


04. ArgoCDでのテナントパターン実践一覧

お待たせしました。

ここからは、KubernetesのマルチテナントパターンをArgoCDで具体的に実践し、おすすめのパターン実践を解説していきます。

なお、オススメするものを ★ としています。

実Cluster
テナント
仮想Cluster
テナント
Namespace
テナント
AppProject
テナント
CLモード
AppProject
テナント
NSモード
対応するテナントパターン Clusters
as-a-Service
Control Planes
as-a-Service
Namespaces
as-a-Service
カスタムリソース
テナント
ArgoCDがテナント間で占有 / 共有 占有 占有 占有 共有 占有
テナント間でKubernetesリソースを分離できるか Namespaced
スコープ
リソース
Cluster
スコープ
リソース
オススメ ★★


以降の図の凡例です。

ArgoCDの各コンポーネント (application-controller、argocd-server、dex-server、repo-server) と各リソース (Application、AppProject) を区別しています。

argocd_multi-tenant_legend


04-02. Clusters as-a-Service 実践

実Clusterテナント

実Clusterテナントは、Clusters as-a-Serviceなテナントの実践であり、実際のClusterをテナントの単位とします。

後述の仮想Clusterと対比させるために、"実Cluster" と呼ぶことにします。

各プロダクトチームは、実Clusterテナント内のApplicationを操作し、正しいプロダクト用Clusterにマニフェストをデプロイします。

argocd_multi-tenant_physical_cluster


オススメしない理由

実Clusterテナントには、以下のメリデメがあります。

デメリットの回避策も考慮して、独断と偏見でオススメしませんでした。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 - テナントを増やすために実Clusterを用意する必要があり、作業量が多い。 ➡︎ IaCツールで実Clusterを用意するようにすれば作業量を減らせるが、やっぱりとてもつらい😭
安全性
(セキュリティ)
ClusterからClusterへの名前解決を不可能にすれば、他のテナントからの通信を遮断できる。 - ➡︎ -
保守性 ClusterスコープまたはNamespacedスコープなKubernetesリソースを他のテナントから分離できる。
これらのKubernetesリソース (特にCRD) の変更が他のテナントに影響しない。
各テナントが、個別に実Clusterを保守しないといけない。(例:アップグレード、機能修正、など) ➡︎ 回避できず、とてもつらい😭
性能 Clusterのハードウェアリソースを他のテナントと奪い合うことなく、これを独占できる。 - ➡︎ -
信頼性 テナントごとに実Clusterが独立しており、他の実Clusterから障害の影響を受けない。 - ➡︎ -


04-03. Control Planes as-a-Service 実践

仮想Clusterテナント - ★

仮想Clusterテナントは、Control Planes as-a-Serviceなテナントの実践であり、仮想Clusterをテナントの単位とします。

各プロダクトチームは、仮想Clusterテナント内のApplicationを操作し、正しいプロダクト用Clusterにマニフェストをデプロイします。

argocd_multi-tenant_virtual_cluster


オススメした理由

仮想Clusterテナントには、以下のメリデメがあります。

デメリットの回避策も考慮して、独断と偏見で オススメ しました。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 テナントを増やすためにマニフェストで定義した仮想Clusterを用意するだけでよく、実Clusterを用意することと比べて作業量が少ない。 - ➡︎ -
安全性
(セキュリティ)
仮想ClusterからホストClusterへの名前解決を不可能にすれば、他のテナントからの通信を遮断できる。 - ➡︎ -
保守性 ClusterスコープまたはNamespacedスコープなKubernetesリソースを他のテナントから分離できる。
これらのKubernetesリソース (特にCRD) の変更が他のテナントに影響しない。
各テナントが、個別に仮想Clusterを保守しないといけない。(例:アップグレード、機能修正、など) ➡︎ 仮想Clusterに関する知見を持つ組織であれば、各テナントで保守できる。
性能 - Clusterのハードウェアリソースを他のテナントと奪い合うことになる。 ➡︎ 多くの利用者が同時並行的にArgoCDを操作する状況になりにくければ、奪い合いも起こらない。
信頼性 テナントごとに仮想Clusterが独立しており、他の仮想Clusterから障害の影響を受けない。 - ➡︎ -


04-04. Namespaces as-a-Service 実践

Namespaceテナントは、Namespaces as-a-Serviceなテナントの実践であり、Namespaceをテナントの単位とします。

後述の AppProjectテナント は二重のテナントを持ち、Namespaceテナントも兼ねています。

そのため、ここではNamespaceテナントの解説は省略します。


04-05. カスタムリソーステナントの実践

AppProjectテナント

argocd_multi-tenant_appproject

AppProjectテナントは、カスタムリソーステナントの実践であり、NamespaceとAppProjectをテナントの単位とします。

AppProjectテナントは、二重のテナント (第一テナントにNamespace第二テナントに複数のAppProject) を持ち、"あらゆる面から" マニフェストのデプロイを制限します。

特に、AppProjectはNamespaceスコープなカスタムリソースであり、自身に所属するApplicationを一括して制限します。

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: foo-tenant
  namespace: foo
  # 自身に所属するApplicationを制限する
spec: ...
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: infra-application
  namespace: foo
spec:
  # foo-tenantに所属する
  project: foo-tenant
  ...


▶ カスタムリソースの仕様について

カスタムリソースのCRDの実装から、ドキュメントに未記載の仕様を読み取れることがあります。

例えば、本記事ではAppProjectに自由にNamespaceを設定していますが、ArgoCDのドキュメントには任意のNamespaceを設定できることが明記されていません。

しかし、AppProjectのCRDの.spec.scopeキーからも分かる通り、AppProjectはNamespacedスコープなカスタムリソースであり、任意のNamespaceを設定できます👍
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
  labels:
    app.kubernetes.io/name: appprojects.argoproj.io
    app.kubernetes.io/part-of: argocd
  name: appprojects.argoproj.io
spec:
  group: argoproj.io
  names:
    kind: AppProject

    ...

  # Namespacedスコープなカスタムリソースであるとわかる
  scope: Namespaced

...

CLモード vs. NSモード

ArgoCDには、ClusterスコープモードNamespacedスコープモード (以降、"CLモード""NSモード") があります。

スコープモードに応じて、AppProjectテナントの設計方法が異なります。

本章では、CLモードとNSモードの両方でAppProjectテナントを解説していきます。


05. CLモードなArgoCD

CLモードなArgoCDとは

CLモードなArgoCDの場合、各テナント間で共有のArgoCDを管理します

例えば、AppProjectテナントとして、プロダクト別のNamespace (foobarbaz) とAppProject (foobarbaz) を用意します。

別途、ArgoCD専用のNamespace (argocd) を用意し、ここに関連するKubernetesリソース (例:ConfigMap) を配置します。

各プロダクトチームは、AppProjectテナント内のApplicationを操作し、正しいプロダクト用Clusterにマニフェストをデプロイします。

argocd_multi-tenant_appproject_cluster-scope

AppProject

NSモードと同様にして、AppProjectに所属するApplicationによるマニフェストのデプロイを制限できます。

例えば、以下のような実装になります。

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: foo-tenant
  namespace: foo
spec:
  destinations:
    # ArgoCD用Clusterに関する認可を設定する
    # App-Of-Appsパターンの場合に使用する
    - namespace: foo
      server: "https://kubernetes.default.svc"
    # プロダクト用Clusterに関する認可を設定する
    - namespace: "*"
      server: https://foo-cluster.gr7.ap-northeast-1.eks.amazonaws.com
  # CLモードでは設定が必要である
  sourceNamespaces:
    - foo

Applicationを操作するログインユーザーが、無認可のNamespaceやClusterをデプロイ先に指定できないように、.spec.destinationキーで制限しています。

一方で後述のNSモードとは異なり、CLモードなArgoCDは任意のNamespaceのApplicationにアクセスできます。

そのため、.spec.sourceNamespacesキーで、特定のNamespaceのApplicationがこのAppProjectに所属できないように、ApplicationのNamespaceを制限しています。

ArgoCDコンポーネント用ConfigMap (argocd-cmd-params-cm)

NSモードと同様にして、argocd-cmd-params-cmでは、ArgoCDの各コンポーネントのコンテナの引数を設定できます。

例えば、以下のような実装になります。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  # 専用のNamespaceを設定する
  namespace: argocd
data:
  # CLモードでは設定が必要である
  # 全てのNamespaceを指定したい場合は、ワイルドカードを設定する
  application.namespaces: "*"

.application.namespacesキーは、argocd-serverとapplication-controllerの--application-namespacesオプションに相当します。

一方での後述のNSモードとは異なり、CLモードなArgoCDは任意のNamespaceのApplicationにアクセスできます。

--application-namespacesオプションで、任意のNamespaceにアクセスするための認可を設定できます。

--application-namespacesオプションの設定方法について

argocd-cmd-params-cmの代わりに、例えば以下のようにPodに引数を直接渡しても良いです🙆🏻‍

例えば、以下のような実装になります。
apiVersion: v1
kind: Pod
metadata:
  name: argocd-server
  namespace: argocd
spec:
  containers:
    - name: argocd-server
      image: quay.io/argoproj/argocd:latest
      args:
        - /usr/local/bin/argocd-server
        # コンテナ起動時の引数として
        - --application-namespaces="*"

  ...
apiVersion: v1
kind: Pod
metadata:
  name: argocd-application-controller
  namespace: argocd
spec:
  containers:
    - name: argocd-application-controller
      image: quay.io/argoproj/argocd:latest
      args:
        - /usr/local/bin/argocd-application-controller
        # コンテナ起動時の引数として
        - --application-namespaces="*"

  ...

ログインユーザー用ConfigMap (argocd-rbac-cm)

NSモードと同様にして、argocd-rbac-cmでは、Applicationを操作するログインユーザーが、無認可のAppProjectやNamespaceに所属するApplicationを操作できないように制限します。

例えば、以下のような実装になります。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  # 専用のNamespaceを設定する
  namespace: argocd
data:
  # デフォルトのロール
  # @see https://github.com/argoproj/argo-cd/blob/master/assets/builtin-policy.csv#L9-L16
  policy.default: role:readonly
  policy.csv: |
    p, role:foo, *, *, foo/*/*, allow
    p, role:bar, *, *, bar/*/*, allow
    p, role:baz, *, *, baz/*/*, allow

    g, foo-team, role:foo
    g, bar-team, role:bar
    g, baz-team, role:baz
  scopes: "[groups]"

認証済みグループ (foo-team、bar-team、baz-team) に対して、無認可のAppProject (foobarbaz) に所属するApplicationを操作できないように、認可スコープを制限しています。


▶ AppProjectの認可定義の記法について

ConfigMap (argocd-rbac-cm) の認可スコープの定義には、 Casbin の記法を使用します。

今回の実装例で使用したp (パーミッション) とg (グループ) では、以下を記法を使用できます👍
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    # ロールとArgoCD系カスタムリソースの認可スコープを定義する
    p, role:<ロール名>, <Kubernetesリソースの種類>, <アクション名>, <AppProject名>/<ApplicationのNamespace名>/<Application名>, <許否>

    # 認証済みグループにロールを紐付ける
    g, <グループ名>, role:<ロール名>
  scopes: "[groups]"



オススメしない理由

CLモードなArgoCDのAppProjectテナントには、以下のメリデメがあります。

デメリットの回避策も考慮して、独断と偏見でオススメしませんでした。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 テナントを増やすためにNamespaceとAppProjectを用意するだけでよく、作業量が少ない。 - ➡︎ -
安全性
(セキュリティ)
NetworkPolicyでNamespace間の名前解決を不可能にすれば、他のNamespaceからの通信を遮断できる。 - ➡︎ -
保守性 ArgoCD用Clusterの管理者が単一のClusterを保守すればよい。(例:アップグレード、機能修正、など) AppProjectはNamespacedスコープなカスタムリソースのため、ClusterスコープなKubernetesリソースを他のテナントと共有しないといけない。
そのため、ClusterスコープなKubernetesリソース (特にCRD) の変更は全てのテナントに影響する。
➡︎ ArgoCDのアップグレード時 (CRDの変更時) は、ついでにKubernetesもアップグレードしたい。
新しいClusterを別に作成し、そこで新ArgoCDを作成すれば一石二鳥である。
性能 - Clusterのハードウェアリソースを他のテナントと奪い合うことになる。 ➡︎ 多くの利用者が同時並行的にArgoCDを操作する状況になりにくければ、奪い合いも起こらない。
信頼性 - ClusterまたはArgoCDで障害が起こると、これは全てのテナントに影響する。 ➡︎ 代わりにNodeやArgoCDを十分に冗長化して可用性を高めれば、影響を緩和できる。
ただ、そもそもの影響範囲が大きすぎる😭


05-02. NSモードなArgoCD - ★★

NSモードなArgoCDとは

NSモードなArgoCDの場合、前述のCLモードとは異なり、各AppProjectテナント間でArgoCDを占有します。

例えば、AppProjectテナントとして、プロダクト別のNamespace (foobarbaz) とAppProject (foobarbaz) を用意します。

各AppProjectテナントに、ArgoCDと関連するKubernetesリソース (例:ConfigMap) を配置します。

各プロダクトチームは、AppProjectテナント内のApplicationを操作し、正しいプロダクト用Clusterにマニフェストをデプロイします。

argocd_multi-tenant_appproject_namespaced-scope

AppProject

CLモードと同様にして、AppProjectに所属するApplicationによるマニフェストのデプロイを制限できます。

例えば、以下のような実装になります。

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: foo-tenant
  namespace: foo
spec:
  destinations:
    # ArgoCD用Clusterに関する認可を設定する
    # App-Of-Appsパターンの場合に使用する
    - namespace: foo
      server: "https://kubernetes.default.svc"
    # プロダクト用Clusterに関する認可を設定する
    - namespace: "*"
      server: https://foo-cluster.gr7.ap-northeast-1.eks.amazonaws.com
# NSモードでは設定が不要である
# sourceNamespaces:
#   - foo

Applicationを操作するログインユーザーが、無認可のNamespaceやClusterをデプロイ先に指定できないように、.spec.destinationキーで制限しています。

前述のCLモードとは異なり、NSモードなArgoCDは自身が所属するNamespaceのApplicationのみにアクセスできます。

そのため、.spec.sourceNamespacesキーでマニフェストのデプロイを制限する必要はありません。

ArgoCDコンポーネント用ConfigMap (argocd-cmd-params-cm)

CLモードと同様にして、argocd-cmd-params-cmでは、ArgoCDの各コンポーネントのコンテナの引数を設定できます。

例えば、以下のような実装になります。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  namespace: foo
data:
# NSモードでは設定が不要である
# application.namespaces: "*"

前述の通り、.application.namespacesキーは、argocd-serverとapplication-controllerの--application-namespacesオプションに相当します。

前述のCLモードとは異なり、NSモードなArgoCDは自身が所属するNamespaceのApplicationのみにアクセスできます

そのため、.application.namespacesキーでNamespaceに関する認可を設定する必要はありません

もちろん、Podのコンテナ引数にも設定は不要です。

ログインユーザー用ConfigMap (argocd-rbac-cm)

CLモードと同様にして、argocd-rbac-cmでは、Applicationを操作するログインユーザーが、無認可のAppProjectやNamespaceに所属するApplicationを操作できないように制限します。

例えば、以下のような実装になります。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: foo
data:
  # デフォルトのロール
  # @see https://github.com/argoproj/argo-cd/blob/master/assets/builtin-policy.csv#L9-L16
  policy.default: role:readonly
  policy.csv: |
    p, role:app, *, *, app/*/*, allow
    p, role:infra, *, *, infra/*/*, allow

    g, app-team, role:app
    g, infra-team, role:infra
  scopes: "[groups]"

認証済みグループ (app-team、infra-team) に対して、無認可のAppProject (appinfra) に所属するApplicationを操作できないように、認可スコープを制限しています。


特にオススメした理由

NSモードなArgoCDのAppProjectテナントには、以下のメリデメがあります。

デメリットの回避策も考慮して、独断と偏見で 特にオススメ しました。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 テナントを増やすためにNamespaceとAppProjectを用意するだけでよく、作業量が少ない。 - ➡︎ -
安全性
(セキュリティ)
NetworkPolicyでNamespace間の名前解決を不可能にすれば、他のNamespaceからの通信を遮断できる。 - ➡︎ -
保守性 単一のClusterを保守すればよい。(例:アップグレード、機能修正、など) AppProjectはNamespacedスコープなカスタムリソースのため、ClusterスコープなKubernetesリソースを他のテナントと共有しないといけない。
そのため、ClusterスコープなKubernetesリソース (特にCRD) の変更は全てのテナントに影響する。
➡︎ ArgoCDのアップグレード時 (CRDの変更時) は、ついでにKubernetesもアップグレードしたい。
新しいClusterを別に作成し、そこで新ArgoCDを作成すれば一石二鳥である。
性能 - Clusterのハードウェアリソースを他のテナントと奪い合うことになる。 ➡︎ 多くの利用者が同時並行的にArgoCDを操作する状況になりにくければ、奪い合いも起こらない。
信頼性 テナントごとにArgoCDを占有しており、他のArgoCDから障害の影響を受けない。 Clusterで障害が起こると、これは全てのテナントに影響する。 ➡︎ 代わりに、Nodeを十分に冗長化して可用性を高める。
いずれかのインスタンスで障害が起こっても、正常なインスタンスでArgoCDが稼働できる。


AppProjectテナント例の一覧

NSモードなArgoCDを採用する場合、AppProjectテナント例を解説していきます。

前述の通り、AppProjectテナントが二重テナント (第一テナントにNamespace、第二テナントに複数のAppProject) を持つことに留意してください。

なお、オススメするものを ★ としています。

テナント例
(二重テナント)
オススメ
Namespace
(第一テナント)
AppProject
(第二テナント)
テナント例1 プロダクトの実行環境別 プロダクトの実行環境別
テナント例2 プロダクト別 プロダクトの実行環境別
テナント例3 プロダクト別 プロダクトのサブチーム別 ★★


▶ Namespaceの分割パターンについて

本記事では言及しませんが、Namespaceにはいくつかの分割パターンがあり、本記事ではこれも取り入れてAppProjectテナントを設計しています。

例えば、"管理チーム別" (今回でいうプロダクト別) というNamespaceの分割パターンは、様々な著名な書籍やブログで紹介されています👀


テナント例1

Namespace (プロダクトの実行環境別)、AppProject (プロダクトの実行環境別)

プロダクトの実行環境 (Dev環境、Tes環境) 別に管理されたClusterがいる状況と仮定します。

この場合に、プロダクトの実行環境別にNamespace (devtes) とAppProject (devtes) を用意します。

argocd_multi-tenant_appproject_namespaced-scope_pattern_1

オススメしなかった理由

テナント例1には、以下のメリデメがあります。

独断と偏見でオススメしませんでした。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 - ArgoCDのPod数が多くなり、将来的にNode当たりのPodやIPアドレスの上限数にひっかかりやすい。
その時点で、AppProjectテナントの増やせなくなる。
➡︎ 例えばAWS EKSの場合、Node数を増やしたり、Nodeのスペックを上げる。
ただ、お金がかかる😭
安全性
(セキュリティ)
ログインユーザー用ConfigMap (argocd-rbac-cm) を使用すれば、無認可の実行環境別AppProjectに所属するApplicationを操作できないように制限できる。 - ➡︎ -
保守性 異なる実行環境に関するApplicationが共存しておらず、別のargocd-serverから操作することになるため、実行環境間の選択ミスが起こりにくい。 - ➡︎ -


テナント例2 - ★

Namespace (プロダクト別)、AppProject (プロダクトの実行環境別)

プロダクトの実行環境 (Dev環境、Tes環境) 別に管理されたClusterがいる状況と仮定します。

プロダクト別にNamespace (foobar) 、プロダクトの実行環境別にAppProject (devtes) を用意します。

argocd_multi-tenant_appproject_namespaced-scope_pattern_2

オススメした理由

テナント例2には、以下のメリデメがあります。

独断と偏見で オススメ しました。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 ArgoCDのPod数が多くなり、将来的にNode当たりのPodやIPアドレスの上限数にひっかかりにくい。 - ➡︎ -
安全性
(セキュリティ)
ログインユーザー用ConfigMap (argocd-rbac-cm) を使用すれば、無認可の実行環境別AppProjectを操作できないように制限できる。 - ➡︎ -
保守性 - 異なる実行環境に関するApplicationが共存しており、同じargocd-server (ダッシュボード) から操作することになるため、実行環境間の選択ミスが起こりやすい。 ➡︎ ダッシュボードにはApplicationのフィルタリング機能があるため、選択ミスを回避できる。


テナント例3 - ★★

Namespace (プロダクト別)、AppProject (プロダクトのサブチーム別)

プロダクトの実行環境 (Dev環境、Tes環境) 別に管理されたClusterがいる状況と仮定します。

プロダクト別にNamespace (foobar) 、プロダクトのサブチーム別にAppProject (appinfra) を用意します。

argocd_multi-tenant_appproject_namespaced-scope_pattern_3

特にオススメした理由

テナント例3には、以下のメリデメがあります。

独断と偏見で 特にオススメ しました。

アーキテクチャ
特性
メリット ⭕️ デメリット × デメリットの回避策
拡張性 ArgoCDのPod数が多くなり、将来的にNode当たりのPodやIPアドレスの上限数にひっかかりにくい。 - ➡︎ -
安全性
(セキュリティ)
ログインユーザー用ConfigMap (argocd-rbac-cm) を使用すれば、無認可のサブチーム別AppProjectに所属するApplicationを操作できないように制限できる。 - ➡︎ -
保守性 - 異なる実行環境に関するApplicationが共存しており、同じargocd-server (ダッシュボード) から操作することになるため、実行環境間の選択ミスが起こりやすい。 ➡︎ ダッシュボードにはApplicationのフィルタリング機能があるため、選択ミスを回避できる。


06. どのような誤った操作を防いでくれるのか

そろそろ解説を読むのがしんどい方がいるのではないでしょうか。

『君がッ、泣くまで、解説をやめないッ!』

AppProjectテナントとNamespacedスコープモードがマニフェストのデプロイをどのように制限するのかについて、例を挙げて解説します。

ここでは、以下のAppProjectを作成したと仮定します。

AppProjectテナントが二重テナント (第一テナントにNamespace、第二テナントに複数のAppProject) を持つことに留意してください。

apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  # appチーム
  name: app
  namespace: foo
spec:
  destinations:
    # ArgoCD用Clusterに関する認可を設定する
    # Namespace (foo) へのデプロイを許可する
    - namespace: foo
      server: "https://kubernetes.default.svc"
      # プロダクト用Clusterに関する認可を設定する
      # Namespace (app) へのデプロイを許可する
    - namespace: app
      server: https://foo-cluster.gr7.ap-northeast-1.eks.amazonaws.com
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  # infraチーム
  name: infra
  namespace: foo
spec:
  destinations:
    # ArgoCD用Clusterに関する認可を設定する
    # Namespace (foo) へのデプロイを許可する
    - namespace: foo
      server: "https://kubernetes.default.svc"
    # プロダクト用Clusterに関する認可を設定する
    # Namespace (infra) へのデプロイを許可する
    - namespace: infra
      server: https://foo-cluster.gr7.ap-northeast-1.eks.amazonaws.com


マニフェストのデプロイ制限

プロダクトの実行環境 (Dev環境、Tes環境) 別に管理されたClusterがいる状況と仮定します。

プロダクト別にNamespace (foo) 、プロダクトのサブチーム別にAppProject (appinfra) を用意します。

AppProjectテナントは、例えば 赤線 の方法で、マニフェストのデプロイを制限します。

argocd_multi-tenant_appproject_namespaced-scope_restrict-manifest-deploy

マニフェストをデプロイできる場合

マニフェストを正しくデプロイする場合、AppProjectテナントはこれを制限しません。

(1) argocd-serverは、argocd-cmd-params-cmからアクセスできるNamespaceを取得します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  namespace: foo
data:
# 設定しないことで、argocd-serverは同じNamespaceにしかアクセスできなくなる。
# application.namespaces: "*"

(2) fooプロダクトのinfraチームが、argocd-serverを操作します。

(3) argocd-serverは、argocd-rbac-cmからApplication操作に関する認可スコープを取得します

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: foo
data:
  policy.default: role:readonly
  policy.csv: |
    p, role:app, *, *, app/*/*, allow
    p, role:infra, *, *, infra/*/*, allow

    g, app-team, role:app
    g, infra-team, role:infra
  scopes: "[groups]"

(4) infraチームは、認可されたAppProjectに所属するApplicationを操作します。

(5) infraチームは、Dev環境のfooプロダクト用ClusterのNamespace (infra) にマニフェストをデプロイできます。

(🚫制限例1) 無認可のNamespaceでApplicationを作成しようとした場合

例えば、fooプロダクトのinfraチームが無認可のNamespace (bar) でApplicationを作成しようとします。

すると、argocd-serverは以下のようなエラーを返却し、この操作を制限します。

namespace bar is not permitted in project 'infra-team'

無認可のNamespaceでApplicationを作れてしまうと、そのApplicationから無認可のプロダクト用Clusterにマニフェストをデプロイできてしまいます😈

(🚫制限例2) 無認可のAppProjectでApplicationを作成しようとした場合

例えば、fooプロダクトのinfraチームが、無認可のAppProject (app) でApplicationを作成しようとします。

すると、argocd-serverは以下のようなエラーを返却し、この操作を制限します。

Application referencing project 'app' which does not exist

任意のAppProjectでApplicationを作成できてしまうと、そのApplicationから無認可のプロダクト用Clusterにマニフェストをデプロイできてしまいます😈

(🚫制限例3) 無認可のClusterをデプロイ先に指定しようとした場合

例えば、fooプロダクトのinfraチームがApplicationを操作し、無認可のプロダクト用Cluster (bar-cluster) をデプロイ先として指定しようします。

すると、argocd-serverは以下のようなエラーを返却し、この操作を制限します。

application destination
{https://bar-cluster.gr7.ap-northeast-1.eks.amazonaws.com infra} is not permitted in project 'infra-team'

任意のClusterをデプロイ先に指定できてしまうと、Applicationから無認可のプロダクト用Clusterにマニフェストをデプロイできてしまいます😈

(🚫制限例4) 無認可のNamespaceをデプロイ先に指定しようとした場合

例えば、fooプロダクトのinfraチームがApplicationを操作し、無認可のNamespace (app) をデプロイ先に指定しようします。

すると、argocd-serverは以下のようなエラーを返却し、この操作を制限します。

application destination
{https://foo-cluster.gr7.ap-northeast-1.eks.amazonaws.com app} is not permitted in project 'infra-team'

任意のNamespaceをデプロイ先に指定できてしまうと、そのApplicationから無認可のNamespaceにマニフェストをデプロイできてしまいます😈


▶ AppProjectで設定できる認可の種類について

AppProjectテナントは、その他にも以下のような認可を設定できます。

  • argocd-serverとapplication-controllerでデプロイできるKubernetesリソースの種類 (.spec.clusterResourceWhitelistキー、.spec.namespaceResourceWhitelistキー、など)
  • repo-serverでポーリングできるリポジトリ (.spec.sourceReposキー)
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: foo-tenant
  namespace: foo
spec:
  clusterResourceWhitelist:
    - group: "*"
      kind: "*"
  namespaceResourceWhitelist:
    - group: "*"
      kind: "*"
  sourceRepos:
    - "*"

  ...

"AppProjectテナントによるマニフェストのデプロイ丸ごとの制限" という観点でテーマが異なるため、本記事では言及しませんでした🙇🏻‍


カスタムリソースのReconciliation制限

プロダクトの実行環境 (Dev環境、Tes環境) 別に管理されたClusterがいる状況と仮定します。

プロダクト別にNamespace (foo) 、プロダクトのサブチーム別にAppProject (appinfra) を用意します。

AppProjectテナントは、例えば 赤線 の方法で、ArgoCD系カスタムリソースに対するapplication-controllerのReconciliationを制限します。

argocd_multi-tenant_appproject_namespaced-scope_restrict-reconciliation

ArgoCD系カスタムリソースをReconciliationできる場合

正しいNamespaceに対してReconciliationを実行する場合、AppProjectテナントはこれを制限しません。

(1) application-controllerは、argocd-cmd-params-cmから自身がアクセスできるNamespaceを取得します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  namespace: foo
data:
# 設定しないことで、application-controllerは同じNamespaceにしかアクセスできなくなる。
# application.namespaces: "*"

(2) application-controllerは、同じNamespaceに所属するArgoCD系カスタムリソースに対して、Reconciliationを実行します。

(🚫制限例1) 無認可のNamespaceにReconciliationを実行しようとした場合

例えば、application-controllerがReconciliationの対象とするNamespaceを選ぼうとしているとします。

すると、application-controllerは内部で検証メソッドを実行し、無認可のNamespace (bar) は選ばないようにします。


07. おわりに

KubernetesのマルチテナントパターンとArgoCDでのパターン実践をもりもり布教しました。

あらゆる面からマニフェストのデプロイを制限してくれる、AppProjectテナントの素晴らしさが伝わりましたでしょうか。

KubernetesのマルチテナントパターンをArgoCDでどう実践するべきか、について困っている方の助けになれば幸いです👍


謝辞

本記事のタイトルは、私が崇拝しているドメイン駆動設計の書籍 "実践ドメイン駆動設計" から拝借しました🙏

また、ArgoCDでのパターン実践の収集にあたり、以下の方からの意見も参考にさせていただきました。

この場で感謝申し上げます🙇🏻‍


記事関連のおすすめ書籍


【Terraform🧑‍🚀】tfstateファイルの分割パターンとディレクトリ構成への適用


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

  • Terraformのtfstateファイルを分割する目的と、オススメの分割パターンについて (★)
  • Terraformのリポジトリやリモートバックエンドのディレクトリ構成の設計について

記事のざっくりした内容は、以下のスライドからキャッチアップできちゃいます!



01. はじめに


どうも、Mitchell Hashimoto です。

terraform_icon.png


さて最近の業務で、全プロダクトの技術基盤開発チームに携わっており、チームが使っているTerraform🧑🏻‍🚀のリポジトリをリプレイスする作業を担当しました。

このリポジトリでは単一のtfstateファイルが状態を持ち過ぎている課題を抱えていたため、課題に合った適切な分割パターンでリプレイスしました。

今回は、この時に整理した分割パターン (AWS向け) を記事で解説しました。

もちろん、GoogleCloudやAzureでも読み換えていただければ、同じように適用できます。

知る限りの分割パターンを記載したところ、情報量がエグいことになってしまったため、気になる分割パターンだけ拾って帰っていただけるとハッピーです🙏

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. なぜ tfstate ファイルを分割するのか

分割していない場合

そもそも、なぜtfstateファイルを分割する必要があるのでしょうか。

tfstateファイルを分割しなかったと仮定します。

様々なインフラコンポーネントを単一のtfstateファイルで状態を持つ場合、1回のterraformコマンド全てのコンポーネントの状態を操作できて楽です。

ただし、複数の作業ブランチがある状況だと煩わしいことが起こります。

各作業ブランチでインフラコンポーネントの状態を変更しかけていると、terraformコマンドでtargetオプションが必要になります。

単一のtfstateファイルで管理するコンポーネントが多くなるほど、この問題は顕著になります。

terraform_architecture_same-tfstate

分割している場合

その一方で、tfstateファイルをいい感じに分割したと仮定します。

各作業ブランチでは、まるで暗黙的にtargetオプションがついたように、他の作業ブランチから影響を受けずにterraformコマンドを実行できます。

よって、各tfstateファイルを操作できる管理者は互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate

分割しなくていい場合

運用ルールや開発者人数が理由で作業が衝突せず、targetオプションが必要ない状況であれば、tfstateファイルは分割しなくてもよいでしょう。

tfstateファイルを分割するメリットが少ないです🙅🏻‍

terraform_architecture_tiny-tfstate


03. tfstate ファイルの分割

分割の境界

それでは、tfstateファイルの分割の境界はどのようにして見つければよいのでしょうか。

これを見つけるコツは、できるだけ相互に依存しないインフラリソースの関係 に注目することだと考えています。

ここでいう依存とは、tfstateファイルが他のtfstateファイルの状態を使用することです。

状態をほとんど使用し合わないインフラリソース同士を、異なるtfstateファイルで管理します。

異なるtfstateファイルで管理できる分割パターンについては後述します。


▶ 『依存』という用語について

ソフトウェアアーキテクチャの文脈では、他を使用することを『依存』と表現します。

そのため便宜上、tfstateファイルでも同じ用語で表現することにしました。

@tmknom さんが述べている通り、Terraformをよりよく設計するためには、『ソフトウェアの基礎知識』が必要です👍


状態の依存関係図

依存関係図とは

分割したtfstateファイル間の状態の依存関係を表現した図です。

プロバイダーのアカウントの状態をtfstateファイルで管理していることを想像してみてください。

terraform_architecture_different-tfstate_independent_diagram


似たものとしてterraform graphコマンドによるグラフがありますが、これはインフラリソース間の依存関係図です。

tfstateファイル間で相互に依存関係があるからといって、個別のインフラリソース間で循環参照が起こってしまうというわけではないです。

続いて、依存関係がある場合と無い場合で、どのような依存関係図になるかを紹介していきます。

依存関係の表現

▼ 依存関係の表現記法

tfstateファイル間で状態の依存関係がある場合、これを図で表現すると分割の状況がわかりやすくなります。

『依存』は、---> (波線矢印) で表現することとします。

設定値の参照数が少ないほどよいです。

依存関係がある場合については、後述します。


▶ 『依存』の波線矢印について

ソフトウェアアーキテクチャの文脈では、『依存』を---> (波線矢印) で表現します。

そのため便宜上、tfstateファイルでも同じ記号で表現することにしました👍

▼ 依存関係がない場合

例えば、AWSリソースからなるプロダクトをいくつかのtfstateファイル (foo-tfstatebar-tfstate) に分割したと仮定します。

ここで仮定した状況では、

tfstate ファイル間に依存関係はない

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

tfstateファイル間に依存関係がない状況がベストです。

terraform_architecture_different-tfstate_independent

▼ 依存関係がある場合

同様に分割したと仮定します。

ここで仮定した状況では、

foo-tfstate ➡︎ bar-tfstate の方向に依存している

とします。

そのため、---> (波線矢印) を使用して、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_dependent


04. tfstate ファイルに基づくその他の設計

リポジトリ 🐱 の設計

リポジトリ分割

ここまでで、tfstateファイル分割について簡単に紹介しました。

リポジトリの分割は、tfstateファイル分割に基づいて設計しましょう。

異なるリポジトリtfstateファイルをおいた方がよい場合については、分割パターン で説明しています。

🐱 foo-repository/
├── backend.tf # fooコンポーネントの状態を持つ tfstate ファイルを指定する
...
🐱 bar-repository/
├── backend.tf # barコンポーネントの状態を持つ tfstate ファイルを指定する
...

ディレクトリ 📂 構成

リポジトリ内のディレクトリ構成も、tfstateファイル分割に基づいて設計しましょう。

率直に言うと、Terraformのディレクトリ構成のパターンは無数にあります。

そのため、基準なしにディレクトリ構成を考えると何でもあり になってしまいます。

その一方で、tfstateファイル分割に基づいて設計することにより、明確なディレクトリ構成パターン として抽出可能になります。

🐱 repository/
├── 📂 foo/
│    ├── backend.tf # fooコンポーネントの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 bar/
      ├── backend.tf # barコンポーネントの状態を持つ tfstate ファイルを指定する
      ...


▶ ローカルモジュールのディレクトリ構成の設計について

Terraformには、そのリポジトリ内だけでブロック (例:resourcedata) のセットを使い回すことを目的とした、ローカルモジュールがあります。

今回、これのディレクトリ構成は設計に含めていません。

混同しやすいのですが、tfstateファイル分割に基づくディレクトリ構成とローカルモジュール内のそれは、全く別のテーマとして切り離して考えることができます👍


リモートバックエンド 🪣 の設計

リモートバックエンド分割

本記事では、リモートバックエンドとしてAWS S3バケットを使用することを想定しています。

リモートバックエンドの分割は、tfstateファイル分割に基づいて設計しましょう。

異なるリモートバックエンドにtfstateファイルをおいた方がよい場合については、分割パターン で説明しています。

🪣 foo-bucket/
│
└── terraform.tfstate # fooコンポーネントの状態を持つ
🪣 bar-bucket/
│
└── terraform.tfstate # barコンポーネントの状態を持つ

ディレクトリ構成

リモートバックエンド内のディレクトリ構成も、tfstateファイル分割に基づいて設計しましょう。

🪣 bucket/
├── 📂 foo/
│    └── terraform.tfstate # fooコンポーネントの状態を持つ
│
└── 📂 bar/
      └── terraform.tfstate # barコンポーネントの状態を持つ


05. 状態の依存関係の定義方法

terraform_remote_stateブロックの場合

terraform_remote_stateブロックによる依存

terraform_remote_stateブロックには、以下のメリデメがあります。

アーキテクチャ
特性
メリット ⭕️ デメリット ×
可読性 - terraform_remote_stateブロックに加えてoutputブロックも実装が必要であり、outputブロックは依存先のAWSリソースが一見してわかりにくい。
拡張性 依存先のAWSリソースに関わらず、同じterraform_remote_stateブロックを使い回せる。 -
保守性 - 依存先と依存元の間でTerraformのバージョンに差がありすぎると、tfstateファイル間で互換性がなくなり、terraform_remote_stateブロックの処理が失敗する。

本記事では、 terraform_remote_state ブロックを使用して、状態の依存関係を定義 していきます。

tfstateファイルが他のtfstateファイルに依存する方法として、後述のAWSリソース別dataブロックがあります。

状態の依存関係図

例えば、AWSリソースからなるプロダクトをいくつかのtfstateファイル (foo-tfstatebar-tfstate) に分割したと仮定します。

ここで仮定した状況では、bar-tfstateファイルはVPCの状態を持っており、

foo-tfstate ファイルは bar-tfstate ファイルに依存している

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_dependent_terraform-remote-state

リポジトリディレクトリ構成

tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

ディレクトリの設計方法は、分割パターン で説明しています。

🐱 repository/
├── 📂 foo/
│    ├── backend.tf # fooコンポーネントの状態を持つ tfstate ファイルを指定する
│    ├── remote_state.tf # terraform_remote_stateブロックを使用し、bar-tfstate ファイルに依存する
│    ├── provider.tf
│    ...
│
└── 📂 bar/
      ├── backend.tf # barコンポーネントの状態を持つ tfstate ファイルを指定する
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── provider.tf
      ...

foo-tfstateファイルがbar-tfstateファイルに依存するために必要な実装は、以下の通りになります。

resource "example" "foo" {

  # fooリソースは、bar-tfstate ファイルのVPCに依存する
  vpc_id = data.terraform_remote_state.bar.outputs.bar_vpc_id

  ...
}

data "terraform_remote_state" "bar" {

 backend = "s3"

  config = {
    bucket = "tfstate"
    key    = "bar/terraform.tfstate"
    region = "ap-northeast-1"
  }
}
# VPCの状態は、bar-tfstate ファイルで持つ
output "bar_vpc_id" {
  value = aws_vpc.bar.id
}

resource "aws_vpc" "bar" {
  ...
}

リモートバックエンドのディレクトリ構成

tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

🪣 bucket/
├── 📂 foo
│    └── terraform.tfstate # fooコンポーネントの状態を持つ
│
└── 📂 bar
      └── terraform.tfstate # barコンポーネントの状態を持つ


AWSリソース別dataブロックの場合

AWSリソース別dataブロックによる依存

AWSリソース別dataブロックには、以下のメリデメがあります。

アーキテクチャ
特性
メリット ⭕️ デメリット ×
可読性 依存先のAWSリソースがわかりやすい。 -
拡張性 - 依存先のAWSリソース別dataブロックが必要である。
保守性 依存先と依存元の間でTerraformのバージョンに差があっても、tfstateファイル間で直接的に依存するわけではないため、バージョン差の影響を受けない。 -

今回は使用しませんが、依存関係の他の定義方法として、AWSリソース別dataブロックがあります。

これは、tfstateファイルが自身以外 (例:コンソール画面、他のtfstateファイル) で作成されたAWSリソースの状態に依存するために使用できます。

terraform_remote_stateブロックとは異なり、直接的にはtfstateファイルに依存しません。

AWSリソース別dataブロックの場合は、実際のAWSリソースの状態に依存することにより、間接的にAWSリソースのtfstateファイルに依存することになります。

状態の依存関係図

例えば、AWSリソース別dataブロックも同様にして、AWSリソースからなるプロダクトをいくつかのtfstateファイル (foo-tfstatebar-tfstate) に分割したと仮定します。

ここで仮定した状況では、bar-tfstateファイルはVPCの状態を持っており、

foo-tfstate ファイルは bar-tfstate ファイルに依存している

とします。

想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_dependent_data

リポジトリディレクトリ構成

ディレクトリ構成は、tfstateファイル分割に基づいて、以下の通りになります。

🐱 repository/
├── 📂 foo/
│    ├── backend.tf # fooコンポーネントの状態を持つ tfstate ファイルを指定する
│    ├── data.tf # dataブロックを使用し、bar-tfstate ファイルに依存する
│    ├── provider.tf
│    ...
│
└── 📂 bar/
      ├── backend.tf # barコンポーネントの状態を持つ tfstate ファイルを指定する
      ├── provider.tf
      ...

foo-tfstateファイルがbar-tfstateファイルに依存するために必要な実装は、以下の通りになります。

# fooリソースの状態は、foo-tfstate ファイルで持つ
resource "example" "foo" {

  # fooリソースは、bar-tfstate ファイルのVPCに依存する
  vpc_id     = data.aws_vpc.bar.id
}

# VPCの状態は、bar-tfstate ファイルで持つ
data "aws_vpc" "bar" {

  filter {
    name   = "tag:Name"
    values = ["<bar-tfstateが持つVPCの名前>"]
  }
}

リモートバックエンドのディレクトリ構成

tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

🪣 bucket/
├── 📂 foo
│    └── terraform.tfstate # fooコンポーネントの状態を持つ
│
└── 📂 bar
      └── terraform.tfstate # barコンポーネントの状態を持つ


06. tfstate ファイルの分割パターン

オススメな設計の一覧

前述の通り、tfstateファイルの分割の境界は、『他の状態にできるだけ依存しないリソースの関係』から見つけることができます。

分割しすぎると terraform_remote_stateブロック地獄 になるため、細かすぎず粗すぎない適切な境界を見つけていきましょう。

今回は、私が考える分割パターンをいくつか紹介します。

全てが実用的なパターンというわけでないため、オススメするものを ★ としています。

推奨

任意
tfstate
分割パターン
大分類
tfstate
分割パターン
小分類
オススメ 対応する
リポジトリ構成 🐱
対応する
リモートバックエンド構成 🪣
推奨 上層 プロバイダーのアカウント別 ★★★ リポジトリ自体
または上層ディレクト
リモートバックエンド自体
または上層ディレクト
下層実行環境別 ★★★ 下層ディレクト 下層ディレクト
任意 中間層 運用チーム責務範囲別 ★★ 中間層ディレクト 中間層ディレクト
プロダクトのサブコンポーネント ★★
運用チーム責務範囲別
×
プロダクトのサブコンポーネント
(組み合わせ)
同じテナント内のプロダクト別
AWSリソースの種類グループ別
AWSリソースの状態の変更頻度グループ別


大分類 (上層/下層/中間層) とディレクトリ構成の関係

リポジトリの場合

記事内のここ で、リポジトリ内のディレクトリ構成はtfstateファイル分割に基づいて設計するべき、という説明をしました。

tfstateファイルの分割パターンは、上層/下層/中間層 の層に大別できます。

これらの層は、以下の通りリポジトリ自体・ディレクトリ構成の設計方法に影響します。

# リポジトリ自体を分割する場合
🐱 上層/
├── 📂 中間層/
│    ├── 📂 下層/
│    │    ├── backend.tfvars # 分割された tfstate ファイルを指定する
│    │    ...
│    │
...
# リポジトリ内のディレクトリを分割する場合
🐱 リポジトリ/
├── 📂 上層/
│    ├── 📂 中間層/
│    │    ├── 📂 下層/
│    │    │    ├── backend.tfvars # 分割された tfstate ファイルを指定する
│    │    │    ...
│    │    │
...

リモートバックエンドの場合

記事内のここ で、リモートバックエンドのディレクトリ構成についても言及しました。

これらの層は、以下の通りリモートバックエンド自体・ディレクトリ構成の設計方法に影響します。

# リモートバックエンド自体を分割する場合
🪣 上層/
├── 📂 中間層/
│    ├── 📂 下層/
│    │    └── terraform.tfstate # 分割された状態を持つ
│    │
│    │
...
# リモートバックエンド内のディレクトリを分割する場合
🪣 bucket/
├── 📂 上層/
│    ├── 📂 中間層/
│    │    ├── 📂 下層/
│    │    │    └── terraform.tfstate # 分割された状態を持つ
│    │    │
│    │    │
...


07. 上層の分割 (推奨)

上層の分割について

上層の分割は 推奨 です。

Terraformに携わる管理者の数が少なくても採用した方がよいです。

tfstateファイルをパターンに応じて分割し、これに基づいてディレクトリ・リモートバックエンドも設計しましょう。


プロバイダーのアカウント別 - ★★★

この分割方法について

上層分割の中でも、基本的な方法の1つです。

プロバイダーのアカウント別にtfstateファイルを分割し、上層もこれに基づいて設計します。

この分割方法により、各プロバイダーの管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_provider-accounts_branch


▶ おすすめ度について

一部のプロバイダーは、分割できず、他のプロバイダーと同じtfstateファイルで状態を管理せざるを得ない場合があります。

例えば、Kubernetesのプロバイダーは、EKSと同じtfstateファイルで管理した方がよいです👍

【プロバイダーアカウント別】状態の依存関係図

例えば、以下のプロバイダーを使用したい状況と仮定します。

  • 主要プロバイダー (AWS)
  • アプリ/インフラ監視プロバイダー (Datadog)
  • ジョブ監視プロバイダー (Healthchecks)
  • インシデント管理プロバイダー (PagerDuty)

ここで仮定した状況では、

各プロバイダーの tfstate ファイル間で状態が相互に依存している

とします。

AWSリソース間の相互依存ではないため、循環参照は起こりません。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_provider-accounts

【プロバイダーアカウント別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

プロバイダーアカウント別に分割したtfstateファイルを、異なるリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🐱 aws-repository/
├── backend.tf # AWSの状態を持つ tfstate ファイルを指定する
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
...
🐱 datadog-repository/
├── backend.tf # Datadogの状態を持つ tfstate ファイルを指定する
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
...
🐱 healthchecks-repository/
├── backend.tf # Healthchecksの状態を持つ tfstate ファイルを指定する
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
...
🐱 pagerduty-repository/
├── backend.tf # PagerDutyの状態を持つ tfstate ファイルを指定する
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
...

▼ 同じリポジトリの場合

プロバイダーアカウント別に分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🐱 repository/
├── 📂 aws/
│    ├── backend.tf # AWSの状態を持つ tfstate ファイルを指定する
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ...
│
├── 📂 datadog/
│    ├── backend.tf # Datadogの状態を持つ tfstate ファイルを指定する
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ...
│
├── 📂 healthchecks/
│    ├── backend.tf # Healthchecksの状態を持つ tfstate ファイルを指定する
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ...
│
└── 📂 pagerduty/
      ├── backend.tf # PagerDutyの状態を持つ tfstate ファイルを指定する
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── remote_state.tf # terraform_remote_state ブロックを使用する
      ├── provider.tf
      ...


【プロバイダーアカウント別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

プロバイダーアカウント別に分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🪣 aws-bucket/
│
└── terraform.tfstate # AWSの状態を持つ
🪣 datadog-bucket/
│
└── terraform.tfstate # Datadogの状態を持つ
🪣 healthchecks-bucket/
│
└── terraform.tfstate # Healthchecksの状態を持つ
🪣 pagerduty-bucket/
│
└── terraform.tfstate # PagerDutyの状態を持つ

▼ 同じリモートバックエンドの場合

プロバイダーアカウント別に分割したtfstateファイルを、同じリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🪣 bucket/
├── 📂 aws
│    └── terraform.tfstate # AWSの状態を持つ
│
├── 📂 datadog
│    └── terraform.tfstate # Datadogの状態を持つ
│
├── 📂 healthchecks
│    └── terraform.tfstate # Healthchecksの状態を持つ
│
└── 📂 pagerduty
      └── terraform.tfstate # PagerDutyの状態を持つ


08. 下層の分割 (推奨)

下層の分割について

下層の分割は 推奨 です。

Terraformに携わる管理者の数が少なくても採用した方がよいです。

tfstateファイルをパターンに応じて分割し、これに基づいてディレクトリ・リモートバックエンドも設計しましょう。


実行環境別 - ★★★

この分割方法について

下層分割の中でも、基本的な方法の1つです。

実行環境別にtfstateファイルを分割し、下層もこれに基づいて設計します。

この分割方法により、各実行環境の管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_environments_branch


▶ おすすめ度について

この分割方法は、様々なブログ・著名な書籍で紹介されています👀

私を含め、ほとんどの方に採用経験があるのではないでしょうか。

【実行環境別】状態の依存関係図

例えば、以下の実行環境を構築したい状況と仮定します。

  • Tes環境 (検証環境)
  • Stg環境 (ユーザー受け入れ環境)
  • Prd環境 (本番環境)

かつ、以下のプロバイダーを使用したい状況と仮定します。

  • 主要プロバイダー (AWS)
  • アプリ/インフラ監視プロバイダー (Datadog)
  • ジョブ監視プロバイダー (Healthchecks)
  • インシデント管理プロバイダー (PagerDuty)

ここで仮定した状況では、

各実行環境の tfstate ファイルは他の実行環境には依存していない

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_environments

【実行環境別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

プロバイダーアカウント別にtfstateファイルを分割することは推奨としているため、その上でディレクトリ構成を考えます。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🐱 aws-repository/
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
├── 📂 tes/ # Tes環境
│    ├── backend.tfvars # Tes環境のAWSリソースの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/ # Stg環境
└── 📂 prd/ # Prd環境
🐱 datadog-repository/
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
├── 📂 tes/
│    ├── backend.tfvars # Tes環境のDatadogの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/
└── 📂 prd/
🐱 healthchecks-repository/
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
├── 📂 tes/
│    ├── backend.tfvars # HealthchecsのTes環境の状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/
└── 📂 prd/
🐱 pagerduty-repository/
├── output.tf # 他の tfstate ファイルから依存される
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── provider.tf
├── 📂 tes/
│    ├── backend.tfvars # Tes環境のPagerDutyの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/
└── 📂 prd/

▼ 同じリポジトリの場合

プロバイダーアカウント別にtfstateファイルを分割することは推奨としているため、その上でディレクトリ構成を考えます。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🐱 repository/
├── 📂 aws/
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # Tes環境のAWSリソースの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    └── 📂 prd/ # Prd環境
│
├── 📂 datadog/
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ├── 📂 tes/
│    │    ├── backend.tfvars # Tes環境のDatadogの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/
│    └── 📂 prd/
│
├── 📂 healthchecks/
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ├── 📂 tes/
│    │    ├── backend.tfvars # Tes環境のHealthchecksの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/
│    └── 📂 prd/
│
└── 📂 pagerduty/
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── remote_state.tf # terraform_remote_state ブロックを使用する
      ├── provider.tf
      ├── 📂 tes/
      │    ├── backend.tfvars # Tes環境のPagerDutyの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/
      └── 📂 prd/

【実行環境別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

実行環境別に分割したtfstateファイルを、異なるリモートバックエンドで管理します。

tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

例えば、前述の依存関係図の状況と仮定します。

🪣 tes-aws-bucket/
│
└── terraform.tfstate # Tes環境のAWSリソースの状態を持つ
🪣 tes-datadog-bucket/
│
└── terraform.tfstate # Tes環境のDatadogの状態を持つ
🪣 tes-healthchecks-bucket/
│
└── terraform.tfstate # Tes環境のHealthchecksの状態を持つ
🪣 tes-pagerduty-bucket/
│
└── terraform.tfstate # Tes環境のPagerDutyの状態を持つ

▼ 同じリモートバックエンド x AWSアカウント別に異なる実行環境 の場合

プロバイダーアカウント別に分割したtfstateファイルを、同じリモートバックエンドで管理します。

また、AWSアカウント別に異なる実行環境を作成していると仮定します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 aws/
│    └── terraform.tfstate # Tes環境のAWSリソースの状態を持つ
│
├── 📂 datadog/
│    └── terraform.tfstate # Tes環境のDatadogの状態を持つ
│
├── 📂 healthchecks/
│    └── terraform.tfstate # Tes環境のHealthchecksの状態を持つ
│
└── 📂 pagerduty/
      └── terraform.tfstate # Tes環境のPagerDutyの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...

▼ 同じリモートバックエンド x 単一のAWSアカウント内に全ての実行環境 の場合

プロバイダーアカウント別に分割したtfstateファイルを、同じリモートバックエンドで管理します。

また、単一のAWSアカウント内に全実行環境を作成しているとします。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🪣 bucket/
├── 📂 aws/
│    ├── 📂 tes/ # Tes環境
│    │    └── terraform.tfstate # Tes環境のAWSリソースの状態を持つ
│    │
│    ├── 📂 stg/ # Stg環境
│    └── 📂 prd/ # Prd環境
│
├── 📂 datadog/
│    ├── 📂 tes/
│    │    └── terraform.tfstate # Tes環境のDatadogの状態を持つ
│    │
│    ├── 📂 stg/
│    └── 📂 prd/
│
├── 📂 healthchecks/
│    ├── 📂 tes/
│    │    └── terraform.tfstate # Tes環境のHealthchecksの状態を持つ
│    │
│    ├── 📂 stg/
│    └── 📂 prd/
│
└── 📂 pagerduty/
      ├── 📂 tes/
      │    └── terraform.tfstate # Tes環境のPagerDutyの状態を持つ
      │
      ├── 📂 stg/
      └── 📂 prd/


09. 中間層の分割 (任意)

中間層の分割について

中間層の分割は 任意 です。

Terraformに携わる管理者が多くなるほど、効力を発揮します。


運用チーム責務範囲別 - ★★

この分割方法について

運用チーム (例:アプリチーム、インフラチーム) のAWSリソースの責務範囲別でtfstateファイルを分割し、中間層もこれに基づいて設計します。

この分割方法により、各運用チームが互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_teams_branch


▶ おすすめ度について

この分割方法は、AWSドキュメント・著名な書籍で紹介されています👀

Terraformに携わるチームが複数ある非常に大規模なプロダクトほど効力を発揮します。

実際に私も現在進行形で採用しており、非常に実用的と考えています。

【チーム別】状態の依存関係図

例えば、以下の運用チームに分割した状況と仮定します。

  • frontendチーム (アプリのフロントエンド領域担当)
  • backendチーム (アプリのバックエンド領域担当)
  • sreチーム (インフラ領域担当)

ここで仮定した状況では、

各チームが管理する tfstate ファイル間で状態が相互に依存している

とします。

AWSリソース間の相互依存ではないため、循環参照は起こりません。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_teams

【チーム別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

この場合では、運用チーム責務範囲別に分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

🐱 aws-frontend-team-repository/ # frontendチーム
├── output.tf # 他の tfstate ファイルから依存される
├── provider.tf
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── cloudfront.tf
├── s3.tf
├── 📂 tes/ # Tes環境
│    ├── backend.tfvars # frontendチームの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/ # Stg環境
│    ├── backend.tfvars # frontendチームの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 prd/ # Prd環境
      ├── backend.tfvars # frontendチームの状態を持つ tfstate ファイルを指定する
      ...
🐱 aws-backend-team-repository/ # backendチーム
├── output.tf # 他の tfstate ファイルから依存される
├── provider.tf
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── elasticache.tf
├── ses.tf
├── sns.tf
├── rds.tf
├── 📂 tes
│    ├── backend.tfvars # backendチームの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg
│    ├── backend.tfvars # backendチームの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 prd
      ├── backend.tfvars # backendチームの状態を持つ tfstate ファイルを指定する
       ...
🐱 aws-sre-team-repository/ # sreチーム
├── output.tf # 他の tfstate ファイルから依存される
├── provider.tf
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── alb.tf
├── cloudwatch.tf
├── ec2.tf
├── ecs.tf
├── eks.tf
├── iam.tf
├── vpc.tf
├── 📂 tes
│    ├── backend.tfvars # sreチームの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg
│    ├── backend.tfvars # sreチームの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 prd
      ├── backend.tfvars # sreチームの状態を持つ tfstate ファイルを指定する
      ...

▼ 同じリポジトリの場合

この場合では、運用チーム責務範囲別に分割したtfstateファイルを、異なるリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

🐱 aws-repository/
├── 📂 frontend-team # frontendチーム
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── cloudfront.tf
│    ├── s3.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # frontendチームの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # frontendチームの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # frontendチームの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 backend-team # backendチーム
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── elasticache.tf
│    ├── ses.tf
│    ├── sns.tf
│    ├── rds.tf
│    ├── 📂 tes
│    │    ├── backend.tfvars # backendチームの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg
│    │    ├── backend.tfvars # backendチームの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd
│          ├── backend.tfvars # backendチームの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 sre-team # sreチーム
      ├── provider.tf
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── remote_state.tf # terraform_remote_state ブロックを使用する
      ├── alb.tf
      ├── cloudwatch.tf
      ├── ec2.tf
      ├── ecs.tf
      ├── eks.tf
      ├── iam.tf
      ├── vpc.tf
      ├── 📂 tes
      │    ├── backend.tfvars # sreチームの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg
      │    ├── backend.tfvars # sreチームの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd
           ├── backend.tfvars # sreチームの状態を持つ tfstate ファイルを指定する
           ...

【チーム別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

運用チーム責務範囲別の場合、異なるリモートバックエンドで管理するとバックエンドが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリモートバックエンドの場合

この場合では、プロバイダーアカウント別に分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 frontend-team
│    └── terraform.tfstate # frontendチームの状態を持つ
│
├── 📂 backend-team
│    └── terraform.tfstate # backendチームの状態を持つ
│
└── 📂 sre-team
      └── terraform.tfstate # sreチームの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...


プロダクトのサブコンポーネント別 - ★★

この分割方法について

プロダクトのサブコンポーネント (例:アプリ、ネットワーク、認証/認可、監視、など) 別でtfstateファイルを分割し、中間層もこれに基づいて設計します。

この分割方法により、サブコンポーネントの管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_product_sub-components_branch


▶ おすすめ度について

サブコンポーネントは、分けようと思えばいくらでも細分化できてしまいます。

細分化した数だけterraform_remote_stateブロック地獄になっていくため、適切な数 (35個くらい) にしておくように注意が必要です。

この分割方法は、後述のAWSリソースの種類グループとごっちゃになってしまう場合があるため、プロダクトのサブコンポーネントとして意識的に分割させる必要があります👍

【サブコンポーネント別】状態の依存関係図

例えば、以下のサブコンポーネントに分割した状況と仮定します。

  • application (Web3層系)
  • auth (認証/認可系)
  • monitor (監視系)
  • network (ネットワーク系)

ここで仮定した状況では、

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_product_sub-components

【サブコンポーネント別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

プロダクトのサブコンポーネント別の分割パターンの場合、異なるリポジトリで管理するとリポジトリが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリポジトリの場合

この場合では、プロダクトのサブコンポーネント別に分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

🐱 aws-repository/
├── 📂 application/
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── provider.tf
│    ├── alb.tf
│    ├── cloudfront.tf
│    ├── ec2.tf
│    ├── ecs.tf
│    ├── eks.tf
│    ├── ses.tf
│    ├── sns.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # applicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # applicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # applicationコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 auth/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── iam.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # authコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # authコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # authコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 monitor/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── cloudwatch.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # monitorコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # monitorコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # monitorコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 network
      ├── provider.tf
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── route53.tf
      ├── vpc.tf
      ├── 📂 tes/ # Tes環境
      │    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/ # Stg環境
      │    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd/ # Prd環境
           ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
           ...

【サブコンポーネント別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

プロダクトのサブコンポーネント別の分割パターンの場合、異なるリモートバックエンドで管理するとバックエンドが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリモートバックエンドの場合

この場合では、プロダクトのサブコンポーネント別に分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 application
│    └── terraform.tfstate # applicationコンポーネントの状態を持つ
│
├── 📂 auth
│    └── terraform.tfstate # authコンポーネントの状態を持つ
│
├── 📂 monitor
│    └── terraform.tfstate # monitorコンポーネントの状態を持つ
│
└── 📂 network
      └── terraform.tfstate # networkコンポーネントの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...


運用チーム責務範囲別 × プロダクトサブコンポーネント別 - ★

この分割方法について

運用チーム責務範囲別とプロダクトサブコンポーネント別を組み合わせてtfstateファイルを分割し、中間層もこれに基づいて設計します。

この分割方法により、各運用チーム内のサブコンポーネントの管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_teams_resource-type_branch


▶ おすすめ度について

この分割方法は、Terraformに携わるチームが複数あり、かつチームの人数も多い、非常に大規模なプロダクトほど効力を発揮します。

実際に私も現在進行形で採用しており、非常に実用的と考えています👍

【チーム別 × サブコンポーネント別】状態の依存関係図

以下の運用チームに分割した状況と仮定します。

また、各運用チームでTerraformを変更できる管理者が相当数するため、プロダクトのサブコンポーネント別にも分割したとします。

  • frontendチーム
    • application
    • monitor
  • backendチーム
    • application
    • monitor
  • sreチーム
    • application
    • auth
    • monitor
    • network

ここで仮定した状況では、

  • 各プロダクトのtfstateファイルの依存は一方向
  • 最終的に、sreチームの管理する tfstate ファイルに依存している

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_teams_resource-type

【チーム別 × サブコンポーネント別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

この場合では、運用チーム責務範囲別とプロダクトサブコンポーネント別を組み合わせて分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

🐱 aws-frontend-team-repository/
├── 📂 application/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── cloudfront.tf
│    ├── ses.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # frontendチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # frontendチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # frontendチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 monitor/
      ├── provider.tf
      ├── remote_state.tf # terraform_remote_state ブロックを使用する
      ├── cloudwatch.tf
      ├── 📂 tes/ # Tes環境
      │    ├── backend.tfvars # frontendチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/ # Stg環境
      │    ├── backend.tfvars # frontendチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd/ # Prd環境
            ├── backend.tfvars # frontendチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
            ...
🐱 aws-backend-team-repository/
├── 📂 application/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── api_gateway.tf
│    ├── elasticache.tf
│    ├── rds.tf
│    ├── ses.tf
│    ├── sns.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # backendチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # backendチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # backendチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 monitor/
      ├── provider.tf
      ├── remote_state.tf # terraform_remote_state ブロックを使用する
      ├── cloudwatch.tf
      ├── 📂 tes/ # Tes環境
      │    ├── backend.tfvars # backendチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/ # Stg環境
      │    ├── backend.tfvars # backendチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd/ # Prd環境
            ├── backend.tfvars # backendチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
            ...
🐱 aws-sre-team-repository/
├── 📂 application/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── alb.tf
│    ├── ec2.tf
│    ├── ecs.tf
│    ├── eks.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # sreチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # sreチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # sreチームが管理するapplicationコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 auth/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── iam.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # sreチームが管理するauthコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # sreチームが管理するauthコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # sreチームが管理するauthコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 monitor/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── cloudwatch.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # sreチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # sreチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # sreチームが管理するmonitorコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 network
      ├── provider.tf
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── route53.tf
      ├── vpc.tf
      ├── 📂 tes/ # Tes環境
      │    ├── backend.tfvars # sreチームが管理するnetworkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/ # Stg環境
      │    ├── backend.tfvars # sreチームが管理するnetworkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd/ # Prd環境
            ├── backend.tfvars # sreチームが管理するnetworkコンポーネントの状態を持つ tfstate ファイルを指定する
            ...

▼ 同じリポジトリの場合

運用チーム責務範囲別とプロダクトサブコンポーネント別を組み合わせる分割パターンの場合、同じリポジトリで管理するとリポジトリが巨大になってしまいます。

そのため、これはお勧めしません。

【チーム別 × サブコンポーネント別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

運用チーム責務範囲別とプロダクトサブコンポーネント別を組み合わせる分割パターンの場合、異なるリモートバックエンドで管理するとバックエンドが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリモートバックエンドの場合

この場合では、運用チーム責務範囲別とプロダクトサブコンポーネント別を組み合わせて分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 frontend-team
│    ├── 📂 application
│    │    └── terraform.tfstate # frontendチームが管理するapplicationコンポーネントの状態を持つ
│    │
│    └── 📂 monitor
│         └── terraform.tfstate # frontendチームが管理するmonitorコンポーネントの状態を持つ
│
├── 📂 backend-team
│    ├── 📂 application
│    │    └── terraform.tfstate # backendチームが管理するapplicationコンポーネントの状態を持つ
│    │
│    └── 📂 monitor
│          └── terraform.tfstate # backendチームが管理するmonitorコンポーネントの状態を持つ
│
└── 📂 sre-team
      ├── 📂 application
      │    └── terraform.tfstate # sreチームが管理するapplicationコンポーネントの状態を持つ
      │
      ├── 📂 auth
      │    └── terraform.tfstate # sreチームが管理するauthコンポーネントの状態を持つ
      │
      ├── 📂 monitor
      │    └── terraform.tfstate # sreチームが管理するmonitorコンポーネントの状態を持つ
      │
      └── 📂 network
            └── terraform.tfstate # sreチームが管理するnetworkコンポーネントの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...


同じテナント内のプロダクト別

この分割方法について

同じテナント (例:同じAWSアカウントの同じVPC) 内に複数の小さなプロダクトがある場合、プロダクト別でtfstateファイルを分割し、中間層もこれに基づいて設計します。

ここでいうプロダクトは、アプリを動かすプラットフォーム (例:EKS、ECS、AppRunner、EC2) とそれを取り巻くAWSリソースを指しています。

この分割方法により、各プロダクトの管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_products_branch


▶ おすすめ度について

AWSの設計プラクティスとしてプロダクトごとにVPCを分けた方がよいため、この分割方法を採用することは少ないかもしれません。

ただ現実として、各プロダクトの使用するIPアドレス数が少なく、またプロダクト別にVPCを分割するのが煩雑という現場はあります😭

【同じテナント内のプロダクト】状態の依存関係図

例えば、以下のプロダクトに分割した状況と仮定します。

ここで仮定した状況では、

  • 各プロダクトの tfstate ファイルの依存は一方向
  • 最終的に、共有networkコンポーネントの tfstate ファイルに依存している

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_products

【同じテナント内のプロダクト】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

この場合では、同じテナント内のプロダクトに分割したtfstateファイルを、異なるリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

# fooプロダクトの tfstate ファイルのリポジトリ
🐱 aws-foo-product-repository/
├── provider.tf
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── 📂 tes/ # Tes環境
│    ├── backend.tfvars # fooプロダクトの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/ # Stg環境
│    ├── backend.tfvars # fooプロダクトの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 prd/ # Prd環境
      ├── backend.tfvars # fooプロダクトの状態を持つ tfstate ファイルを指定する
      ...
# barプロダクトの tfstate ファイルのリポジトリ
🐱 aws-bar-product-repository/
├── provider.tf
├── remote_state.tf # terraform_remote_state ブロックを使用する
├── 📂 tes/ # Tes環境
│    ├── backend.tfvars # barプロダクトの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/ # Stg環境
│    ├── backend.tfvars # barプロダクトの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 prd/ # Prd環境
      ├── backend.tfvars # barプロダクトの状態を持つ tfstate ファイルを指定する
      ...
# 共有networkコンポーネントの tfstate ファイルのリポジトリ
🐱 aws-network-repository/
├── output.tf # 他の tfstate ファイルから依存される
├── provider.tf
├── route53.tf
├── vpc.tf
├── 📂 tes/ # Tes環境
│    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
│    ...
│
├── 📂 stg/ # Stg環境
│    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
│    ...
│
└── 📂 prd/ # Prd環境
      ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      ...

▼ 同じリポジトリの場合

この場合では、同じテナント内のプロダクトに分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

🐱 aws-repository/
├── 📂 foo-product/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # fooプロダクトの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # fooプロダクトの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # fooプロダクトの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 bar-product/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # barプロダクトの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # barプロダクトの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # barプロダクトの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 network
      ├── provider.tf
      ├── output.tf # 他の tfstate ファイルから依存される
      ├── route53.tf
      ├── vpc.tf
      ├── 📂 tes/ # Tes環境
      │    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/ # Stg環境
      │    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd/ # Prd環境
           ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
           ...

【同じテナント内のプロダクト】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

同じテナント内のプロダクトの場合、異なるリモートバックエンドで管理するとバックエンドが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリモートバックエンドの場合

この場合では、同じテナント内のプロダクトに分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

前述の依存関係図の状況と仮定します。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 foo-product
│    └── terraform.tfstate # fooプロダクトの状態を持つ
│
├── 📂 bar-product
│    └── terraform.tfstate # barプロダクトの状態を持つ
│
└── 📂 network
      └── terraform.tfstate # networkコンポーネントの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...


AWSリソースの種類グループ別

この分割方法について

AWSリソースの種類グループ別でtfstateファイルを分割し、中間層もこれに基づいて設計します。

この分割方法により、各AWSリソースの種類グループも管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_resource-type_branch


▶ おすすめ度について

AWSリソースの種類グループは、分けようと思えばいくらでも細分化できてしまいます。

細分化した数だけterraform_remote_stateブロック地獄になっていくため、適切な数 (35個くらい) にしておくように注意が必要です。

特にこの分割方法は、グループ数がどんどん増えていく可能性があります😇

【種類グループ別】状態の依存関係図

例えば、以下の種類グループに分割した状況と仮定します。

  • application (Webサーバー、Appサーバー系)
  • auth (認証/認可系)
  • datastore (DBサーバー系)
  • cicd (CI/CD系)
  • monitor (監視系)
  • network (ネットワーク系)

ここで仮定した状況では、

  • 各プロダクトのtfstateファイルの依存は一方向
  • 最終的に、networkグループやauthグループの tfstate ファイルに依存している

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_resource-type

【種類グループ別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

AWSリソースの種類グループ別の分割パターンの場合、異なるリポジトリで管理するとリポジトリが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリポジトリの場合

この場合では、AWSリソースの種類グループ別に分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

🐱 aws-repository/
├── 📂 application/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── alb.tf
│    ├── api_gateway.tf
│    ├── cloudfront.tf
│    ├── ec2.tf
│    ├── ecs.tf
│    ├── eks.tf
│    ├── ses.tf
│    ├── sns.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # applicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # applicationコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # applicationコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 auth/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── iam.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # authコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # authコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # authコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 cicd/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── codebuild.tf
│    ├── codecommit.tf
│    ├── codedeploy.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # cicdコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # cicdコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # cicdコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 datastore/
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── elasticache.tf
│    ├── rds.tf
│    ├── s3.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # datastoreコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # datastoreコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # datastoreコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 monitor/
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── cloudwatch.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # monitorコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # monitorコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # monitorコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 network
      ├── provider.tf
      ├── output.tf # 他の tfstate ファイルから参照できるように、outputブロックを定義する
      ├── route53.tf
      ├── vpc.tf
      ├── 📂 tes/ # Tes環境
      │    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg/ # Stg環境
      │    ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd/ # Prd環境
           ├── backend.tfvars # networkコンポーネントの状態を持つ tfstate ファイルを指定する
           ...

【種類グループ別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

AWSリソースの種類グループ別の分割パターンの場合、異なるリモートバックエンドで管理するとバックエンドが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリモートバックエンドの場合

この場合では、AWSリソースの種類グループ別に分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 application
│    └── terraform.tfstate # applicationコンポーネントの状態を持つ
│
├── 📂 auth
│    └── terraform.tfstate # authコンポーネントの状態を持つ
│
├── 📂 cicd
│    └── terraform.tfstate # cicdコンポーネントの状態を持つ
│
├── 📂 datastore
│    └── terraform.tfstate # datastoreコンポーネントの状態を持つ
│
├── 📂 monitor
│    └── terraform.tfstate # monitorコンポーネントの状態を持つ
│
└── 📂 network
      └── terraform.tfstate # networkコンポーネントの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...


AWSリソースの状態の変更頻度グループ別

この分割方法について

AWSリソースの状態の変更頻度グループ別でtfstateファイルを分割し、中間層もこれに基づいて設計します。

この分割方法により、各変更頻度グループの管理者が互いに影響を受けずに、terraformコマンドの結果を得られるようになります。

terraform_architecture_different-tfstate_update-frequence_branch


▶ おすすめ度について

変更頻度の境界が曖昧なため、この分割方法は個人的にお勧めしません。

【変更頻度グループ別】状態の依存関係図

例えば、以下の変更頻度グループに分割した状況と仮定します。

  • 変更高頻度グループ
  • 変更中頻度グループ
  • 変更低頻度グループ

ここで仮定した状況では、

  • 各プロダクトのtfstateファイルの依存は一方向
  • 最終的に、変更低頻度グループの tfstate ファイルに依存している

とします。

そのため、想定される状態の依存関係図は以下の通りになります。

なお、依存方向は状況によって異なることをご容赦ください。

terraform_architecture_different-tfstate_update-frequence

【変更頻度グループ別】リポジトリディレクトリ構成

▼ 異なるリポジトリの場合

AWSリソースの変更頻度グループ別の分割パターンの場合、異なるリポジトリで管理するとリポジトリが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリポジトリの場合

この場合では、AWSリソースの変更頻度グループ別に分割したtfstateファイルを、同じリポジトリで管理します。

例えば、tfstateファイル分割に基づいて、リポジトリディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

🐱 aws-repository/
├── 📂 high-freq # 高頻度変更グループ
│    ├── provider.tf
│    ├── remote_state.tf # terraform_remote_state ブロックを使用する
│    ├── api_gateway.tf
│    ├── cloudfront.tf
│    ├── cloudwatch.tf
│    ├── ec2.tf
│    ├── ecs.tf
│    ├── eks.tf
│    ├── iam.tf
│    ├── 📂 tes/ # Tes環境
│    │    ├── backend.tfvars # high-freqコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg/ # Stg環境
│    │    ├── backend.tfvars # high-freqコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd/ # Prd環境
│          ├── backend.tfvars # high-freqコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
├── 📂 low-freq # 低頻度変更グループ
│    ├── provider.tf
│    ├── output.tf # 他の tfstate ファイルから依存される
│    ├── route53.tf
│    ├── vpc.tf
│    ├── 📂 tes
│    │    ├── backend.tfvars # low-freqコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    ├── 📂 stg
│    │    ├── backend.tfvars # low-freqコンポーネントの状態を持つ tfstate ファイルを指定する
│    │    ...
│    │
│    └── 📂 prd
│          ├── backend.tfvars # low-freqコンポーネントの状態を持つ tfstate ファイルを指定する
│          ...
│
└── 📂 middle-freq # 中頻度変更グループ (高頻度とも低頻度とも言えないリソース)
      ├── provider.tf
      ├── remote_state.tf # terraform_remote_state ブロックを使用する
      ├── elasticache.tf
      ├── rds.tf
      ├── s3.tf
      ├── ses.tf
      ├── 📂 tes
      │    ├── backend.tfvars # middle-freqコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      ├── 📂 stg
      │    ├── backend.tfvars # middle-freqコンポーネントの状態を持つ tfstate ファイルを指定する
      │    ...
      │
      └── 📂 prd
           ├── backend.tfvars # middle-freqコンポーネントの状態を持つ tfstate ファイルを指定する
           ...

【変更頻度グループ別】リモートバックエンドのディレクトリ構成

▼ 異なるリモートバックエンドの場合

AWSリソースの変更頻度グループ別の分割パターンの場合、異なるリモートバックエンドで管理するとバックエンドが増え過ぎてしまいます。

そのため、これはお勧めしません。

▼ 同じリモートバックエンドの場合

この場合では、AWSリソースの変更頻度グループ別に分割したtfstateファイルを、異なるリモートバックエンドで管理します。

例えば、tfstateファイル分割に基づいて、リモートバックエンド内のディレクトリ構成例は以下の通りになります。

この例では、状態の依存関係図と同じ状況を仮定しています。

# Tes環境の状態のみを管理するバケット
🪣 tes-bucket/
├── 📂 high-freq
│    └── terraform.tfstate # high-freqコンポーネントの状態を持つ
│
├── 📂 middle-freq
│    └── terraform.tfstate # middle-freqコンポーネントの状態を持つ
│
└── 📂 low-freq
      └── terraform.tfstate # low-freqコンポーネントの状態を持つ
# Stg環境の状態のみを管理するバケット
🪣 stg-bucket/
│
...
# Prd環境の状態のみを管理するバケット
🪣 prd-bucket/
│
...


10. おわりに

Terraformのtfstateファイルの分割パターンをもりもり布教しました。

ぜひ採用してみたい分割パターンはあったでしょうか。

Terraformの開発現場の具体的な要件は千差万別であり、特にtfstateファイル間の状態の依存関係は様々です。

もし、この記事を参考に設計してくださる方は、分割パターンを現場に落とし込んで解釈いただけると幸いです🙇🏻‍


「自分を信じても…信頼に足る仲間を信じても…誰にもわからない…」

(お友達の@nwiizo, 2023, Terraform Modules で再利用できるので最高ではないでしょうか?)


謝辞

今回、Terraformの分割パターンの収集にあたり、以下の方々からの意見・実装方法も参考にさせていただきました。

(アルファベット順)

この場で感謝申し上げます🙇🏻‍


記事関連のおすすめ書籍


【ArgoCD🐙】ArgoCDのマイクロサービスアーキテクチャと自動デプロイの仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️



01. はじめに


ロケットに乗るタコのツラが腹立つわー。

argocd_rocket
画像引用元:Argo Project


さて最近の業務で、全プロダクトの技術基盤開発チームに携わっており、全プロダクト共有のArgoCD🐙とAWS EKSをリプレイスしました。

今回は、採用した設計プラクティスの紹介も兼ねて、ArgoCDのマイクロサービスアーキテクチャと自動デプロイの仕組みを記事で解説しました。

ArgoCDは、kubectlコマンドによるマニフェストのデプロイを自動化するツールです。

ArgoCDのアーキテクチャには変遷があり、解説するのは執筆時点 (2023/05/02) で最新の 2.6 系のArgoCDです。

アーキテクチャや仕組みはもちろん、個々のマニフェストの実装にもちょっとだけ言及します。

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. 概要

アーキテクチャ

レイヤー

まずは、ArgoCDのアーキテクチャのレイヤーがどのようになっているかを見ていきましょう。

ArgoCD公式から、コンポーネント図が公開されています。

図から、次のようなことがわかります👇

  • 下位レイヤー向きにしか依存方向がなく、例えばコアドメインとインフラのレイヤー間で依存性は逆転させていない。
  • レイヤーの種類 (UI、アプリケーション、コアドメイン、インフラ) とそれらの依存方向から、レイヤードアーキテクチャのようなレイヤーに分けている。
  • 特にコアドメインレイヤーが独立したコンポーネントに分割されており、マイクロサービスアーキテクチャを採用している。

argocd_architecture_layer.png


▶ ArgoCDのマイクロサービスアーキテクチャの分割単位について

ArgoCDのマイクロサービスアーキテクチャは、機能単位の分割方法を採用していると推測しています。

本記事では詳しく言及しませんが、マイクロサービスアーキテクチャの分割方法には大小いくつかの種類があり、境界付けられたコンテキストで分割することがベタープラクティスと言われています😎

(境界付けられたコンテキストについても、ちゃんと記事を投稿したい...)

機能単位による分割は、境界付けられたコンテキストのそれよりも粒度が小さくなります。


▶ ArgoCDのマイクロサービスアーキテクチャの設計図について

ArgoCDでは、マイクロサービスアーキテクチャの設計図にコンポーネント図を使用しています。

コンポーネント図では、依存方向 (そのコンポーネントがいずれのコンポーネントを使用するのか) に着目できます。

そのため、これはマイクロサービス間の依存方向を視覚化するために有効なUML図です🙆🏻‍

コンポーネント

次に、コンポーネントの種類を紹介します。

ArgoCDの各コンポーネントが組み合わさり、マニフェストの自動的なデプロイを実現します。

ArgoCD (2.6系) のコンポーネントはいくつかあり、主要なコンポーネントの種類とレイヤーは以下の通りです👇

コンポーネント レイヤー 機能
argocd-server
(argocd-apiserver)
UI・アプリケーション みんながよく知るArgoCDのダッシュボードです。
また、ArgoCDのAPIとしても機能します。
現在、複数のレイヤーの責務を持っており、将来的にUIとアプリケーションは異なるコンポーネントに分割されるかもしれません。
application-controller コアドメイン Clusterにマニフェストをデプロイします。
また、ArgoCD系カスタムリソースのカスタムコントローラーとしても機能します。
repo-server コアドメイン マニフェスト/チャートリポジトリからクローンを取得します。
また、クローンからマニフェストを作成します。
redis-server インフラ application-controllerの処理結果のキャッシュを保管します。
dex-server インフラ SSOを採用する場合、argocd-serverの代わりに認可リクエストを作成し、またIDプロバイダーに送信します。
これにより、argocd-server上の認証フェーズをIDプロバイダーに委譲できます。


以降の図の凡例です。

ArgoCDの各コンポーネント (application-controller、argocd-server、dex-server、repo-server) と各リソース (Application、AppProject) を区別しています。

argocd_architecture_legend.png

仕組み

それでは、ArgoCDは、どのようにコンポーネントを組み合わせて、マニフェストをデプロイするのでしょうか。

ここではプロダクト用Cluster管理者 (デプロイ先となるClusterを管理するエンジニア) は、ArgoCDのダッシュボードを介してマニフェストをデプロイするとしましょう。

まずは、概要を説明していきます。

argocd_architecture_introduction.png

(1) repo-serverによるクローン取得

ArgoCDのCluster上で、repo-serverがマニフェスト/チャートリポジトリのクローンを取得します。

(2) application-controllerによるマニフェスト取得

application-controllerは、repo-serverからマニフェストを取得します。

(3) application-controllerによるCluster確認

application-controllerは、プロダクト用Clusterの現状を確認します。

(4) application-controllerによる処理結果保管

application-controllerは、処理結果をredis-serverに保管します。

(5) argocd-serverによるキャッシュ取得

argocd-serverは、redis-serverからキャッシュを取得します。

(6) 管理者のログイン

プロダクト用Cluster管理者は、argocd-serverにログインしようとします。

(7) IDプロバイダーへの認証フェーズ委譲

argocd-serverは、ログイン時にIDプロバイダーに認証フェーズを委譲するために、dex-serverをコールします。


▶ argocd-serverのログイン手法について

プロダクト用Cluster管理者のログインには、利便性と安全性を兼ね備えたSSOの採用がオススメです。

今回の記事では、SSOを採用した場合の仕組みを紹介しています🙇🏻‍

(8) dex-serverによる認証リクエスト送信

dex-serverは、IDプロバイダーに認可リクエストを作成し、これをIDプロバイダーに送信します。

(9) argocd-serverによる認可フェーズ実行

argocd-serverで認可フェーズを実施します。

ログインが完了し、プロダクト用Cluster管理者は認可スコープに応じてダッシュボードを操作できます。


▶ ArgoCDをどのClusterで管理するかについて

プロダクト用Clusterの障害の影響範囲を受けないように、ArgoCDは、プロダクト用Clusterとは独立したClusterで作成した方がよいです。

今回の記事では、ArgoCD用Clusterを採用した場合の仕組みを紹介しています🙇🏻‍

(10) application-controllerによるマニフェストデプロイ

application-controllerは、Clusterにマニフェストをデプロイします。

マニフェストのデプロイの仕組みをざっくり紹介しました。

ただこれだと全く面白くないため、各コンポーネントの具体的な処理と、各々がどのように通信しているのかを説明します✌️


03. repo-server

repo-serverとは

まずは、コアドメインレイヤーにあるrepo-serverです。

マニフェスト/チャートリポジトリ (例:GiHub、GitHub Pages、Artifact Hub、AWS ECR、Artifact Registry、など) からクローンを取得します。

repo-serverを持つPodには、他に軽量コンテナイメージからなるInitContainerとサイドカー (cmp-server) がおり、それぞれ機能が切り分けられています👍

仕組み

argocd_architecture_repo-server.png

(1) InitContainerによるお好きなツールインストール & argocd-cliバイナリコピー

repo-serverの起動時に、InitContainerでお好きなマニフェスト管理ツール (Helm、Kustomize、など) やプラグイン (helm-secrets、KSOPS、SOPS、argocd-vault-plugin、など) をインストールします。

また、サイドカーのcmp-serverでは起動時に/var/run/argocd/argocd-cmp-serverコマンドを実行する必要があり、InitContainer (ここではcopyutilコンテナ) を使用して、ArgoCDのコンテナイメージからargocd-cliのバイナリファイルをコピーします。

repo-serverのざっくりした実装例は以下の通りです👇

ここでは、ArgoCDで使いたいツール (Helm、SOPS、helm-secrets) をInitContainerでインストールしています。

apiVersion: v1
kind: Pod
metadata:
  name: argocd-repo-server
  namespace: argocd
spec:
  containers:
    - name: repo-server
      image: quay.io/argoproj/argocd:latest

  initContainers:
    # HelmをインストールするInitContainer
    - name: helm-installer
      image: alpine:latest
      command:
        - /bin/sh
        - -c
      args:
        - |
          # インストール処理
      volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools
    # SOPSをインストールするInitContainer
    - name: sops-installer
      image: alpine:latest
      command:
        - /bin/sh
        - -c
      args:
        - |
          # インストール処理
      volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools
    # helm-secretsをインストールするInitContainer
    - name: helm-secrets-installer
      image: alpine:latest
      command:
        - /bin/sh
        - -c
      args:
        - |
          # インストール処理
      volumeMounts:
        - mountPath: /helm-working-dir/plugins
          name: helm-working-dir

    ...

    # cmp-serverにargocd-cliのバイナリをコピーするInitContainer
    - name: copyutil
      image: quay.io/argoproj/argocd:latest
      command:
        - cp
        - -n
        - /usr/local/bin/argocd
        - /var/run/argocd/argocd-cmp-server
      volumeMounts:
        - name: var-files
          mountPath: /var/run/argocd

  # Podの共有ボリューム
  volumes:
    - name: custom-tools
      emptyDir: {}
    - name: var-files
      emptyDir: {}


▶ ArgoCDのコンテナイメージに組み込まれているツールについて

ArgoCDのコンテナイメージ (quay.io/argoproj/argocd) には、いくつかのツール (例:Helm、Kustomize、Ks、Jsonnet、など) の推奨バージョンがあらかじめインストールされています。

そのため、これらのツールのプラグイン (例:helm-secrets) を使用する場合、上記のコンテナイメージからなるrepo-server内のツールをcmp-serverにコピーすればよいのでは、と思った方がいるかもしれません。

この方法は全く問題なく、cmp-serverの/usr/local/binディレクトリ配下にツールをコピーするように、InitContainerを定義してもよいです。
apiVersion: v1
kind: Pod
metadata:
  name: argocd-repo-server
  namespace: foo
spec:
  containers:
    - name: repo-server
      image: quay.io/argoproj/argocd:latest
      volumeMounts:
        - mountPath: /usr/local/bin/helm
          # Podの共有ボリュームを介して、repo-serverでHelmを使用する。
          name: custom-tools

  initContainers:
    - name: copy-helm
      image: quay.io/argoproj/argocd:latest
      # InitContainer上のHelmをVolumeにコピーする
      command:
        - /bin/cp
        - -n
        - /usr/local/bin/helm
        - /custom-tools/helm
      volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools

  # 共有ボリューム
  volumes:
    - name: custom-tools
      emptyDir: {}

反対に、これらツールをInitContainerでインストールし直す場合は、ArgoCD上での推奨バージョンをちゃんとインストールするようにしましょう👍

2.6系では、ArgoCDのリポジトリ内のtool-versions.shファイルに、Helmのバージョンが定義されています。
spec:

  ...

  initContainers:
    - name: helm-installer
      image: alpine:latest
      command:
        - /bin/sh
        - -c
      # ArgoCDのリポジトリ上のtool-versions.shファイルから、Helmのバージョンを取得する
      args:
        - |
          apk --update add curl wget
          ARGOCD_VERSION=$(curl -s https://raw.githubusercontent.com/argoproj/argo-helm/argo-cd-<ArgoCDのバージョン>/charts/argo-cd/Chart.yaml | grep appVersion | sed -e 's/^[^: ]*: //')
          HELM_RECOMMENDED_VERSION=$(curl -s https://raw.githubusercontent.com/argoproj/argo-cd/"${ARGOCD_VERSION}"/hack/tool-versions.sh | grep helm3_version | sed -e 's/^[^=]*=//')
          wget -q https://get.helm.sh/helm-v"${HELM_RECOMMENDED_VERSION}"-linux-amd64.tar.gz
          tar -xvf helm-v"${HELM_RECOMMENDED_VERSION}"-linux-amd64.tar.gz
          cp ./linux-amd64/helm /custom-tools/
          chmod +x /custom-tools/helm
      volumeMounts:
        - mountPath: /custom-tools
          name: custom-tools

  ...

(2) repo-serverによる認証情報取得

repo-serverは、Secret (argocd-repo-creds) からリポジトリの認証情報を取得します。

argocd-repo-credsではリポジトリの認証情報のテンプレートを管理しています。

指定した文字列から始まる (最長一致) URLを持つリポジトリに接続する場合、それらの接続で認証情報を一括して適用できます。

argocd-repo-credsのざっくりした実装例は以下の通りです👇

ここでは、リポジトリSSH公開鍵認証を採用し、argocd-repo-credsに共通の秘密鍵を設定しています。

apiVersion: v1
kind: Secret
metadata:
  name: argocd-repo-creds-github
  namespace: argocd
  labels:
    argocd.argoproj.io/secret-type: repo-creds
type: Opaque
data:
  type: git
  url: https://github.com/hiroki-hasegawa
  # 秘密鍵
  sshPrivateKey: |
    MIIC2 ...

あとは、各リポジトリのSecret (argocd-repo) にURLを設定しておきます。

すると、先ほどのargocd-repo-credsのURLに最長一致するURLを持つSecretには、一括して秘密鍵が適用されます。

# foo-repositoryをポーリングするためのargocd-repo
apiVersion: v1
kind: Secret
metadata:
  namespace: argocd
  name: foo-argocd-repo
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
data:
  # 認証情報は設定しない。
  # チャートリポジトリ名
  name: bar-repository
  # https://github.com/hiroki-hasegawa に最長一致する。
  url: https://github.com/hiroki-hasegawa/bar-chart.git
---
# baz-repositoryをポーリングするためのargocd-repo
apiVersion: v1
kind: Secret
metadata:
  namespace: foo
  name: baz-argocd-repo
  labels:
    argocd.argoproj.io/secret-type: repository
type: Opaque
data:
  # 認証情報は設定しない。
  # チャートリポジトリ名
  name: baz-repository
  # https://github.com/hiroki-hasegawa に最長一致する。
  url: https://github.com/hiroki-hasegawa/baz-chart.git

(3) repo-serverのよるクローン取得とポーリング

repo-serverは、認証情報を使用して、リポジトリgit cloneコマンドを実行します。

取得したクローンを、/tmp/_argocd-repoディレクトリ配下にUUIDの名前で保管します。

また、リポジトリの変更をポーリングし、変更を検知した場合はgit fetchコマンドを実行します。

# クローンが保管されていることを確認できる
$ kubectl -it exec argocd-repo-server \
    -c repo-server \
    -n foo \
    -- bash -c "ls /tmp/_argocd-repo/<URLに基づくUUID>"

# リポジトリ内のファイル
Chart.yaml  README.md  templates  values.yaml


▶ repo-serverでのクローン保管先のバージョン差異について

2.3以前では、repo-serverは/tmpディレクトリ配下にURLに基づく名前でクローンを保管します。
$ kubectl -it exec argocd-repo-server \
    -c repo-server \
    -n foo \
    -- bash -c "ls /tmp/https___github.com_hiroki-hasegawa_foo-repository"

# リポジトリ内のファイル
Chart.yaml  README.md  templates  values.yaml

(4) repo-serverによるサイドカーコール

repo-serverは、自身にマウントされたいくつかのマニフェスト管理ツール (例:Helm、Kustomize) を実行する機能を持っています。

しかし、実行できないツールではサイドカー (cmp-server) をコールします。

この時、Applicationのspec.source.pluginキーでプラグイン名を指定すると、そのApplicationではサイドカーをコールします。

逆を言えば、プラグイン名を指定していないApplicationは、サイドカーをコールしない です。

apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
  name: foo-application
  namespace: foo
spec:
  source:
    plugin:
      name: helm-secrets # このプラグイン名は、ConfigManagementPluginのmetadata.nameキーに設定したもの

  ...

このコールは、Volume上のUnixドメインソケットを経由します。

Unixドメインソケットのエンドポイントの実体は.sockファイルです。

$ kubectl exec -it argocd-repo-server -c foo-plugin-cmp-server\
    -- bash -c "ls /home/argocd/cmp-server/plugins/"

foo-plugin.sock


Unixソケットドメインについて

Unixソケットドメインは、同じOS上のファイルシステムを介して、データを直接的に送受信する仕組みです。

Unixソケットドメインを使用すると、同じVolumeがマウントされたコンテナのプロセス間で、データを送受信できます👍

(5) repo-serverによる暗号化キーと暗号化変数の取得

cmp-serverは、暗号化キー (例:AWS KMS、Google CKM、など) を使用してSecretストア (例:AWS SecretManager、Google SecretManager、SOPS、Vault、など) の暗号化変数を復号化します。


クラウドプロバイダーの暗号化キーを使用するために必要な証明書について

暗号化キーがクラウドプロバイダーにある場合、クラウドプロバイダーがHTTPSプロトコルの使用を求める場合があります。

cmp-serverに軽量なコンテナイメージを使用していると、/etc/sslディレクトリ (ディレクトリはOSによって異なる) に証明書が無く、cmp-serverがHTTPSプロトコルを使用できない可能性があります。

その場合は、お好きな方法で証明書をインストールし、コンテナにマウントするようにしてください👍
apiVersion: v1
kind: Pod
metadata:
  name: argocd-repo-server
  namespace: foo
spec:
  containers:
    - name: repo-server
      image: quay.io/argoproj/argocd:latest

  ...

    # サイドカーのcmp-server
    - name: helm-secrets-cmp-server
      image: ubuntu:latest

      ...

      volumeMounts:
        # サイドカーがAWS KMSを使用する時にHTTPSリクエストを送信する必要があるため、証明書をマウントする
        - name: certificate
          mountPath: /etc/ssl
  ...

  initContainers:
    - name: certificate-installer
      image: ubuntu:latest
      command:
        - /bin/sh
        - -c
      args:
        - |
          apt-get update -y
          # ルート証明書をインストールする
          apt-get install -y ca-certificates
          # 証明書を更新する
          update-ca-certificates
      volumeMounts:
        - mountPath: /etc/ssl
          name: certificate

  volumes:
    - name: certificate
      emptyDir: {}

(6) サイドカーによるプラグイン処理の取得

cmp-serverは、マニフェスト管理ツールのプラグイン (helm-secrets、argocd-vault-plugin、など) を実行します。

この時マニフェストの作成時のプラグインとして、ConfigMap配下のConfigManagementPluginでプラグインの処理を定義します。

ざっくりした実装例は以下の通りです👇

ここでは、プラグインとしてhelm-secretsを採用し、helm secrets templateコマンドの実行を定義します。

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmp-cm
  namespace: foo
data:
  helm-secrets-plugin.yaml: |
    apiVersion: argoproj.io/v1alpha1
    kind: ConfigManagementPlugin
    metadata:
      namespace: foo
      name: helm-secrets # このプラグイン名は、Applicationのspec.source.pluginキーで指定したもの
    spec:
      generate:
        command:
          - /bin/bash
          - -c
        args:
          - |
            set -o pipefail
            helm secrets template -f $ARGOCD_ENV_SECRETS -f $ARGOCD_ENV_VALUES -n $ARGOCD_APP_NAMESPACE $ARGOCD_APP_NAME .
  foo-plugin.yaml: |
    ...


▶ ConfigManagementPluginのファイル名について

プラグインごとにConfigManagementPluginのマニフェストを定義できるように、各ConfigManagementPluginを異なるファイル名とし、ConfigMapで管理するとよいです👍

(7) サイドカーによるプラグイン処理の実行

cmp-serverはプラグインを実行し、Secretを含むマニフェストを作成します。

ConfigMap配下のファイルをplugin.yamlの名前でサイドカーにマウントする必要があります。

また、先ほどのUnixドメインソケットの.sockファイルや、 cmp-serverがプラグインを実行するための各バイナリファイルもマウントが必要です。

ざっくりした実装例は以下の通りです👇

ここでは、helm-secretsプラグインを実行するサイドカー (helm-secrets-cmp-server) を作成します。

apiVersion: v1
kind: Pod
metadata:
  name: argocd-repo-server
spec:
  containers:
    # repo-server
    - name: repo-server
      image: quay.io/argoproj/argocd:latest

    ...

    # helm-secretsのcmp-server
    - name: helm-secrets-cmp-server
      # コンテナイメージは軽量にする
      image: ubuntu:latest
      command:
        - /var/run/argocd/argocd-cmp-server
      env:
        # helmプラグインの場所を設定する
        - name: HELM_PLUGINS
          value: /helm-working-dir/plugins
      securityContext:
        runAsNonRoot: true
        runAsUser: 999
      volumeMounts:
        # リポジトリのクローンをコンテナにマウントする
        - name: tmp
          mountPath: /tmp
        # ConfigManagementPluginのマニフェスト (helm-secrets.yaml) を "plugin.yaml" の名前でコンテナにマウントする
        - name: argocd-cmp-cm
          mountPath: /home/argocd/cmp-server/config/plugin.yaml
          subPath: helm-secrets.yaml
        # コンテナ間で通信するためのUnixドメインソケットファイルをコンテナにマウントする
        - name: plugins
          mountPath: /home/argocd/cmp-server/plugins
        # 任意のツールのバイナリファイルをコンテナにマウントする
        - name: custom-tools
          mountPath: /usr/local/bin
        # helmプラグインのバイナリをコンテナにマウントする
        - name: helm-working-dir
          mountPath: /helm-working-dir/plugins

      ...

  # Podの共有ボリューム
  volumes:
    # リポジトリのクローンを含む
    - name: tmp
      emptyDir: {}
    # Helmなどの任意のツールを含む
    - name: custom-tools
      emptyDir: {}
    # helmプラグインを含む
    - name: helm-working-dir
      emptyDir: {}


▶ マウント時のConfigManagementPluginのファイル名について

ArgoCDのv2.6では、ConfigManagementPluginのマニフェスト/home/argocd/cmp-server/configディレクトリに、plugin.yamlの名前でマウントしないといけません。

これは、cmp-serverの起動コマンド (/var/run/argocd/argocd-cmp-server) がplugin.yamlの名前しか扱えないためです。

ArgoCD公式の見解で、サイドカーでは単一のプラグインしか実行できないように設計しているとのコメントがありました。

今後のアップグレードで改善される可能性がありますが、v2.6では、ConfigManagementPluginの数だけcmp-serverが必要になってしまいます🙇🏻‍


▶ Kustomizeのプラグインをどのコンテナで実行するかについて

Kustomizeのプラグイン (例:KSOPS) によるマニフェスト作成は、サイドカーではなくrepo-serverで実行した方がよいかもしれません (Helmプラグインサイドカーで問題ないです)。

執筆時点 (2023/05/02) では、ArgoCDとKustomizeが密に結合しています。

例えば、ArgoCD上のKustomize系オプションはrepo-serverでマニフェストを作成することを想定して設計されています。

無理やりサイドカーでKustomizeのプラグインを実行しようとすると、ArgoCDの既存のオプションを無視した実装になってしまうため、Kustomizeのプラグインだけはrepo-serverで実行することをお勧めします😢


クラウドプロバイダーのSecretストアを採用する場合について

今回は詳しく言及しませんが、クラウドプロバイダーのSecretストア (例:AWS SecretManager、Google SecretManager、など) の変数を使用する場合は、Secretのデータ注入ツールのプラグイン (特にargocd-vault-plugin) を採用しなくてもよいです。

この場合、代わりにSecretsストアCSIドライバーやExternalSecretsOperatorを使用できます。

これらは、クラウドプロバイダーから変数を取得し、これをSecretにデータとして注入してくれます🙇🏻‍


04. application-controller、redis-server

application-controllerとは

コアドメインレイヤーにあるapplication-controllerです。

Clusterにマニフェストをデプロイします。

また、ArgoCD系カスタムリソースのカスタムコントローラーとしても機能します。

redis-serverとは

インフラレイヤーにあるredis-serverです。

application-controllerの処理結果のキャッシュを保管します。

仕組み

argocd_architecture_application-controller.png

(1) ArgoCD用Cluster管理者のkubectl applyコマンド

ArgoCD用Clusterの管理者は、ClusterにArgoCD系のカスタムリソース (例:Application、AppProject、など) をデプロイします。


▶ ArgoCD自体のデプロイにargo-helmを採用する場合について

『卵が先か、ニワトリが先か』みたいな話ですが、ArgoCD自体はArgoCD以外でデプロイする必要があります。

この時、argo-helmを使用すると簡単にArgoCDのマニフェストを作成できます。


ただしHelmの重要な仕様として、チャートの更新時に使用するhelm upgradeコマンドは、CRDを作成できる一方でこれを変更できません。

HelmでCRDを作成するとHelmの管理ラベルが挿入されてしまうため、作成の時点からCRDがHelmの管理外となるように、kubectlコマンドでCRDを作成した方がよいです👍
$ kubectl diff -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=<バージョンタグ>"

$ kubectl apply -k "https://github.com/argoproj/argo-cd/manifests/crds?ref=<バージョンタグ>"
ArgoCD上でHelmを使用してデプロイする場合はこの仕様を気にしなくてよいのかな、と思った方がいるかもしれないです。

ですが本記事で解説した通り、ArgoCDはcmp-serverのhelm templateコマンド (この時、--include-crdsオプションが有効になっている) や、application-controllerのkubectl applyコマンドを組み合わせてマニフェストをデプロイしているため、CRDもちゃんと更新してくれます👍🏻

(2) application-controllerによるArgoCD系カスタムリソースのReconciliation

kube-controller-managerは、application-controllerを操作し、Reconciliationを実施します。

application-controllerは、Etcd上に永続化されたマニフェストと同じ状態のArgoCD系カスタムリソースを作成/変更します。


▶ カスタムコントローラーでもあるapplication-controllerについて

先ほど記載したと通り、application-controllerはカスタムコントローラーとしても機能します。

本記事では詳しく言及しませんが、カスタムコントローラーの仕組みやCRDとの関係については、以下の記事が非常に参考になりました🙇🏻‍

(3) application-controllerによるマニフェスト取得

application-controllerは、repo-serverからリポジトリマニフェストを取得します。

取得したマニフェストは、repo-serverのサイドカーであるcmp-serverが作成したものです。

(4) application-controllerによるヘルスチェック

application-controllerは、プロダクト用Clusterをヘルスチェックします。

application-controllerには、gitops-engineパッケージが内蔵されており、これはヘルスチェックからデプロイまでの基本的な処理を実行します。


▶ gitops-engineパッケージについて

gitops-engineは、ArgoCDのデプロイに必要な処理を揃えたパッケージです。

執筆時点 (2023/05/02) の最新バージョン `v0.7.0` では以下のディレクトリからなります👇
🐱 gitops-engine/
├── 📂 pkg
│    ├── cache
│    ├── diff   # リポジトリとClusterの間のマニフェストの差分を検出する。ArgoCDのDiff機能に相当する。
│    ├── engine # 他のパッケージを使い、GitOpsの一連の処理を実行する。
│    ├── health # Clusterのステータスをチェックする。ArgoCDのヘルスチェック機能に相当する。
│    ├── sync   # Clusterにマニフェストをデプロイする。ArgoCDのSync機能に相当する。
│    └── utils  # 他のパッケージに汎用的な関数を提供する。
│
...
マイクロサービスアーキテクチャ

(5) application-controllerによるマニフェスト差分検出

application-controllerは、プロダクト用Clusterのマニフェストと、repo-serverから取得したマニフェストの差分を検出します。

ここで、kubectl diffコマンドの実行が自動化されています。

(6) application-controllerによる処理結果保管

application-controllerは、処理結果をredis-serverに保管します。

redis-serverは、Applicationやリポジトリのコミットの単位で、application-controllerの処理結果を保管しています。

$ kubectl exec -it argocd-redis-server \
    -n foo \
    -- sh -c "redis-cli --raw"

127.0.0.1:6379> keys *

...

app|resources-tree|<Application名>|<キャッシュバージョン>
cluster|info|<プロダクト用ClusterのURL>|<キャッシュバージョン>
git-refs|<マニフェスト/チャートリポジトリのURL>|<キャッシュバージョン>
mfst|app.kubernetes.io/instance|<Application名>|<最新のコミットハッシュ値>|<デプロイ先Namespace>|*****|<キャッシュバージョン>

...

(7) application-controllerによるマニフェストデプロイ

application-controllerは、Applicationの操作に応じて、Clusterにマニフェストをデプロイします。

ここで、kubectl applyコマンドの実行が自動化されています。


▶ application-controllerがマニフェストを操作した証拠について

Kubernetesリソースのマニフェストには、metadata.managedFieldsキーがあり、何がそのマニフェストを作成/変更したのかを確認できます。

実際にマニフェストを確認してみると、確かにapplication-controllerがマニフェストを作成/変更してくれたことを確認できます。
apiVersion: apps/v1
kind: Deployment
metadata:
  managedFields:
    # ArgoCDのapplication-controllerによる管理
    - manager: argocd-application-controller
      apiVersion: apps/v1
      # kube-apiserverに対するリクエスト内容
      operation: Update
      time: "2022-01-01T16:00:00.000Z"
      # ArgoCDのapplication-controllerが管理するマニフェストのキー部分
      fields: ...


05. dex-server

dex-serverとは

インフラレイヤーにあるdex-serverです。

SSO (例:OAuth 2.0SAML、OIDC) を採用する場合、argocd-serverの代わりに認可リクエストを作成し、またIDプロバイダー (例:GitHub、Keycloak、AWS Cognito、Google Auth、など) に送信します。

これにより、argocd-server上の認証フェーズをIDプロバイダーに委譲できます。


▶ dex-serverの必要性について

dex-serverを使わずに、argocd-serverからIDプロバイダーに認可リクエストを直接的に送信することもできます。

執筆時点 (2023/05/02) で、argocd-serverは特にOIDCの認可リクエストを作成できるため、ログイン要件がOIDCの場合は、dex-serverを必ずしも採用してなくもよいです。

言い換えれば、その他のSSO (例:OAuth 2.0SAML) を使用する場合は、dex-serverを採用する必要があります👍

仕組み

argocd_architecture_dex-server.png

(1) プロダクト用Cluster管理者のログイン

プロダクト用Cluster管理者がダッシュボード (argocd-server) にSSOを使用してログインしようとします。

(2) IDプロバイダーへの認証フェーズ委譲

argocd-serverは、認証フェーズをIDプロバイダーに委譲するために、dex-serverをコールします。


▶ 認証フェーズの委譲について

argocd-serverの認証認可処理は、AuthN (認証) と AuthZ (認可) から構成されています。

今回は認証フェーズを委譲した場合で仕組みを解説していますが、反対に委譲しなかった場合、このAuthNでArgoCD上で定義したユーザーやグループを認証することになります👍

argocd_auth_architecture

(3) dex-serverによる認可リクエスト作成

dex-serverは、認可リクエストを作成します。

認可リクエストに必要な情報は、ConfigMap (argocd-cm) で設定しておく必要があります。

argocd-cmのざっくりした実装例は以下の通りです👇

ここでは、IDプロバイダーをGitHubとし、認可リクエストに必要なクライアントIDとクライアントシークレットを設定しています。

apiVersion: v1
kind: ConfigMap
metadata:
  namespace: foo
  name: argocd-cm
data:
  dex.config: |
    connectors:
      - type: github
        id: github
        name: GitHub SSO
        config:
          clientID: *****
          clientSecret: *****
        # dex-serverが認可レスポンスを受信するURLを設定する
        redirectURI: https://example.com/api/dex/callback


▶ dex-serverの設定について

dex.configキー配下の設定方法は、dexのドキュメントをみるとよいです👍

(4) dex-serverによる認可リクエスト送信

dex-serverは、前の手順で作成した認可リクエストをIDプロバイダーに送信します。

(5) IDプロバイダーによる認証フェーズ実施

IDプロバイダー側でSSOの認証フェーズを実施します。

IDプロバイダーは、コールバックURL (<ArgoCDのドメイン名>/api/dex/callback) を指定して、認可レスポンスを送信します。

認可レスポンスは、argocd-serverを介して、dex-serverに届きます。


▶ dex-serverのコールバックURLについて

IDプロバイダー側のコールバックURLの設定で、dex-serverのエンドポイントを指定する必要があります。

例えばGitHubをIDプロバイダーとする場合、 Developer settingsタブ でSSOを設定する必要があり、この時にAuthorization callback URLという設定箇所があるはずです👍🏻

(6) argocd-serverによる認可フェーズ実施

argocd-serverは、AuthZで認可フェーズを実施します。

ConfigMap (argocd-rbac-cm) を参照し、IDプロバイダーから取得したユーザーやグループに、ArgoCD系カスタムリソースに関する認可スコープを付与します。

ざっくりした実装例は以下の通りです👇

ここでは、developerロールにはdevというAppProjectに属するArgoCD系カスタムリソースにのみ、またmaintainerロールには全てのAppProjectの操作を許可しています。

またこれらのロールを、IDプロバイダーで認証されたグループに紐づけています。

特定のArgoCD系カスタムリソースのみへのアクセスを許可すれば、結果として特定のClusterへのデプロイのみを許可したことになります👍

apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: foo
data:
  # デフォルトのロール
  policy.default: role:developer
  policy.csv: |
    p, role:developer, *, *, dev/*/*, allow
    p, role:maintainer, *, *, dev/*/*, allow
    p, role:maintainer, *, *, prd/*/*, allow

    g, developers, role:developer
    g, maintainers, role:maintainer
  scopes: "[groups]"


▶ AppProjectの認可定義の記法について

ConfigMap (argocd-rbac-cm) の認可スコープの定義には、 Casbin の記法を使用します。

今回の実装例で使用したp (パーミッション) とg (グループ) では、以下を記法を使用できます👍
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-rbac-cm
  namespace: argocd
data:
  policy.default: role:readonly
  policy.csv: |
    # ロールとArgoCD系カスタムリソースの認可スコープを定義する
    p, role:<ロール名>, <Kubernetesリソースの種類>, <アクション名>, <AppProject名>/<ApplicationのNamespace名>/<Application名>, <許否>

    # 認証済みグループにロールを紐付ける
    g, <グループ名>, role:<ロール名>
  scopes: "[groups]"


06. argocd-server (argocd-apiserver)

argocd-serverとは

最後に、インフラレイヤーにあるargocd-serverです。

『argocd-apiserver』とも呼ばれます。

みんながよく知るArgoCDのダッシュボードです。

また、ArgoCDのAPIとしても機能し、他のコンポーネントと通信します🦄

仕組み

argocd_architecture_argocd-apiserver.png

(1) application-controllerによるヘルスチェック

application-controllerは、プロダクト用Clusterをヘルスチェックします。

(2) application-controllerによるマニフェスト差分検出

application-controllerは、プロダクト用Clusterのマニフェストと、ポーリング対象のリポジトリマニフェストの差分を検出します。

(3) application-controllerによる処理結果保管

application-controllerは、処理結果をredis-serverに保管します。

(4) application-controllerによる処理結果取得

argocd-serverは、redis-serverから処理結果を取得します。

(5) プロダクト用Cluster管理者のログイン

プロダクト用Cluster管理者がダッシュボード (argocd-server) にSSOを使用してログインしようとします。

(6) Ingressコントローラーによるルーティング

Ingressコントローラーは、Ingressのルーティングルールを参照し、argocd-serverにルーティングします。

(7) IDプロバイダーへの認証フェーズ委譲

argocd-serverは、ログイン時にIDプロバイダーに認証フェーズを委譲するために、dex-serverをコールします。

(8) IDプロバイダーによる認証フェーズ実施

IDプロバイダー上で認証フェーズが完了します。

argocd-serverは、ConfigMap (argocd-rbac-cm) を参照し、プロダクト用Cluster管理者に認可スコープを付与します。

(9) argocd-serverによる認可フェーズ実施

argocd-serverは、認可スコープに応じて、プロダクト用Cluster管理者がApplicationを操作可能にします。


▶ Namespacedスコープモードについて

今回の図のように、単一のArgoCD用Clusterで複数プロダクトのApplicationを管理する場合、Namespaceを単位としたテナント分割を設定した方がよいです。

その場合、ArgoCD本体をNamespacedスコープモードに設定する必要があります。

Namespacedスコープモードの場合、以下の設定が不要です。
apiVersion: v1
kind: ConfigMap
metadata:
  name: argocd-cmd-params-cm
  namespace: foo
data:
  # 設定してはダメ
  # application.namespaces: "*" # 全てのNamespaceを許可する。
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
  name: dev-foo-project
  namespace: foo
spec:
  # 設定してはダメ
  # sourceNamespaces:
  #  - "foo"
これらにより、fooのNamespaceに属するArgoCDは、他のNamespaceにはアクセスできなくなります👍

(10) application-controllerによるマニフェストデプロイ

プロダクト用Cluster管理者は、ダッシュボード (argocd-server) を使用して、ClusterにマニフェストをSyncします。

この時、Applicationを介してapplication-controllerを操作し、マニフェストをデプロイします。

図では、App-Of-Appsパターンを採用したと仮定しています👨‍👩‍👧‍👦


▶ App-Of-Appsパターンについて

ArgoCDにはApp-Of-Appsパターンというデザインパターンがあります。

これは、Applicationを階層的に作成するパターンであり、最下層のApplication配下のマニフェストをより疎結合に管理できます✌️

例えば以下の画像の通り、最上位のApplication配下に、チーム別の親Applicationを配置します (アプリチームの親Application、インフラチームのそれ) 。

その後、両方のApplication配下にさらにチャート別に最下層の子Applicationを配置し、チャートのデプロイを管理します。

アプリチーム最下層の子Applicationではアプリコンテナのチャート、インフラチームの子Applicationでは監視/ネットワーク/ハードウェアリソース管理系のチャートを管理します👍

root-application


07. アーキテクチャのまとめ

今までの全ての情報をざっくり整理して簡略化すると、ArgoCDは以下の仕組みでマニフェストをデプロイすることになります👇

argocd_architecture.png


08. おわりに

ArgoCDによるデプロイの仕組みの仕組みをもりもり布教しました。

ArgoCDは、UIが使いやすく、仕組みの詳細を知らずとも比較的簡単に運用できるため、ユーザーフレンドリーなツールだと思っています。

もしArgoCDを使わずにマニフェストをデプロイしている方は、ArgoCDの採用をハイパー・ウルトラ・アルティメットおすすめします👍


謝辞

ArgoCDの設計にあたり、以下の方に有益なプラクティスをご教授いただきました。

この場で感謝申し上げます🙇🏻‍


記事関連のおすすめ書籍


【Istio⛵️】Istioを安全にアップグレードするカナリア方式とその仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

  • Istioのアップグレード手法の種類について
  • 安全なカナリア方式の仕組みについて



01. はじめに


隠しません。

有吉弘行のサンデーナイトドリーマー が生きがいです。

istio-icon


さて、最近の業務でIstio⛵️をひたすらアップグレードしています。

今回は、採用したアップグレード手法の紹介も兼ねて、Istioの安全なアップグレード手法の仕組みを記事で解説しました。

Istioのアップグレード手法には変遷があり、解説するのは執筆時点 (2023/02/26) で最新の 1.14 系のアップグレード手法です。

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. なぜ安全なアップグレードが必要なのか

起こりうる問題

そもそも、なぜIstioで安全なアップグレードを採用する必要があるのでしょうか。

Istioで問題が起こると、Pod内のistio-proxyコンテナが正しく稼働せず、システムに大きな影響を与える可能性があります。

例えば、istio-proxyコンテナのPodへのインジェクションがずっと完了せず、アプリコンテナへの通信が全て遮断されるといったことが起こることがあります。

istio_istio_incident


採用するべきアップグレード手法

執筆時点 (2023/02/26) では、Istiodコントロールプレーン (以降、Istiodとします) のアップグレード手法には、『インプレース方式』と『カナリア方式』があります。

また合わせてアップグレードが必要なIstio IngressGatewayには、その手法に『インプレース方式』があります。

istio_upgrade_list

今回の安全なアップグレード手法として、Istiodでは『カナリアアップグレード』、Istio IngressGatewayでは『インプレースアップグレード』を採用します。


03. アップグレード手法を説明する前に

カナリアリリースとは

Istiodのカナリアアップグレードが理解しやすくなるように、カナリアリリースから説明したいと思います。

カナリアリリースは、実際のユーザーにテストしてもらいながらリリースする手法です。

もしカナリアリリースをご存知の方は、 04. アップグレード手法の概要 まで飛ばしてください🙇🏻‍


カナリアリリースの手順

カナリアリリースは、一部のユーザーを犠牲にすることになる一方で、アプリを実地的にテストできる点で優れています。

手順を交えながら説明します。

(1) 新環境のリリース

旧環境のアプリを残したまま、新環境をリリースします。

この段階では、全てのユーザー (100%) を旧環境にルーティングします。

canary-release_1

(2) 新環境への重み付けルーティング

ロードバランサーで重み付けを変更し、一部のユーザー (ここでは10%) を新環境にルーティングします。

canary-release_2

(3) 実地的テストの実施

ユーザーの手を借りて新環境を実地的にテストします (例:該当のエラーメトリクスが基準値を満たすか) 。

canary-release_3

(4) 重み付けの段階的変更

新環境に問題が起こらなければ、重み付けを段階的に変更し、最終的には全てのユーザー (100%) を新環境にルーティングします。

canary-release_4


カナリアリリース』の呼称の由来

カナリアリリースについては、その呼称の由来を知ると、より理解が深まります。

カナリアリリースは、20世紀頃の炭坑労働者の危機察知方法に由来します。

炭鉱内には有毒な一酸化炭素が発生する場所がありますが、これは無色無臭なため、気づくことに遅れる可能性があります。

そこで当時の炭鉱労働者は、一酸化炭素に敏感な『カナリア』を炭鉱内に持ち込み、カナリアの様子から一酸化炭素の存在を察知するようにしていたそうです。

つまり、先ほどの『犠牲になる一部のユーザー』が、ここでいうカナリアというわけです😨

canary_release_origin
画像引用元:George McCaa, U.S. Bureau of Mines


04. アップグレード手法の概要

カナリアリリースを理解したところで、Istioの安全なアップグレード手法の概要を説明します。

おおよそ以下の手順からなります。

なお各番号は、05. アップグレード手法の詳細 の (1) 〜 (8) に対応しています。


(1) アップグレード前の検証

旧Istiodが稼働しています。

ここで、アップグレードが可能かどうかを検証しておきます。

istio_canary-upgrade_1


(2) 新Istiodのインストール

新Istiod (discoveryコンテナ) をインストールします。

istio_canary-upgrade_2


(3) Webhookの宛先のServiceの変更

新Istiodのistio-proxyコンテナをインジェクションできるように、Webhookの宛先のServiceを変更します。

この手順は重要で、後の (3) Webhookの宛先のServiceの変更 で詳細を説明しています。


(4) Istio IngressGatewayをインプレースアップグレード

Istio IngressGatewayをインプレースアップグレードします。

istio_canary-upgrade_4


(5) 一部のNamespaceのistio-proxyコンテナをアップグレード

一部のNamespaceで、istio-proxyコンテナをカナリアアップグレードします。

istio_canary-upgrade_5


▶︎ 『カナリアアップグレード』の呼称について

ここで、カナリアリリースのような重み付けがなく、カナリアアップグレードの『カナリア』という呼称に違和感を持つ方がいるかもしれません。

これについては、全てのNamespaceのistio-proxyコンテナを一斉にアップグレードするのではなく、段階的にアップグレードしていく様子を『カナリア』と呼称している、と個人的に推測しています。

もし『カナリアアップグレード』の由来をご存じの方は、ぜひ教えていただけると🙇🏻‍


(6) ユーザの手を借りたテスト

ユーザーの手を借りて、実地的にテストします (例:該当のエラーメトリクスが基準値以下を満たすか) 。

istio_canary-upgrade_6


(7) istio-proxyコンテナの段階的アップグレード

新Istiodのistio-proxyコンテナに問題が起こらなければ、他のNamespaceでもistio-proxy コンテナを段階的にカナリアアップグレードしていきます。

一方でもし問題が起これば、Namespaceのistio-proxyコンテナとIstio IngressGatewayをダウングレードします。

istio_canary-upgrade_7


(8) 旧Istiodのアンインストール

最後に、旧Istiodをアンインストールします。

istio_canary-upgrade_8


05. アップグレード手法の詳細

istioctl コマンドを使用したアップグレード

ここからは、04. アップグレード手法の概要 を深ぼっていきます。

今回は、ドキュメントで一番優先して記載されている istioctl コマンドを使用した手順 を説明します。

なお各番号は、04. アップグレード手法の概要 の (1) 〜 (8) に対応しています。


▶︎ アップグレードに使用するツールについて

もちろん、istioctlコマンド以外のツール (例:helmコマンド、helmfileコマンド、ArgoCD) を使用してもアップグレードできます。

細かな手順が異なるだけで、アップグレード手法の概要は同じです🙆🏻‍


前提

Namespace

まず最初に、前提となる状況を設定しておきます。

istio_canary-upgrade_rollout-restart_1

各Namespaceのistio.io/revラベルにdefaultが設定されているとします。

$ kubectl get namespace -L istio.io/rev

NAME              STATUS   AGE   REV
foo               Active   34d   default
bar               Active   34d   default
baz               Active   34d   default
istio-ingress     Active   34d   default

...


▶︎ istio.io/revラベル値のエイリアスについて

istio.io/revラベル値は、どんなエイリアスでもよいです。

よくあるエイリアスとしてdefaultstableを使用します👍

さらに、マニフェストに書き起こすと以下のようになっています。

apiVersion: v1
kind: Namespace
metadata:
  name: foo
  labels:
    istio.io/rev: default

このistio.io/revラベルがあることにより、そのNamespaceのPodにistio-proxyコンテナを自動的にインジェクションします。


▶︎ istio-proxyコンテナのインジェクションの仕組みについてについて

istio-proxyコンテナのインジェクションの仕組みについては、今回言及しておりません。

以下の記事で解説していますため、もし気になる方はよろしくどうぞ🙇🏻‍

Istiod

istio_canary-upgrade_rollout-restart_1

すでに1-14-6のIstiodが動いており、1-15-4カナリアアップグレードします。

IstiodはDeployment配下のPodであり、このPodはIstiodの実体であるdiscoveryコンテナを持ちます。

$ kubectl get deployment -n istio-system -l app=istiod

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-14-6          1/1     1            1           47s # 1-14-6

Istio IngressGateway

Istio IngressGatewayはIstiodとは異なるNamespaceで動いており、インプレースアップグレードします。

istio_canary-upgrade_rollout-restart_1

Istio IngressGatewayはistio-proxyコンテナを持ちます。

$ kubectl get deployment -n istio-ingress

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istio-ingressgateway   1/1     1            1           47s


▶︎ IstiodとIstio IngressGatewayを動かすNamespaceについて

セキュリティのベストプラクティスでは、IstiodとIstio IngressGatewayは異なるNamespaceで動かすことが推奨されています。

マイクロサービス

各Namespaceでマイクロサービスが動いています。

マイクロサービスのPodはistio-proxyコンテナを持ちます。

$ kubectl get deployment -n foo

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
foo    2/2     1            1           47s
...
$ kubectl get deployment -n bar

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
bar    2/2     1            1           47s
..
$ kubectl get deployment -n baz

NAME   READY   UP-TO-DATE   AVAILABLE   AGE
baz    2/2     1            1           47s
...


(1) アップグレード前の検証

ここで実施すること

アップグレード前に、現在のKubernetes Clusterがアップグレード要件を満たしているかを検証します。

istioctl x precheckコマンド

istioctl x precheckコマンドを実行し、アップグレード要件を検証します。

問題がなければ、istioctlコマンドはNo issue ...の文言を出力します。

$ istioctl x precheck

✅ No issues found when checking the cluster.Istiois safe to install or upgrade!
  To get started, check out https://istio.io/latest/docs/setup/getting-started/


▶︎ アップグレード要件が満たない場合について

もし、問題がある場合、istioctl x precheckコマンドはエラー文言を出力します。

例えば、Istioのistio-proxyコンテナのインジェクションではkube-apiserverと通信する必要があります。

そのため、kube-apiserverのバージョンが古すぎるせいでIstioが非対応であると、エラーになります😭

kubectl getコマンド

▼ IstiodのDeployment

kubectl getコマンドを実行し、現在のIstiodのバージョンを確認します👀

まずはIstiodのDeploymentを確認すると、1-14-6のDeploymentがあります。

$ kubectl get deployment -n istio-system -l app=istiod

NAME                   READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-14-6          1/1     1            1           47s # 1-14-6

istio-proxyコンテナのインジェクションの仕組みでいうと、以下の赤枠の要素です👇

istio_canary-upgrade_webhook_1-1

▼ Webhookの宛先のService

次に、 Serviceを確認すると、1-14-6のServiceがあります。

$ kubectl get service -n istio-system -l app=istiod

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
istiod-1-14-6   ClusterIP   10.96.93.151     <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   109s # 1-14-6

このServiceは、kube-apiserverからIstiodへのWebhookを仲介することにより、istio-proxyコンテナのインジェクションを可能にします。

istio-proxyコンテナのインジェクションの仕組みでいうと、以下の赤枠の要素です👇

istio_canary-upgrade_webhook_1-2

▼ 宛先のServiceを決めるMutatingWebhookConfiguration

最後に、MutatingWebhookConfigurationを確認すると、istio-revision-tag-<エイリアス>istio-sidecar-injector-<リビジョン番号>のMutatingWebhookConfigurationがあります。

$ kubectl get mutatingwebhookconfigurations

NAME                            WEBHOOKS   AGE
istio-revision-tag-default      2          114s  # カナリアアップグレード用
istio-sidecar-injector-1-14-6   2          2m16s # インプレースアップグレード用のため今回は言及しない

istio-proxyコンテナのインジェクションの仕組みでいうと、以下の赤枠の要素です👇

istio_canary-upgrade_webhook_1-3

これらのうち、前者 (istio-revision-tag-<エイリアス>) をカナリアアップグレードのために使用します。

このMutatingWebhookConfigurationは、Webhookの宛先のServiceを決めるため、結果的にistio-proxyコンテナのバージョンを決めます。

ここで、MutatingWebhookConfigurationのistio.io/revラベルとistio.io/tagラベルの値も確認しておきます。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.metadata.labels'

...

istio.io/rev: 1-14-6
istio.io/tag: default

...

istio.io/revラベルはIstiodのバージョン、istio.io/tagラベルはこれのエイリアスを表しています。

また、.webhooks[].namespaceSelectorキー配下のistio.io/revキーの検知ルールを確認します。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.webhooks[]'

...

namespaceSelector:
  matchExpressions:
    - key: istio.io/rev
      operator: In
      values:
        - default

...

合わせて、.webhooks[].clientConfig.serviceキー配下のServiceを名前を確認します。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.webhooks[].clientConfig'

...

service:
  name: istiod-1-14-6

...


▶︎ MutatingWebhookConfigurationの役割について

ここで、重要な仕組みをおさらいします。

Namespaceでistio.io/revラベルにdefaultを設定してあるとします。

すると、上記のMutatingWebhookConfigurationがこれを検知します。

MutatingWebhookConfigurationにはdefaultに対応するIstioのリビジョンが定義されており、kube-apiserverが特定のIstioのバージョンのServiceにWebhookを送信可能になります🎉


(2) 新Istiodのインストール

ここで実施すること

それでは、新Istiodをインストールします。

istioctl versionコマンド

新しくインストールするIstiodのバージョンは、istioctlコマンドのバージョンで決まります。

そこで、istioctl versionコマンドを実行し、これのバージョンを確認します。

$ istioctl version

client version: 1.15.4        # アップグレード先のバージョン
control plane version: 1.14.6 # 現在のバージョン
data plane version: 1.14.6

istioctl installコマンド

カナリアアップグレードの場合、istioctl installコマンドを実行します。

ドキュメントではrevisionキーの値がcanaryですが、今回は1-15-4とします。

この値は、Istioが使用する様々なKubernetesリソースの接尾辞や、各リソースのistio.io/revラベルの値になります。

$ istioctl install --set revision=1-15-4

WARNING: Istio is being upgraded from 1.14.6 -> 1.15.4
WARNING: Before upgrading, you may wish to use 'istioctl analyze' to check for IST0002 and IST0135 deprecation warnings.

✅ Istio core installed
✅ Istiod installed
✅ Ingress gateways installed
✅ Installation complete

Thank you for installing Istio 1.15.  Please take a few minutes to tell us about your install/upgrade experience!
▶︎ カナリアアップグレードで指定できるバージョン差について

revisionキーを使用したカナリアアップグレードでは、2つの先のマイナーバージョンまでアップグレードできます。

例えば、現在のIstioが1.14.6であるなら、1.16系まで対応しています👍

kubectl getコマンド

▼ IstiodのDeployment

kubectl getコマンドを実行し、istioctl installコマンドで何をインストールしたのかを確認します👀

まずはIstiodのDeploymentを確認すると、1-15-4というDeploymentが新しく増えています。

$ kubectl get deployment -n istio-system -l app=istiod

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-14-6   1/1     1            1           47s # 1-14-6
istiod-1-15-4   1/1     1            1           47s # 1-15-4

接尾辞の1-15-4は、revisionキーの値で決まります。

この段階では、旧Istiodと新Istioが並行的に稼働しており、kube-apiserverはまだ旧Istiodと通信しています

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_2-1

▼ Webhookの宛先のService

次に Webhookの宛先のServiceを確認すると、istiod-1-15-4というServiceが新しく増えています。

$ kubectl get service -n istio-system -l app=istiod

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
istiod-1-14-6   ClusterIP   10.96.93.151     <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   109s # 1-14-6
istiod-1-15-4   ClusterIP   10.104.186.250   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   87s  # 1-15-4

この段階では、まだWebhookの宛先はistiod-1-14-6のServiceです。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_2-2

▼ Webhookの宛先のServiceを決めるMutatingWebhookConfiguration

最後にMutatingWebhookConfigurationを確認すると、istio-sidecar-injector-1-15-4というMutatingWebhookConfigurationが新しく増えています。

$ kubectl get mutatingwebhookconfigurations

NAME                            WEBHOOKS   AGE
istio-revision-tag-default      2          114s  # カナリアアップグレードで使用する
istio-sidecar-injector-1-14-6   2          2m16s
istio-sidecar-injector-1-15-4   2          2m16s

カナリアアップグレードでは、istio-revision-tag-<エイリアス>のMutatingWebhookConfigurationを使用します。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_2-3


▶︎ アンインストールについて

実は、他にもインストールしているものがあるのですが、話をわかりやすくするために、今回は言及していません🙇🏻‍


(3) Webhookの宛先のServiceの変更

ここで実施すること

この手順では、エイリアスistio.io/tagラベルの値はそのままにしておき、一方でistio.io/revラベルの値を変更します。

さらに、Webhookの宛先のServiceを変更します。

istioctl tag setコマンド

istioctl tag setコマンドを実行し、istio.io/revラベルの値と宛先のServiceを変更します。

$ istioctl tag set default --revision 1-15-4 --overwrite

実行後に、もう一度MutatingWebhookConfigurationを確認すると、istio.io/revラベルの値が変わっています。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.metadata.labels'

...

istio.io/rev: 1-15-4
istio.io/tag: default

...

また、Webhookの宛先のServiceも変わっています。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml \
    | yq '.webhooks[].clientConfig'

...

service:
  name: istiod-1-15-4

...

これらにより、Webhookの宛先が 1-15-4 のService となります。

そのため、 1-15-4 の istio-proxy コンテナをインジェクションできる ようになります。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_3


(4) Istio IngressGatewayをインプレースアップグレード

ここで実施すること

Webhookの宛先が1-15-4のServiceに変わったところで、Istio IngressGatewayをインプレースアップグレードします。

kubectl rollout restartコマンド

kubectl rollout restartコマンドを実行し、Istio IngressGatewayをインプレースアップグレードします。

$ kubectl rollout restart deployment istio-ingressgateway-n istio-ingress

再作成したPodのイメージを確認してみると、istio-proxyコンテナを1-15-4にアップグレードできています。

$ kubectl get pod bar -n bar -o yaml | yq '.spec.containers[].image'

docker.io/istio/proxyv2:1.15.4 # istio-proxyコンテナ


▶︎ istioctl proxy-statusコマンドについて

kubectl getコマンドの代わりに、istioctl proxy-statusコマンドを使用して、アップグレードの完了を確認してもよいです。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_2


▶︎ Istio IngressGatewayの通信遮断について

Istio IngressGatewayのアップグレード時、マイクロサービスへのインバウンド通信が遮断されてしまうと思った方がいるかもしれません。

この点については、DeploymentがローリングアップデートでIstio IngressGatewayのPodを入れ替えるため、安心していただいて問題ありません🙆🏻‍


(5) 一部のNamespaceのistio-proxyコンテナをアップグレード

ここで実施すること

続けて、一部のNamespaceのistio-proxyコンテナをアップグレードします。

Podの再作成により、新Istiodのistio-proxyコンテナがインジェクションされるため。istio-proxyコンテナをアップグレードできます。

kubectl rollout restartコマンド

前提にあるように、Namespaceには foo bar baz があります。

kubectl rollout restartコマンドを実行し、baristio-proxyコンテナからアップグレードします。

$ kubectl rollout restart deployment bar -n bar

再作成したPodのイメージを確認してみると、istio-proxyコンテナを1-15-4にアップグレードできています。

$ kubectl get pod bar -n bar -o yaml | yq '.spec.containers[].image'

bar-app:1.0 # マイクロサービス
docker.io/istio/proxyv2:1.15.4 # istio-proxyコンテナ


▶︎ istioctl proxy-statusコマンドについて

kubectl getコマンドの代わりに、istioctl proxy-statusコマンドを使用して、アップグレードの完了を確認してもよいです。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_3


(6) ユーザの手を借りたテスト

ここで実施すること

Istioを部分的にアップグレードしたところで、アップグレードが完了したNamespaceをテストします。

ユーザーの手を借りて実地的にテストします (例:該当のエラーメトリクスが基準値を満たすか) 。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_4

もし問題が起こった場合

もし問題が起こった場合、1-14-6にダウングレードしていきます。

istioctl tag setコマンドを実行し、istio.io/revラベルの値を元に戻します。

$ istioctl tag set default --revision 1-14-6 --overwrite

その後、kubectl rollout restartコマンドの手順を実行し、istio-proxyコンテナをダウングレードしてきます。


(7) istio-proxyコンテナの段階的アップグレード

ここで実施すること

先ほどのNamespaceで問題が起こらなければ、残ったNamespace (foobaz、...) のistio-proxyコンテナも段階的にアップグレードしていきます。

kubectl rollout restartコマンド

同様にkubectl rollout restartコマンドを実行し、istio-proxyコンテナからアップグレードします。

$ kubectl rollout restart deployment foo -n foo

$ kubectl rollout restart deployment baz -n baz

...

最終的に、全てのNamespacemのistio-proxyコンテナが新しくなります。

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_5


(8) 旧Istiodのアンインストール

ここで実施すること

最後に、旧Istiodのアンインストールします。

istioctl uninstallコマンド

istioctl uninstallコマンドを実行し、旧Istiodをアンインストールします。

$ istioctl uninstall --revision 1-14-6

✅ Uninstall complete

今の状況は以下の通りです👇

istio_canary-upgrade_rollout-restart_6

kubectl getコマンド

▼ IstiodのDeployment

kubectl getコマンドを実行し、istioctl uninstallコマンドで何をアンインストールしたのかを確認します👀

まずはIstiodのDeploymentを確認すると、1-14-6というDeploymentが無くなっています。

$ kubectl get deployment -n istio-system -l app=istiod

NAME            READY   UP-TO-DATE   AVAILABLE   AGE
istiod-1-15-4   1/1     1            1           47s # 1-15-4

▼ Webhookの宛先のService

次に Webhookの宛先のServiceを確認すると、istiod-1-14-6というServiceが無くなっています。

$ kubectl get service -n istio-system -l app=istiod

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)                                 AGE
istiod-1-15-4   ClusterIP   10.104.186.250   <none>        15010/TCP,15012/TCP,443/TCP,15014/TCP   87s  # 1-15-4

▼ 宛先のServiceを決めるMutatingWebhookConfiguration

最後にMutatingWebhookConfigurationを確認すると、istio-sidecar-injector-1-14-6というMutatingWebhookConfigurationが無くなっています。

$ kubectl get mutatingwebhookconfigurations

NAME                            WEBHOOKS   AGE
istio-revision-tag-default      2          114s  # 次のカナリアアップグレードでも使用する
istio-sidecar-injector-1-15-4   2          2m16s

これで、新Istiodに完全に入れ替わったため、アップグレードは完了です。

今の状況は以下の通りです👇

istio_canary-upgrade_webhook_4


▶︎ アンインストールについて

実は、他にもアンインストールしているものがあるのですが、話をわかりやすくするために、今回は言及していません🙇🏻‍


06. おわりに

Istioを安全にアップグレードするカナリア方式とその仕組みをもりもり布教しました。

Istioへの愛が溢れてしまいました。

これからIstioを採用予定の方は、Istioを安全にアップグレードするために十分に準備しておくことをお勧めします👍


記事関連のおすすめ書籍


【Istio⛵️】サービスメッシュの登場経緯とIstioサイドカーインジェクションの仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

  • 代表的なサービスメッシュの種類について
  • Istioのサイドカーインジェクションの仕組みについて



01. はじめに


推し (Istio) が尊い🙏🙏🙏

istio-icon


さて、前回の記事の時と同様に、最近の業務でもオンプレとAWS上のIstio⛵️をひたすら子守りしています。

今回は、子守りの前提知識の復習もかねて、サービスメッシュを実装するIstioサイドカーインジェクションを記事で解説しました。

解説するのは、執筆時点 (2023/01/14) 時点で最新の 1.14 系のIstioです。

執筆時点 (2023/01/14) では、Istioが実装するサービメッシュには、『サイドカープロキシメッシュ』と『アンビエントメッシュ』があります。

サイドカープロキシメッシュの仕組みの軸になっているものは、サイドカーコンテナであるistio-proxyコンテナです。

Istioは、KubernetesのPodの作成時に、istio-proxyコンテナをPod内に自動的にインジェクション (注入) します

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. サービスメッシュが登場した経緯

なぜサービスメッシュが登場したのか

そもそも、なぜサービスメッシュが登場したのでしょうか。

マイクロサービスアーキテクチャのシステムには、アーキテクチャ固有のインフラ領域の問題 (例:サービスディスカバリーの必要性、マイクロサービス間通信の暗号化、テレメトリー作成、など) があります。

アプリエンジニアが各マイクロサービス内にインフラ領域の問題に関するロジックを実装すれば、これらの問題の解決できます。

service-mesh_layer

しかし、アプリエンジニアはアプリ領域の問題に責務を持ち、インフラ領域の問題はインフラエンジニアで解決するようにした方が、互いに効率的に開発できます。

そこで、インフラ領域の問題を解決するロジックをサイドカーとして切り分けます。

service-mesh_sidecar


これにより、アプリエンジニアとインフラエンジニアの責務を分離可能になり、凝集度が高くなります。

また、インフラ領域の共通ロジックをサイドカーとして各マイクロサービスに提供できるため、単純性が高まります。

こういった流れの中で、サービスメッシュが登場しました。


サイドカープロキシメッシュ

Istioのサイドカーによるサービスメッシュ (サイドカープロキシメッシュ) は、

  • サイドカーコンテナ (istio-proxyコンテナ) が稼働するデータプレーン
  • サイドカーを中央集権的に管理するIstiod (discoveryコンテナ) が稼働するコントロールプレーン

からなります。

istio_sidecar-mesh_architecture



03. admission-controllersアドオンについて

admission-controllersアドオンとは

IstioのPod内へのサイドカーインジェクションの前提知識として、admission-controllersアドオンを理解する必要があります。

もし、admission-controllersアドオンをご存知の方は、 04. サイドカーインジェクションの仕組み まで飛ばしてください🙇🏻‍

kube-apiserverでは、admission-controllersアドオンを有効化できます。

有効化すると、認証ステップと認可ステップの後にmutating-admissionステップとvalidating-admissionステップを実行でき、admissionプラグインの種類に応じた処理を挿入できます。

クライアント (kubectlクライアント、Kubernetesリソース) からのリクエスト (例:Kubernetesリソースに対する作成/更新/削除、kube-apiserverからのプロキシへの転送) 時に、各ステップでadmissionプラグインによる処理 (例:アドオンビルトイン処理、独自処理) を発火させられます。

kubernetes_admission-controllers_architecture


admissionプラグインの種類

admission-controllersアドオンのadmissionプラグインには、たくさんの種類があります。

IstioがPod内にサイドカーをインジェクションする時に使用しているアドオンは、『MutatingAdmissionWebhook』です。

  • CertificateApproval
  • CertificateSigning
  • CertificateSubjectRestriction
  • DefaultIngressClass
  • DefaultStorageClass
  • DefaultTolerationSeconds
  • LimitRanger
  • "MutatingAdmissionWebhook" 👈 これ
  • NamespaceLifecycle
  • PersistentVolumeClaimResize
  • PodSecurity
  • Priority
  • ResourceQuota
  • RuntimeClass
  • ServiceAccount
  • StorageObjectInUseProtection
  • TaintNodesByCondition
  • ValidatingAdmissionWebhook


MutatingAdmissionWebhookプラグイン

MutatingAdmissionWebhookプラグインとは

MutatingAdmissionWebhookプラグインを使用すると、mutating-admissionステップ時に、リクエスト内容を変更する処理をフックできます。

フックする具体的な処理として、webhookサーバーにAdmissionRequestリクエストとして送信することにより、レスポンスのAdmissionResponseに応じてリクエスト内容を動的に変更します。

MutatingWebhookConfigurationで、MutatingAdmissionWebhookプラグインの発火条件やwebhookサーバーの宛先情報を設定します。

MutatingWebhookConfigurationの具体的な実装については、サイドカーインジェクションの仕組みの中で説明していきます。

admission-controllers_mutating-admission

AdmissionReview、AdmissionRequest、AdmissionResponse

▼ AdmissionReview

AdmissionReviewは以下のようなJSONであり、kube-apiserverとwebhookサーバーの間でAdmissionRequestとAdmissionResponseを運びます。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  # AdmissionRequest
  "request": {},
  # AdmissionResponse
  "response": {},
}

▼ AdmissionRequest

AdmissionRequestは以下のようなJSONです。

kube-apiserverがクライアントから受信した操作内容が持つことがわかります。

例で挙げたAdmissionRequestでは、クライアントがDeploymentをCREATE操作するリクエストをkube-apiserverに送信したことがわかります。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  # AdmissionRequest
  "request": {

    ...

    # 変更されるKubernetesリソースの種類を表す。
    "resource": {
      "group": "apps",
      "version": "v1",
      "resource": "deployments"
    },
    # kube-apiserverの操作の種類を表す。
    "operation": "CREATE",

    ...

  }
}

▼ AdmissionResponse

一方でAdmissionResponseは、例えば以下のようなJSONです。

AdmissionResponseは、マニフェスト変更処理をpatchキーの値に持ち、これはbase64方式でエンコードされています。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  # AdmissionResponse
  "response": {
      "uid": "<value from request.uid>",
      # 宛先のwebhookサーバーが受信したか否かを表す。
      "allowed": true,
      # PathによるPatch処理を行う。
      "patchType": "JSONPatch",
      # Patch処理の対象となるKubernetesリソースと処理内容を表す。base64方式でエンコードされている。
      "patch": "W3sib3AiOiAiYWRkIiwgInBhdGgiOiAiL3NwZWMvcmVwbGljYXMiLCAidmFsdWUiOiAzfV0=",
    },
}

エンコード値をデコードしてみると、例えば以下のようなpatch処理が定義されています。

# patchキーをbase64方式でデコードした場合
[{"op": "add", "path": "/spec/replicas", "value": 3}]

マニフェストに対する操作 (op) 、キー (path) 、値 (value) が設定されています。

kube-apiserverがこれを受信すると、指定されたキー (.spec.replicas) に値 (3) に追加します。


04. サイドカーインジェクションの仕組み

全体のフロー

前提知識を踏まえた上で、admission-controllersアドオンの仕組みの中で、サイドカーistio-proxyコンテナがどのようにPodにインジェクションされるのかを見ていきましょう。

最初に、サイドカーインジェクションのフローは以下の通りになっています。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow


クライアント ➡︎ kube-apiserver

ここで説明するフロー箇所

『クライアント ➡︎ kube-apiserver』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_1

(1) Podの作成をリクエス

まずは、クライアントがkube-apiserverにリクエストを送信するところです。

クライアント (Deployment、DaemonSet、StatefulSet、を含む) は、Podの作成リクエストをkube-apiserverに送信します。

この時のリクエスト内容は、以下の通りとします。

# Podを作成する。
$ kubectl apply -f foo-pod.yaml
# foo-pod.yamlファイル
apiVersion: v1
kind: Pod
metadata:
  name: foo-pod
  namespace: foo-namespace
spec:
  containers:
    - name: foo
      image: foo:1.0.0
      ports:
        - containerPort: 80

またNamespaceでは、あらかじめistio-proxyコンテナのインジェクションが有効化されているとします。

Istioではv1.10以降、リビジョンの番号のエイリアスを使用して、istio-proxyコンテナのインジェクションを有効化するようになりました。

apiVersion: v1
kind: Namespace
metadata:
  name: foo-namespace
  labels:
    # istio-proxyコンテナのインジェクションを有効化する。
    # エイリアスは自由
    istio.io/rev: <エイリアス>


istio.io/revラベル値のエイリアスについて

istio.io/revラベル値は、どんなエイリアスでもよいです。

よくあるエイリアスとしてdefaultstableを使用します👍


kube-apiserver ➡︎ Service

ここで説明するフロー箇所

『kube-apiserver ➡︎ Service』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_2

(2) 認証/認可処理をコール

kube-apiserverは、認証ステップと認可ステップにて、クライアントからのリクエストを許可します。

(3) アドオンの処理をコール

kube-apiserverは、mutating-admissionステップにて、MutatingAdmissionWebhookプラグインの処理をコールします。

前提知識の部分で具体的な実装を省略しましたが、Istioのバージョン1.14.3時点で、MutatingWebhookConfigurationは以下のようになっています。

Namespaceでサイドカーインジェクションを有効化する時に使用したエイリアスは、このMutatingWebhookConfigurationで実体のリビジョン番号と紐づいています。

$ kubectl get mutatingwebhookconfiguration istio-revision-tag-default -o yaml
apiVersion: admissionregistration.k8s.io/v1beta1
kind: MutatingWebhookConfiguration
metadata:
  name: istio-revision-tag-default
  labels:
    app: sidecar-injector
    # エイリアスの実体
    istio.io/rev: <リビジョン番号>
    # リビジョン番号のエイリアス
    istio.io/tag: <エイリアス>
webhooks:
  - name: rev.namespace.sidecar-injector.istio.io
    # MutatingAdmissionWebhookプラグインの処理の発火条件を登録する。
    rules:
      - apiGroups: [""]
        apiVersions: ["v1"]
        operations: ["CREATE"]
        resources: ["pods"]
        scope: "*"
    # Webhookの前段にあるServiceの情報を登録する。
    clientConfig:
      service:
        name: istiod-<リビジョン番号>
        namespace: istio-system
        path: "/inject" # エンドポイント
        port: 443
      caBundle: Ci0tLS0tQk ...
    # Namespace単位のサイドカーインジェクション
    # 特定のNamespaceでMutatingAdmissionWebhookプラグインの処理を発火させる。
    namespaceSelector:
      matchExpressions:
        - key: istio.io/rev
          operator: DoesNotExist
        - key: istio-injection
          operator: DoesNotExist
    # Pod単位のサイドカーインジェクション
    # 特定のオブジェクトでMutatingAdmissionWebhookプラグインの処理を発火させる。
    objectSelector:
      matchExpressions:
        - key: sidecar.istio.io/inject
          operator: NotIn
          values:
            - "false"
        - key: istio.io/rev
          operator: In
          values:
            - <エイリアス>

    ...

MutatingWebhookConfigurationには、MutatingAdmissionWebhookプラグインの発火条件やwebhookサーバーの宛先情報を定義します。

MutatingAdmissionWebhookプラグインの発火条件に関して、例えばIstioでは、 NamespaceやPod.metadata.labelsキーに応じてサイドカーインジェクションの有効化/無効化を切り替えることができ、これをMutatingAdmissionWebhookプラグインで制御しています。

webhookサーバーの宛先情報に関して、Istioではwebhookサーバーの前段にServiceを配置しています。

istio_admission-controllers_mutating-admission


MutatingAdmissionWebhookプラグインが発火した場合、Serviceの/inject:443HTTPSプロトコルのリクエストを送信するようになっています。

また、宛先のServiceの名前がistiod-<リビジョン番号>となっていることからもわかるように、Serviceは特定のバージョンのIstiodコントロールプレーンに対応しており、想定外のバージョンのIstiodコントロールプレーンを指定しないように制御しています。

一方で発火しなかった場合には、以降のAdmissionReviewの処理には進みません。

(4) AdmissionRequestに値を詰める

kube-apiserverは、mutating-admissionステップにて、クライアントからのリクエスト内容 (Podの作成リクエスト) をAdmissionReveiew構造体のAdmissionRequestに詰めます。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  # AdmissionRequest
  "request": {

    ...

    # 変更されるKubernetesリソースの種類を表す。
    "resource": {
      "group": "core",
      "version": "v1",
      "resource": "pods"
    },
    # kube-apiserverの操作の種類を表す。
    "operation": "CREATE",

    ...

  }
}

(5) AdmissionReviewを送信

kube-apiserverは、mutating-admissionステップにて、Serviceの/inject:443にAdmissionReview構造体を送信します。


Service ➡︎ webhookサーバー

ここで説明するフロー箇所

『Service ➡︎ webhookサーバー』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_3

(6) 15017番ポートにポートフォワーディング

Serviceは、/inject:443でリクエストを受信し、discoveryコンテナの15017番ポートにポートフォワーディングします。

istio_admission-controllers_mutating-admission


Istioのバージョン1.14.3時点で、Serviceは以下のようになっています。

$ kubectl get svc istiod-service -n istio-system -o yaml
apiVersion: v1
kind: Service
metadata:
  labels:
    app: istiod
  name: istiod-<リビジョン番号>
  namespace: istio-system
spec:
  type: ClusterIP
  selector:
    app: istiod
    istio.io/rev: <リビジョン番号>
  ports:
    - name: grpc-xds
      port: 15010
      protocol: TCP
      targetPort: 15010
    - name: https-dns
      port: 15012
      protocol: TCP
      targetPort: 15012
    # webhookサーバーにポートフォワーディングする。
    - name: https-webhook
      port: 443
      protocol: TCP
      targetPort: 15017
    - name: http-monitoring
      port: 15014
      protocol: TCP
      targetPort: 15014

.spec.selector.istio.io/revキーに、ポートフォワーディング先のPodを指定するためのリビジョン番号が設定されており、このPodはdiscoveryコンテナを持ちます。

Istioは、discoveryコンテナ内でwebhookサーバーを実行し、15017番ポートでリクエストを待ち受けます。


istio.io/rev`discovery`コンテナの待ち受けポートについて

ここで、discoveryコンテナがリクエストを待ち受けているポート番号を見てみると、15017番ポートでリッスンしていることを確認できます👍
$ kubectl exec foo-istiod -n istio-system -- netstat -tulpn

Active Internet connections (only servers)

Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name
tcp        0      0 127.0.0.1:9876          0.0.0.0:*               LISTEN      1/pilot-discovery
tcp6       0      0 :::15017                :::*                    LISTEN      1/pilot-discovery
tcp6       0      0 :::8080                 :::*                    LISTEN      1/pilot-discovery
tcp6       0      0 :::15010                :::*                    LISTEN      1/pilot-discovery
tcp6       0      0 :::15012                :::*                    LISTEN      1/pilot-discovery
tcp6       0      0 :::15014                :::*                    LISTEN      1/pilot-discovery
> - istio/pkg/kube/inject/webhook.go at 1.14.3 · istio/istio · GitHub > - Istio / Application Requirements


kube-apiserver ⬅︎ Service ⬅︎ webhookサーバー (※逆向きの矢印)

ここで説明するフロー箇所

『kube-apiserver ⬅︎ Service ⬅︎ webhookサーバー』の箇所を説明します。

矢印が逆向きなことに注意してください。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_4

(7) patch処理を定義

仕組みの中でも、ここは重要な部分です。

discoveryコンテナ内のwebhookサーバーは、リクエスト内容を書き換えるためのpatch処理を定義します。

webhookサーバーは、マニフェスト.spec.containers[1]パスにistio-proxyキーを追加させるようなpatch処理を定義します。

この定義によって、結果的にサイドカーのインジェクションが起こるということになります。

[

  ...

  {
    "op": "add",
    # .spec.initContainers[1] を指定する。
    "path": "/spec/initContainers/1",
    # マニフェストに追加される構造を表す。
    "value": {
      "name": "istio-init",
      "resources": {
                     ...
      }
    }
  },
  {
    "op": "add",
    # .spec.containers[1] を指定する。
    "path": "/spec/containers/1",
    # マニフェストに追加される構造を表す。
    "value": {
      "name": "istio-proxy",
      "resources": {
                     ...
      }
    }
  }

  ...

]

この時、サイドカーのテンプレートに割り当てられた値が、patch処理を内容を決めます。

...

type SidecarTemplateData struct {
    TypeMeta             metav1.TypeMeta
    DeploymentMeta       metav1.ObjectMeta
    ObjectMeta           metav1.ObjectMeta
    Spec                 corev1.PodSpec
    ProxyConfig          *meshconfig.ProxyConfig
    MeshConfig           *meshconfig.MeshConfig
    Values               map[string]interface{}
    Revision             string
    EstimatedConcurrency int
    ProxyImage           string
}

...


▶ patch処理でインジェクションするコンテナについて

本記事では詳しく言及しませんが、上記のpatch処理ではサイドカーコンテナのistio-proxyコンテナの他に、InitContainerのistio-initコンテナもインジェクション可能にします。

このistio-initコンテナは、istio-proxyコンテナを持つPodです。

インバウンド/アウトバウンド通信の経路を制御するために、Pod内にiptablesのルールを適用する責務を担っています💪🏻

(8) AdmissionResponseに値を詰める

discoveryコンテナ内のwebhookサーバーは、patch処理の定義をAdmissionReveiew構造体のAdmissionResponseに詰めます。

patchキーの値に、先ほどのpatch処理の定義をbase64方式でエンコードした文字列が割り当てられています。

{
  "apiVersion": "admission.k8s.io/v1",
  "kind": "AdmissionReview",
  # AdmissionResponse
  "response": {
      "uid": "*****",
      "allowed": true,
      "patchType": "JSONPatch",
      # Patch処理の対象となるKubernetesリソースと処理内容を表す。base64方式でエンコードされている。
      "patch": "<先ほどのpatch処理の定義をbase64方式でエンコードした文字列>",
    },
}

(9) AdmissionReviewを返信

discoveryコンテナ内のwebhookサーバーは、AdmissionReview構造体をレスポンスとしてkube-apiserverに返信します。


kube-apiserver ➡︎ etcd

ここで説明するフロー箇所

『kube-apiserver ➡︎ etcd』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_5

(10) patch処理をコール

kube-apiserverは、AdmissionReview構造体を受信し、AdmissionResponseに応じてリクエスト内容を書き換えます。

patch処理の定義をAdmissionReview構造体から取り出し、クライアントからのリクエスト内容を書き換えます。

具体的には、istio-proxyコンテナとistio-initコンテナを作成するために、リクエストしたマニフェストの該当箇所にキーを追加します。

apiVersion: v1
kind: Pod
metadata:
  name: foo-pod
  namespace: foo-namespace
spec:
  containers:
    - name: foo
      image: foo:1.0.0
      ports:
        - containerPort: 80
    # kube-apiserverが追加
    - name: istio-proxy

      ...

  # kube-apiserverが追加
  initContainers:
    - name: istio-init

    ...

(11) マニフェストを永続化

kube-apiserverは、etcdにPodのマニフェストを永続化します。


クライアント ⬅︎ kube-apiserver

ここで説明するフロー箇所

『クライアント ⬅︎ kube-apiserver』の箇所を説明します。

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_6

(12) コール完了を返信

kube-apiserverは、クライアントにレスポンスを受信します。

$ kubectl apply -f foo-pod.yaml

# kube-apiserverからレスポンスが返ってくる
pod "foo-pod" created


以降の仕組み

(画像はタブ開き閲覧を推奨)

istio_container-injection_flow_red_7


kube-apiserverは、他のNodeコンポーネント (kube-controlleretcd、kube-scheduler、kubelet、など) と通信し、Podを作成します。

このPodのマニフェストは、アプリコンテナの他に、istio-proxyコンテナとistio-initコンテナを持ちます。

結果として、サイドカーコンテナのistio-proxyコンテナをインジェクションしたことになります。


▶ kube-apiserverと他コンポーネントの通信について

本記事では詳しく言及しませんが、kube-apiserverと他コンポーネントの通信については、以下の記事が非常に参考になりました🙇🏻‍



05. おわりに

サービスメッシュの登場とIstioのサイドカーインジェクションの仕組みをもりもり布教しました。

Istioへの愛が溢れてしまいました。

今回登場したMutatingAdmissionWebhookプラグインに関して、私の関わっているプロダクトではIstio以外 (例:CertManager、Prometheus、AWSaws-eks-vpc-cniアドオン、など) でも使用しています✌️

そのため、MutatingAdmissionWebhookプラグインをどのように使っているのかを一度知れば、知識の汎用性が高いと考えています。

サイドカーインジェクションはIstioでも基本的な機能であり、もし未体験の方がいらっしゃれば、お手元でサイドカーコンテナが追加されることを確認していただくとよいかもしれません👍


記事関連のおすすめ書籍


【Istio⛵️】Istioのサービス間通信を実現するサービスディスカバリーの仕組み


この記事から得られる知識

この記事を読むと、以下を "完全に理解" できます✌️

記事のざっくりした内容は、以下のスライドからキャッチアップできちゃいます!



01. はじめに


推し (Istio) が尊い🙏🙏🙏

istio-icon

3-shake Advent Calender 2022 最終日の記事です🎅

普段、私は 俺の技術ノート に知見を記録しており、はてなブログはデビュー戦となります。

最近の業務で、オンプレとAWS上のIstio⛵️をひたすら子守りしています。

今回は、子守りの前提知識の復習もかねて、Istioのサービス間通信を実現するサービスディスカバリーの仕組みを記事で解説しました。

Istioの機能の1つであるサービスディスカバリーは、その仕組みの多くをEnvoyに頼っているため、合わせてEnvoyの仕組みも説明します。

それでは、もりもり布教していきます😗

記事中のこのボックスは、補足情報を記載しています。

飛ばしていただいても大丈夫ですが、読んでもらえるとより理解が深まるはずです👍


02. サービスディスカバリーについて

マイクロサービスアーキテクチャにおけるサービスディスカバリー

サービスディスカバリーとは

平易な言葉で言い換えると サービス間通信 です。

マイクロサービスアーキテクチャでは、マイクロサービスからマイクロサービスにリクエストを送信する場面があります。

サービスディスカバリーとは、宛先マイクロサービスの宛先情報 (例:IPアドレス、完全修飾ドメイン名、など) を検出し、送信元マイクロサービスが宛先マイクロサービスにリクエストを継続的に送信可能にする仕組みのことです。

service-discovery.png

なぜサービスディスカバリーが必要なのか

そもそも、なぜサービスディスカバリーが必要なのでしょうか。

マイクロサービスアーキテクチャでは、システムの信頼性 (定められた条件下で定められた期間にわたり、障害を発生させることなく実行する程度) を担保するために、マイクロサービスのインスタンスの自動スケーリングを採用します。

この時、自動スケーリングのスケールアウトでマイクロサービスが増加するたびに、各インスタンスには新しい宛先情報が割り当てられてしまいます。

また、マイクロサービスが作り直された場合にも、宛先情報は更新されてしまいます。

このように、たとえインスタンスの宛先情報が更新されたとしても、インスタンスへのリクエストに失敗しない仕組みが必要です。

サービスディスカバリーの要素

サービスディスカバリーの仕組みは、次の要素からなります。

名前解決は、DNSベースのサービスディスカバリー (例:CoreDNS + Service + kube-proxyによるサービスディスカバリー) で必要となり、Istioでは使いません。

そのため、本記事では言及しないこととします🙇🏻‍

service-discovery-pattern.png

要素 責務
送信元マイクロサービス リクエストを送信する。
宛先マイクロサービス リクエストを受信する。
サービスレジストリ 宛先マイクロサービスの宛先情報を保管する。
ロードバランサー 宛先マイクロサービスのインスタンスにロードバランシングする。
名前解決 宛先マイクロサービスへのリクエスト送信時に、名前解決可能にする。


サービスディスカバリーのパターン

サービスディスカバリーのパターンとは

サービスディスカバリーの仕組みにはいくつか種類があります。

Istioのサービスディスカバリーは、このうちのサーバーサイドパターンを実装したものになります。

サーバーサイドパターン

service-discovery-pattern_client-side.png

送信元マイクロサービスから、問い合わせとロードバランシングの責務が切り離されています。

送信元マイクロサービスは、ロードバランサーにリクエストを送信します。

ロードバランサーは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、またリクエストをロードバランシングする責務を担っています💪🏻

(例) Istio、Linkerd、など

クライアントサイドパターン

service-discovery-pattern_server-side.png

通信の送信元マイクロサービスは、宛先マイクロサービスの宛先をサービスレジストリに問い合わせ、さらにロードバランシングする責務を担います。

(例) NetflixのEureka、など


03. Istioのサービスディスカバリーの仕組み

Istioが実装するサービスメッシュには、サイドカープロキシメッシュとアンビエントメッシュがあり、今回はサイドカープロキシメッシュのサービスディスカバリーを取り上げます。

Istioのサービスディスカバリーは、discoveryコンテナとistio-proxyコンテナが軸となり、サーバーサイドパターンのサービスディスカバリーを実装します。

全体像

(1) 〜 (6) の全体像は、以下の通りです👇

istio-proxyコンテナは、サービスレジストリへの問い合わせと、ロードバランシングする責務を担っていることに注目してください。

service-discovery_istio.png

(1) kube-apiserverによる宛先情報保管

kube-apiserverは、Pod等の宛先情報をetcd等に保管します。

これは、Kubernetesの通常の仕組みです。

(2) discoveryコンテナによる宛先情報保管

discoveryコンテナは、kube-apiserverからPod等の宛先情報を取得し、自身に保管します。

(3) istio-proxyコンテナによる宛先情報取得

istio-proxyコンテナは、discoveryコンテナからPod等の宛先情報を双方向ストリーミングRPCで取得します。

(4) istio-proxyコンテナによるリクエスト受信

送信元マイクロサービスがリクエストを送信します。

サーバーサイドパターンでの責務通り、送信元マイクロサービスはロードバランサー (ここではistio-proxyコンテナ) にリクエストを送信します。

この時、送信元マイクロサービスがistio-proxyコンテナに直接的にリクエストを送信しているというよりは、iptablesistio-proxyコンテナにリクエストをリダイレクトします。

istio-proxyコンテナこれを受信します。

(5) istio-proxyコンテナによるロードバランシング

istio-proxyコンテナは、リクエストをロードバランシングし、また宛先Podに送信します。


discoveryコンテナの仕組み

全体像の中から、discoveryコンテナを詳しく見てみましょう。

discoveryコンテナは、別名Istiodと呼ばれています。

XDS-APIというエンドポイントを公開しており、XDS-APIのうち、サービスディスカバリーに関係するAPIは以下の通りです。

今回は詳しく言及しませんが、istio-proxyコンテナがHTTPSリクエストを処理するために、証明書を配布するためのSDS-APIもあります。

APIの種類 説明
LDS-API Envoyのリスナーを取得できる。
RDS-API Envoyのルートを取得できる。
CDS-API Envoyのクラスターを取得できる。
EDS-API Envoyのエンドポイントできる。
ADS-API 各XDS-APIから取得できる宛先情報を整理して取得できる。

service-discovery_xds-api.png

(1) kube-apiserverによる宛先情報保管

kube-apiserverによる宛先情報保管 と同じです。

(2) discoveryコンテナによる宛先情報保管

discoveryコンテナによる宛先情報保管 と同じです。

(3) istio-proxyコンテナによる宛先情報取得

XDS-APIistio-proxyコンテナの間では、gRPCの双方向ストリーミングRPCの接続が確立されています。

そのため、istio-proxyコンテナからのリクエストに応じて宛先情報を返却するだけでなく、リクエストがなくとも、XDS-APIからもistio-proxyコンテナに対して宛先情報を送信します。

XDS-APIのエンドポイントがいくつかあり、各エンドポイントから宛先情報を取得できます。

一方で、各エンドポイントからバラバラに宛先情報を取得すると、Envoy上でこれを整理する時に、宛先情報のバージョンの不整合が起こる可能性があります。

そのため、Istioは実際にはADS-APIを使用して宛先情報を取得します。


istio-proxyコンテナの仕組み

全体像の中から、istio-proxyコンテナを詳しく見てみましょう。

service-discovery_xds-api.png

(1) kube-apiserverによる宛先情報保管

kube-apiserverによる宛先情報保管 と同じです。

(2) discoveryコンテナによる宛先情報保管

discoveryコンテナによる宛先情報保管 と同じです。

(3) istio-proxyコンテナによる宛先情報取得

istio-proxyコンテナでは、pilot-agentとEnvoyが稼働しています。

先ほどistio-proxyコンテナは、双方向ストリーミングRPCでADS-APIから宛先情報を取得すると説明しました。

厳密にはEnvoyが、pilot-agentを介して、ADS-APIから双方向ストリーミングRPCで宛先情報を取得します。

(4) istio-proxyコンテナによるリクエスト受信

istio-proxyコンテナによるリクエスト受信 と同じです。

(5) istio-proxyコンテナによるリクエスト受信

EnvoyはADS-APIから取得した宛先情報に基づいて、宛先マイクロサービスのインスタンスにロードバランシングします。


04. istio-proxyコンテナ内のEnvoyの仕組み

全体像

EnvoyがADS-APIから取得した宛先情報を見ていく前に、Envoyの処理の流れを解説します。

istio-proxyコンテナ内のEnvoyでは、以下の仕組みでHTTPリクエストを処理します。

(1) 〜 (6) の全体像は、以下の通りです👇

service-discovery_envoy.png

(1) 送信元マイクロサービスからリクエスト受信

istio-proxyコンテナは、送信元マイクロサービスからリクエストを受信します。

(2) Envoyによるリスナー選択

Envoyは、リクエストの宛先情報 (例:宛先IPアドレス、ポート番号、パス、ホスト、など) に応じてリスナーを選びます。

(3) Envoyによるルート選択

Envoyは、リスナーに紐づくルートを選びます。


TCPリクエストを処理する場合について

HTTPリクエストを処理する場合、リスナーに紐づくのはルートですが、TCPリクエストの場合はそうではありません。

TCPリクエストを処理する場合、リスナーにクラスターが紐づきます👍🏻

(4) Envoyによるクラスター選択

Envoyは、クラスターに紐づくクラスターを選びます。

(5) Envoyによるエンドポイント選択

Envoyは、クラスターに紐づくエンドポイントを選びます。

(6) 宛先マイクロサービスへのリクエスト送信

Envoyは、エンドポイントに対応するインスタンスにリクエストを送信します。

Envoyで確認した宛先情報を👆に当てはめて見ていくことにしましょう。


EnvoyがADS-APIから取得した宛先情報を見てみよう

config_dumpエンドポイント

実際にEnvoyに登録されている宛先情報は、istio-proxyコンテナ自体のlocalhost:15000/config_dumpからJSON形式で取得できます。

もしお手元にIstioがある場合は、Envoyにどんな宛先情報が登録されているか、Envoyを冒険してみてください。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump" | yq -P


▶ 宛先情報を見やすくするyqコマンドについて

Envoyは、JSON形式で設定を出力します。

JSONだと見にくいため、yqコマンドでYAMLに変換すると見やすくなります👍

リスナー

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したリスナーは、/config_dump?resource={dynamic_listeners}から確認できます。

ここでは、foo-pod内でbar-podのリスナーを確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_listeners}" | yq -P

▼ 結果

以下を確認できました。

  • 宛先IPアドレスや宛先ポート番号に応じてリスナーを選べるようになっており、ここでは<任意のIPアドレス>:50002
  • リスナーに紐づくルートの名前
configs:
  - "@type": type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener
    # リスナー名
    name: 0.0.0.0_50002
    active_state:
      version_info: 2022-11-24T12:13:05Z/468
      listener:
        "@type": type.googleapis.com/envoy.config.listener.v3.Listener
        name: 0.0.0.0_50002
        address:
          socket_address:
            # 受信したパケットのうちで、宛先IPアドレスでフィルタリング
            address: 0.0.0.0
            # 受信したパケットのうちで、宛先ポート番号でフィルタリング
            port_value: 50002
        filter_chains:
          - filter_chain_match:
              transport_protocol: raw_buffer
              application_protocols:
                - http/1.1
                - h2c
            filters:
              - name: envoy.filters.network.http_connection_manager
                typed_config:
                  "@type": type.googleapis.com/envoy.extensions.filters.network.http_connection_manager.v3.HttpConnectionManager
                  stat_prefix: outbound_0.0.0.0_50001
                  rds:
                    config_source:
                      ads: {}
                      initial_fetch_timeout: 0s
                      resource_api_version: V3
                    # 本リスナーに紐づくルートの名前
                    route_config_name: 50002
  ...

  - "@type": type.googleapis.com/envoy.admin.v3.ListenersConfigDump.DynamicListener

  ...

ルート

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したリスナーは、/config_dump?resource={dynamic_route_configs}から確認できます。

ここでは、foo-pod内でbar-podのルートを確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_route_configs}" | yq -P

▼ 結果

コマンドを実行するとYAMLを取得でき、以下を確認できました。

  • リスナーを取得した時に確認できたルートの名前
  • リクエストのパスやHostヘッダーに応じてルートを選べるようになっている
  • ルートに紐づくクラスターの名前
configs:
  - "@type": type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig
    version_info: 2022-11-24T12:13:05Z/468
    route_config:
      "@type": type.googleapis.com/envoy.config.route.v3.RouteConfiguration
      # ルートの名前
      name: 50002
      virtual_hosts:
        - name: bar-service.bar-namespace.svc.cluster.local:50002
          # ホストベースルーティング
          domains:
            - bar-service.bar-namespace.svc.cluster.local
            - bar-service.bar-namespace.svc.cluster.local:50002
            - bar-service
            - bar-service:50002
            - bar-service.bar-namespace.svc
            - bar-service.bar-namespace.svc:50002
            - bar-service.bar-namespace
            - bar-service.bar-namespace:50002
            - 172.16.0.2
            - 172.16.0.2:50002
          routes:
            - match:
                # パスベースルーティング
                prefix: /
              route:
                # 本ルートに紐づくクラスターの名前
                cluster: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
                timeout: 0s
                retry_policy:
                  retry_on: connect-failure,refused-stream,unavailable,cancelled,retriable-status-codes
                  num_retries: 2
                  retry_host_predicate:
                    - name: envoy.retry_host_predicates.previous_hosts
                  host_selection_retry_max_attempts: "5"
                  retriable_status_codes:
                    - 503
                max_stream_duration:
                  max_stream_duration: 0s
                  grpc_timeout_header_max: 0s
              decorator:
                operation: bar-service.bar-namespace.svc.cluster.local:50002/*

  ...

  - '@type': type.googleapis.com/envoy.admin.v3.RoutesConfigDump.DynamicRouteConfig

  ...

クラスタ

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したクラスターは、/config_dump?resource={dynamic_active_clusters}から確認できます。

ここでは、foo-pod内でbar-podのクラスターを確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?resource={dynamic_active_clusters}" | yq -P

▼ 結果

コマンドを実行するとYAMLを取得でき、以下を確認できました。

  • ルートを取得した時に確認できたクラスターの名前
  • クラスターに紐づくエンドポイントの親名
configs:
  - "@type": type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster
    version_info: 2022-11-24T12:13:05Z/468
    cluster:
      "@type": type.googleapis.com/envoy.config.cluster.v3.Cluster
      # クラスターの名前
      name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
      type: EDS
      eds_cluster_config:
        eds_config:
          ads: {}
          initial_fetch_timeout: 0s
          resource_api_version: V3
        # 本クラスターに紐づくエンドポイントの親名
        service_name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
  ...

  - "@type": type.googleapis.com/envoy.admin.v3.ClustersConfigDump.DynamicCluster

  ...

エンドポイント

▼ 確認方法

istio-proxyコンテナがADS-APIから取得したクラスターは、/config_dump?include_edsから確認できます。

ここでは、foo-pod内でbar-podのクラスターを確認したと仮定します。

$ kubectl exec \
    -it foo-pod \
    -n foo-namespace \
    -c istio-proxy \
    -- bash -c "curl http://localhost:15000/config_dump?include_eds" | yq -P

▼ 結果

コマンドを実行するとYAMLを取得でき、以下を確認できました。

  • クラスターを取得した時に確認できたエンドポイントの親名
  • bar-podのインスタンス3個あるため、3個のエンドポイントがあります
configs:
  dynamic_endpoint_configs:
    - endpoint_config:
        "@type": type.googleapis.com/envoy.config.endpoint.v3.ClusterLoadAssignment
        # エンドポイントの親名
        cluster_name: outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local
        endpoints:
          - locality:
              region: ap-northeast-1
              zone: ap-northeast-1a
            lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 冗長化されたbar-podのIPアドレス
                      address: 11.0.0.1
                      # bar-pod内のコンテナが待ち受けているポート番号
                      port_value: 50002
                  health_check_config: {}
                health_status: HEALTHY
                metadata:
                  filter_metadata:
                    istio:
                      workload: bar
                    envoy.transport_socket_match:
                      tlsMode: istio
                # ロードバランシングアルゴリズムを決める数値
                load_balancing_weight: 1
          - locality:
              region: ap-northeast-1
              zone: ap-northeast-1d
            lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 冗長化されたbar-podのIPアドレス
                      address: 11.0.0.2
                      # bar-pod内のコンテナが待ち受けているポート番号
                      port_value: 50002
                  health_check_config: {}
                health_status: HEALTHY
                metadata:
                  filter_metadata:
                    istio:
                      workload: bar
                    envoy.transport_socket_match:
                      tlsMode: istio
                # ロードバランシングアルゴリズムを決める数値
                load_balancing_weight: 1
          - locality:
              region: ap-northeast-1
              zone: ap-northeast-1d
            lb_endpoints:
              - endpoint:
                  address:
                    socket_address:
                      # 冗長化されたbar-podのIPアドレス
                      address: 11.0.0.3
                      # bar-pod内のコンテナが待ち受けているポート番号
                      port_value: 50002
                  health_check_config: {}
                health_status: HEALTHY
                metadata:
                  filter_metadata:
                    istio:
                      workload: bar
                    envoy.transport_socket_match:
                      tlsMode: istio
                # ロードバランシングアルゴリズムを決める数値
                load_balancing_weight: 1
        policy:
          overprovisioning_factor: 140

    ...

    - endpoint_config:

    ...


▶ Envoyの負荷分散方式について

全てのエンドポイントのload_balancing_weightキー値が等しい場合、EnvoyはP2Cアルゴリズムに基づいてロードバランシングします👍


Envoyの処理の流れのまとめ

確認できた宛先情報を、Envoyの処理の流れに当てはめてみました。

service-discovery_envoy_detail.png

(1) 送信元マイクロサービスからリクエスト受信

送信元マイクロサービスは、宛先マイクロサービス (<任意のIP>/:50002) にリクエストを送信します。

サイドカーコンテナのistio-proxyコンテナはこれを受信します。

(2) Envoyによるリスナー選択

Envoyは、リクエストの宛先 (IPアドレス、ポート番号、パス) からPodのリスナー (0.0.0.0_50002) を選びます。

(3) Envoyによるルート選択

Envoyは、リスナーに紐づくPodのルート (50002) を選びます。

(4) Envoyによるクラスター選択

Envoyは、クラスターに紐づくPodのクラスター (outbound|50002|v1|bar-service.bar-namespace.svc.cluster.local) を選びます。

(5) Envoyによるクラスター選択

Envoyは、クラスターに紐づくPodのインスタンスのエンドポイント (11.0.0.X/:50002) を選びます。

(6) 宛先マイクロサービスへのリクエスト送信

Envoyは、エンドポイントの宛先にPodのリクエストを送信します。

サービスディスカバリーの冒険は以上です⛵


05. おわりに

Istioの機能の1つである『サービスディスカバリー』の仕組みを、Envoyを交えながらもりもり布教しました。

愛が溢れてしまいました。

Istioの機能を1つとっても、複雑な仕組みで実現していることがお分かりいただけたかと思います。

Istioありがとう🙏🙏🙏


謝辞

3-shake SRE Tech Talk での発表前後に、以下の方々に発表内容について助言をいただきました。

(アルファベット順)

また、今回の 3-shake Advent Calender 2022 は、以下の方々に企画いただきました。

(アルファベット順)

皆様に感謝申し上げます🙇🏻‍


記事関連のおすすめ書籍