Kubernetes、Istio、Helm、Prometheus、Flux、Gatekeeper、GKE。全部、さわる。

2026/05/27に公開されました。
2026/05/27に更新されました。

Kubernetesの基本からIstio・Helm・Prometheus・Flux・Gatekeeperなどの周辺ツール、GKEまでをハンズオン形式で概観します。


Table of contents


author: Shintaro

この記事の概要と想定読者

この記事ではKubernetesの基本的な仕組みと、それを取り巻く概念 (サービスメッシュやGitOpsなど) やツール (IstioやHelm、Prometheusなど)、そしてGKEを手を動かしながら学びます。基本的にはハンズオン形式でボトムアップに進めますが、概念的な解説との往復で手触り感を伴って理解が進むよう意識しています (完全に個人的な学習スタイルの好みです)。大枠としては以下の流れで進みます。

  1. Kubernetesの基本 (そもそもコンテナとは何なのかなどにも触れつつ概要レベルの解説をします)
  2. Kubernetesの仕組み (ローカルで動かして基本的な機能を確認します)
  3. サービスメッシュとIstioなど、Kubernetesに関連する用語とツールの関係性
  4. IstioやHelm、Prometheusなどのツール (ローカルで動かします)
  5. GKEをはじめとしたGoogle CloudマネージドのKubernetesと関連のサービス (動かします)

「Kubernetes入門」でググったりZennの記事を探してみても、「コントロールプレーン」や「Pod」などの用語にちょっとしたYAMLファイルが添えられた程度の解説か、ほぼハンズオンのみの記事が中心でした。具体と抽象を往復しつつ程よく手触り感が得られる、自分にとって簡単すぎず難しすぎない単独記事は見つけられませんでした。加えて、Istio / Helm / Prometheus / Flux / Gatekeeperといった周辺ツールやGKEなどのマネージドサービスまで含めて「1記事で全体を概観できる」記事も自分の探した範囲では見当たりませんでした。そこで、勉強がてら自分で書いてみました。想定読者は以下のような方 (=記事を書く前の自分です) です。

  • 「コントロールプレーン」、「ノード」など、単語やアーキテクチャ図は何度か見たことはあるが、Kubernetesが結局なんなのかよくわかっていない方
  • 手を動かしてKubernetesの手触り感を得たい方
  • Kubernetesのみならず、関連する用語やツールまで概要レベルで理解したい方

なお、本記事のサンプル構成・コマンドは学習・検証目的であり、本番レベルではありません。本番採用にあたってはRBAC / NetworkPolicy / Secret管理 / HA / 監視・アラート / バックアップなど、本記事で省略している観点を別途検討する必要があります。

本記事の表記ルール

用語の使い分け

記事を書いていて自分でも混乱したので、言葉の使い方についても最初に定義しておきます。この記事ではKubernetesの公式ドキュメントに基づき、以下のように使い分けます。12345

用語何を表現するか本記事での表記方針
リソースPod / Service など API で扱う対象原則この意味で使う。CPU / メモリ / requests / limits などを指すときは、必要に応じて計算リソースと明示する
コンポーネントクラスタを構成する実装要素 (kube-apiserver / etcd / kube-scheduler / kubelet など)コントロールプレーンやノード側の構成要素を指す語として使う
オブジェクトクラスタの望ましい状態を表す永続エンティティ (Pod / Deployment など)マニフェストで作成・管理する対象を指す語として使う

コマンド出力結果など

この記事ではコマンドの出力結果も記載していきますが、その中のAGEやリソース名の末尾にランダム生成されるsuffixなどは文脈上で必要な場合を除いて、以下のようにxaaaなどでダミー化します。

kubectl get pod
# NAME                   READY   STATUS    RESTARTS   AGE
# nginx-aaaaa            1/1     Running   0          xm
# nginx-bbbbb            1/1     Running   0          xm
# nginx-ccccc            1/1     Running   0          xm

Kubernetes の概要

まずKubernetesとは、コンテナ化されたアプリのデプロイやスケーリングなどを自動化する「コンテナオーケストレーションツール」です。他にもDocker SwarmやApache Mesosといったツールがありますが、Kubernetesがデファクトスタンダードです。3大クラウド (Google Cloud、AWS、Azure) ではKubernetesのマネージドサービスが提供されています。

KubernetesはGoogleのBorgやOmegaの知見をもとに開発され6、現在はOSSとしてCloud Native Computing Foundation (CNCF) によって管理されているGo製のツールです。ギリシャ語で「操舵手」や「パイロット」を意味していて7、そのため船の舵がアイコンになっています。

そもそもコンテナとは

Kubernetesは「コンテナをオーケストレーションするツール」ですが、そのオーケストレーション対象である「コンテナ」とは何なのかについても簡単に整理してみます。

一言で雑に表現すると、コンテナとはNamespace (名前空間) という仕組みで隔離されたLinuxのプロセスです8。Linux NamespaceにはMount、PID、Network、UTS、IPC、User、Cgroup、Timeの計8種類があり9、コンテナの隔離ではそのうちPID (プロセス)、Network (ネットワーク)、Mount (ファイルシステム) などが特に使われます。そして、Cgroupsという仕組みで各コンテナやプロセスに割り当てるCPU / メモリなどの計算リソースを制限します10

ここで重要なのは、コンテナは完全に別のマシンや別のOSではないという点です。各コンテナからは自分専用のプロセス空間、ネットワーク、ファイルシステムが見えるため、小さな独立したマシンのように振る舞いますが、実際にはホストOSのカーネルを共有しています11。イメージとしては以下のような感じです。

flowchart TB
    subgraph Host["Host マシン / Linux"]
        direction TB
        K["共有: カーネル"]

        subgraph Containers[" "]
            direction LR
            A["コンテナ A<br/>PID namespace<br/>Net namespace<br/>Mount namespace<br/>cgroup"]
            B["コンテナ B<br/>PID namespace<br/>Net namespace<br/>Mount namespace<br/>cgroup"]
        end

        K ~~~ A
        K ~~~ B
    end

    classDef default fill:transparent,stroke:#6b7280
    style Host fill:transparent,stroke:#6b7280
    style Containers fill:transparent,stroke:transparent
    style K fill:transparent,stroke:transparent

つまりコンテナは、「1台のLinuxの中で、見える範囲と使える計算リソースを強く絞った実行環境」と捉えるとイメージしやすいです。コンテナが比較的軽量で、同じホスト上に高密度で載せやすいのは、こうした仕組みによるものです。

「コンテナイメージをビルドする」とは

コンテナイメージは、Dockerfileの命令を順に積み上げて作る「読み取り専用の複数のファイルシステムレイヤーと、CmdEnvなどの起動設定をまとめたもの」です。本筋からは少し外れる内容なので、興味のある方のみ以下を開いて読んでみてください。

コンテナイメージのビルドの仕組みを深掘り

まず結論ですが、コンテナイメージとは「コンテナを作るための元データ」です。正確には、読み取り専用の複数のファイルシステムレイヤーと、CmdEnvなどの起動設定をまとめたものです1213。そしてビルドとは「Dockerfileに書かれた命令を上から順に実行し、ファイルシステムの変更差分と設定情報を積み上げてコンテナイメージを作る」ことです。(Linuxにおけるファイルシステムとは、データをディスク上で整理するルールのことで、/home/name/...のようにフォルダやファイルの階層構造そのものを指します)

以下のDockerfileを例として考えてみます。

FROM alpine:latest
COPY app.py /app/
ENV MY_APP_VERSION=1.0

この例ではalpine:latestはすでに1つのコンテナイメージであり、このイメージを土台として、app/app.pyを追加する差分が新しいレイヤーとして積み上がります。ここでいう「レイヤー」とは、直前までのファイルシステムに対する変更差分のことです。一方、ENV MY_APP_VERSION=1.0はファイルを増やす命令ではなく、イメージの設定情報に環境変数を記録する命令です。

コンテナイメージは、このように複数の読み取り専用レイヤーと設定情報から構成されます。(コンテナを起動すると、これらの読み取り専用レイヤーの上に、そのコンテナ専用の書き込み可能なレイヤーも1枚追加されます1214)

上記のDockerfileをdocker build -t my-image:v1 .でビルドして、コンテナイメージの中身を確認してみます。docker image inspectを使うと、ビルドされたイメージの詳細情報を確認できます15。以下は今回の説明に関係する部分だけ抜粋したものです。

docker image inspect my-image:v1
# [
#     {
#         "Id": "sha256:xxx",
#         "RootFS": {
#             "Type": "layers",
#             "Layers": [
#                 "sha256:989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e", // Alpine 由来のレイヤー
#                 "sha256:3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d" // COPY で追加されたレイヤー
#             ]
#         },
#         "Metadata": {
#             "LastTagTime": "2026-03-11T21:17:35.057209334+09:00"
#         },
#         "Config": {
#             "Cmd": [
#                 "/bin/sh"
#             ],
#             "Entrypoint": null,
#             "Env": [
#                 "PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin",
#                 "MY_APP_VERSION=1.0" // Dockerfile の "ENV MY_APP_VERSION=1.0" はここに記録される
#             ],
#             "Labels": null,
#             "OnBuild": null,
#             "User": "",
#             "Volumes": null,
#             "WorkingDir": "/"
#         }
#     }
# ]

レイヤー情報はRootFS.Layersに、ENV MY_APP_VERSION=1.0はレイヤーではなくConfig.Envという設定値の1つに含まれていることがわかります。(あとで見ますが、RootFS.Layersに含まれる2つのうち、1つはベースイメージalpine:latestに含まれていたレイヤー、もう1つはCOPY app.py /app/で追加されたレイヤーです)

docker image save my-image:v1 -o my-image.tarで、コンテナイメージをtarアーカイブとして取り出せます16。以下のコマンドで中身を展開してみます。

tar -tf my-image.tar
# blobs/
# blobs/sha256/
# blobs/sha256/00734834cdc61051079b427c349dcc0d338e5af54d9235900868c25f00ed29f1
# blobs/sha256/17e5645281546e5b6cb336853a58c49a02df3612df77dba5aa003eeac0299cc1
# blobs/sha256/3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d
# blobs/sha256/989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e
# blobs/sha256/a3b70f309301534ce7c7fb5564a03c0a0f216aefd0e6856a7cb6d6cc6674b7c4
# blobs/sha256/cc63b61e69632ab13b5604dde713b9525759f87dc54faea6ef9e1abb9686dd55
# index.json
# manifest.json
# oci-layout
# repositories

マニフェスト (manifest.json) やレイヤーのデータ (blobs/sha256/989e...blobs/sha256/3e0e...) が含まれています。ここには、以下の2通りで1つのイメージが表現されています。

  1. Open Container Initiative (OCI)
  2. Docker互換用の補助情報

OCIはコンテナのイメージ形式や実行方法を標準化するための仕様群で、Linux Foundation傘下のプロジェクトです17。これは色々なツールで理解できるコンテナの共通語のようなものであり、DockerなどのツールはこのOCIの仕様に従うことで相互運用しやすくなっています。(上記出力内で言うと、oci-layoutindex.jsonなどがOCI側の情報です。)

例としてmanifest.jsonをピックアップして中身を見てみます。これは上記2通りのうち、Docker互換用 (docker saveで使用される) のマニフェストです。

tar -xOf my-image.tar manifest.json | jq .
# [
#   {
#     "Config": "blobs/sha256/cc63b61e69632ab13b5604dde713b9525759f87dc54faea6ef9e1abb9686dd55",
#     "RepoTags": [
#       "my-image:v1"
#     ],
#     "Layers": [
#       "blobs/sha256/989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e",
#       "blobs/sha256/3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d"
#     ],
#     "LayerSources": {
#       "sha256:3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d": {
#         "mediaType": "application/vnd.oci.image.layer.v1.tar",
#         "size": 2048,
#         "digest": "sha256:3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d"
#       },
#       "sha256:989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e": {
#         "mediaType": "application/vnd.oci.image.layer.v1.tar",
#         "size": 8724480,
#         "digest": "sha256:989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e"
#       }
#     }
#   }
# ]

最初に「コンテナイメージは複数の読み取り専用レイヤーから構成される」と説明しましたが、それを具体的に見てみます。上記出力のうち以下部分がalpine:latest由来のレイヤーと、COPY app.py /app/で追加されたレイヤーです。

#     "Layers": [
#       "blobs/sha256/989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e",
#       "blobs/sha256/3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d"
#     ],
  • alpine:latest由来のレイヤー
tar -tf blobs/sha256/989e799e634906e94dc9a5ee2ee26fc92ad260522990f26e707861a5f52bf64e | head
# bin/
# bin/arch
# bin/ash
# bin/base64
# bin/bbconfig
# bin/busybox
# bin/cat
# bin/chattr
# bin/chgrp
# bin/chmod
  • COPY app.py /app/で追加されたレイヤー
tar -tf blobs/sha256/3e0efc0c962cfdda31aab29a94db20c3b465be2147712937057b7d73607c796d | head
# app/
# app/app.py

まとめると、Kubernetesはコンテナオーケストレーションツールです。コンテナとは隔離されたLinuxのプロセスであり、コンテナイメージをビルドすることはDockerfileの命令を順に適用して、レイヤーと設定情報を作ることです。コンテナイメージとは、それらの読み取り専用レイヤーと設定情報をまとめたものです。

Docker との関係

個人的に「コンテナといえばDocker」の印象が強かったので、ここではKubernetesとDockerの関係について簡単に紹介します。

まずDockerは「イメージのビルド / 管理、コンテナ実行、CLIなどをまとめて提供するツール」です。Dockerのようなツールがないと「そもそもコンテナとは」で紹介したLinuxプロセス / ネットワーク / マウントの分離、計算リソースの制限など、低レイヤーの操作をすべて人間がやる必要があります。Dockerはこれらをdocker runで高レベルに抽象化しています。通常、Kubernetesは複数のサーバー (物理マシンやVM。これを「ノード」と言います) を束ねて、1つの論理的な集合である「クラスタ」を作ります。しかし手元のPCでVMを3台も立ち上げるのは大変なので、ローカルでKubernetesを動かすときは、その再現にDockerが使われることがあります。(今回もDockerが使用されるkindというツールを使っていきます)

ただし基本的に、Kubernetesは開発者がdocker runを直接実行してコンテナを並べていく仕組みではありません。「どんなコンテナを、いくつ動かしたいか」を「マニフェスト」という設定ファイルとして定義し、Kubernetes (正確には、後述するkubeletというコンポーネント) がそれを解釈して各ノード上でコンテナを動かします。

まとめると、Dockerはビルド、CLI、API、イメージ管理、コンテナ実行をまとめた統合ツールである一方、Kubernetesは主にコンテナの配置、復旧、宣言的管理などを担当するオーケストレーションツールです。そのため、用途がそもそも違います。

なお、Kubernetesはオープンソースとして公開されていますが、それをベースに各社が独自機能を加えてパッケージングした「ディストリビューション」も存在します。代表的なのはRed HatによるOpenShift18 で、エンタープライズ向けの管理・セキュリティ機能などが組み込まれており、業務環境では候補に挙がることも多いと考えられます。ただし、本記事では扱いません。

まとめ

この章ではコンテナの仕組み、Kubernetesが「コンテナオーケストレーションツール」として何を解決するのかを概観しました。次章では、Kubernetesが必要になる具体的な背景をもう一段掘り下げます。

Kubernetes が必要な理由

ここまでKubernetes、コンテナ、Dockerあたりのキーワードについて触れてきました。では次に、Kubernetes (というより、コンテナオーケストレーション) が必要な理由について軽く見ていきます。(詳しくは、以下の神記事を読めば間違いないです)

ざっくりと上記の記事の理解をまとめます。本当に雑まとめですが、以下だと解釈しています。

  1. 以下2つの大きな流れの中でコンテナの需要が爆増
    1. OSレベルの仮想化 ⇒ コンテナによる仮想化
    2. モノリス ⇒ マイクロサービス
  2. アプリケーションで使うコンテナが増えすぎて管理・運用がツラくなる

コンテナが増えると、その運用が人間によって正確に回される前提になり、例えば以下のような点でツラくなりそうです。

  • アプリケーションのデプロイのたびに複数のサーバー (VMや物理マシン) でコンテナの取得・停止・差し替えを繰り返すため、更新漏れで環境差分が起きやすい
  • デプロイ後に不具合が起きた際のロールバックも手作業なので、時間がかかる
  • コンテナが落ちた際は、どのサーバーで何が落ちたかdocker psdocker logsで原因を確認 ⇒ 再起動する必要がある
  • 必要なコンテナ数や各サーバーに必要なCPU / メモリを人が見積もるため、過不足が出やすい
  • 機能追加や障害のたびに「どのサーバーに配置するか」を判断する必要があり、運用が属人化しやすい

Kubernetesは上記のような課題を「複数サーバー上のコンテナ運用を、宣言した状態に自動で収束させる」ことによって解決しました。なお、この記事ではKubernetesがデファクトスタンダードになった理由や、他のコンテナオーケストレーションツールの紹介などはバッサリと割愛します。

まとめ

この章ではコンテナのスケール・更新・障害復旧を手作業で運用する難しさと、それを宣言的に扱うKubernetesの役割を整理しました。次章では実際にkindでローカルクラスタを作り、PodからGatewayまで基本リソースを順に動かしていきます。

ローカルで触る Kubernetes

早速ローカル環境で触りながらKubernetesを理解していきます。

前提

ローカルマシンでのKubernetes実行にはkindというツールを使用します (他にもminikube19 やK3s20 といったツールがあります)。kindはPodmanやnerdctlといったツールもproviderとして利用できますが、この記事では後続の手順でdockerコマンドも使用するため、Docker Engineを前提に進めます。21

また、Kubernetes APIとのやり取りにはkubectlという公式CLIを使います。

各種ツールのインストール

  • docker: kindのproviderとして使用
  • kind: ローカルKubernetesクラスタの作成に使用
  • kubectl: クラスタの確認やマニフェストの適用に使用

インストール方法は色々ありますが、ここではWSL2上のUbuntu 24.04 LTSを前提に、公式ドキュメントに沿った方法で進めます。2223

Docker Engine

Docker Engineは公式のapt repositoryからインストールします。

sudo apt update
sudo apt install -y ca-certificates curl
sudo install -m 0755 -d /etc/apt/keyrings
sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc
sudo chmod a+r /etc/apt/keyrings/docker.asc

echo \
  "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu \
  $(. /etc/os-release && echo \"${UBUNTU_CODENAME:-$VERSION_CODENAME}\") stable" | \
  sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
sudo usermod -aG docker $USER
newgrp docker

kubectl

kubectlは公式バイナリをインストールします。この記事ではv1.35.1を使用します。

KUBECTL_VERSION=v1.35.1
curl -LO "https://dl.k8s.io/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl"
chmod +x kubectl
sudo mv kubectl /usr/local/bin/kubectl

kind

kindは公式配布バイナリを使います。この記事ではv0.31.0を使用します。

KIND_VERSION=v0.31.0
curl -Lo ./kind "https://kind.sigs.k8s.io/dl/${KIND_VERSION}/kind-linux-amd64"
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind

実行環境とバージョン確認

  • 実行環境
C:\Users\xxx>wsl --version
# WSL バージョン: 2.6.1.0
# カーネル バージョン: 6.6.87.2-1
# WSLg バージョン: 1.0.66
# MSRDC バージョン: 1.2.6353
# Direct3D バージョン: 1.611.1-81528511
# DXCore バージョン: 10.0.26100.1-240331-1435.ge-release
# Windows バージョン: 10.0.26200.7840
  • 各種ツール
kubectl version --client
# Client Version: v1.35.1
# Kustomize Version: v5.7.1

kind version
# kind v0.31.0 go1.25.5 linux/amd64

docker version --format '{{.Client.Version}}'
# 28.4.0

クラスタを作る

なにはともあれ、まずはクラスタを作っていきましょう。なお、以降の手順は~/k8s-labのような作業用ディレクトリを1つ作って、そこをカレントディレクトリにしている前提で進めます。記事を進める中で複数のYAMLファイルを同じディレクトリに作成していきます。

  • クラスタの作成
# デフォルトではシングルノードのクラスタになるため、マルチノードの場合は明示
cat > kind-config.yaml <<'EOF'
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
EOF

kind create cluster --config kind-config.yaml
# Creating cluster "kind" ...
#  ✓ Ensuring node image (kindest/node:v1.35.0) 🖼
#  ✓ Preparing nodes 📦 📦 📦
#  ✓ Writing configuration 📜
#  ✓ Starting control-plane 🕹️
#  ✓ Installing CNI 🔌
#  ✓ Installing StorageClass 💾
#  ✓ Joining worker nodes 🚜
# Set kubectl context to "kind-kind"
# You can now use your cluster with:

# kubectl cluster-info --context kind-kind

# Have a nice day! 👋
  • クラスタ内のノードの確認
kubectl get node -o wide
# NAME                 STATUS   ROLES           AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE                         KERNEL-VERSION                     CONTAINER-RUNTIME
# kind-control-plane   Ready    control-plane   xs     v1.35.0   172.18.0.4    <none>        Debian GNU/Linux 12 (bookworm)   6.6.87.2-microsoft-standard-WSL2   containerd://2.2.0
# kind-worker          Ready    <none>          xs     v1.35.0   172.18.0.5    <none>        Debian GNU/Linux 12 (bookworm)   6.6.87.2-microsoft-standard-WSL2   containerd://2.2.0
# kind-worker2         Ready    <none>          xs     v1.35.0   172.18.0.2    <none>        Debian GNU/Linux 12 (bookworm)   6.6.87.2-microsoft-standard-WSL2   containerd://2.2.0

コントロールプレーン ×1とワーカーノード ×2が作成されたことが確認できました。つまり、現在以下のような状態になっています。なお、ここからMermaidの図を多用していきますが、基本は無色にして、説明対象のコンポーネントだけ青くハイライトします。

flowchart LR
    subgraph Cluster["クラスタ"]

        subgraph ControlPlane["コントロールプレーン<br>(kind-control-plane)"]
            direction TB
            etcd["etcd"]
            apiserver["kube-apiserver"]
            scheduler["kube-scheduler"]
            controller["kube-controller-manager"]
        end

        subgraph LeftWorker["ワーカーノード<br>(kind-worker2)"]
            direction TB
            kubeletLeft["kubelet"]
            proxyLeft["kube-proxy"]
        end

        subgraph RightWorker["ワーカーノード<br>(kind-worker)"]
            direction TB
            kubeletRight["kubelet"]
            proxyRight["kube-proxy"]
        end

        ControlPlane ~~~ LeftWorker
        ControlPlane ~~~ RightWorker
    end

    classDef default fill:transparent,stroke:#6b7280
    style Cluster fill:transparent,stroke:#6b7280
    style ControlPlane fill:transparent,stroke:#6b7280
    style LeftWorker fill:transparent,stroke:#6b7280
    style RightWorker fill:transparent,stroke:#6b7280

kind の仕組み

これ以降kindのコマンドは出てこないため余談ですが、kindは kubernetes in dockerの略で、その名の通りKubernetesをDockerコンテナの中で動かす技術です。ローカルマシンでKubernetesをテストするために使います。

kind のアーキテクチャ kind 公式ドキュメントより

上記のkind create clusterのログにも出力されていますが、kindest/nodeというコンテナイメージでKubernetesのノードとして動くための土台を作っています。kindはこのコンテナの中でkubeadmというクラスタ初期化ツールを使用して各ノードを初期化しています。

kubectl と kube-apiserver

kubectl get node -o wideで、Kubernetesクラスタ内のノードに関する情報を確認しました。ここで出てくるのがkube-apiserverです。これがAPIサーバーとして動いており、kubectlなどの外部クライアントだけでなく、kube-controller-managerやkubeletなどの内部コンポーネントからのリクエストも受けます。(実際にクラスタの作成などを行うのは他コンポーネントですが、それは別途紹介します)

flowchart LR
    kubectl["kubectl"]

    subgraph Cluster["クラスタ"]

        subgraph ControlPlane["コントロールプレーン<br>"]
            direction TB
            etcd["etcd"]
            apiserver["kube-apiserver"]
            scheduler["kube-scheduler"]
            controller["kube-controller-manager"]
        end
    end

    kubectl -->|API リクエスト| apiserver

    classDef default fill:transparent,stroke:#6b7280
    style Cluster fill:transparent,stroke:#6b7280
    style ControlPlane fill:transparent,stroke:#6b7280
    style kubectl fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
    style apiserver fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

Pod を作る

途中から始めたい方は、サンプルリポジトリ zenn-k8s-handson をcloneしてmake ckpt-00を実行すると、この節の前提状態 (空のkindクラスタ) を再現できます。詳細は同リポジトリのREADMEを参照してください。

話を本筋に戻します。kind create clusterでKubernetesクラスタを作成しました。再掲ですが、現在は以下のようなコントロールプレーンとワーカーノードの状況になっています。

flowchart LR
    subgraph Cluster["クラスタ"]

        subgraph ControlPlane["コントロールプレーン<br>(kind-control-plane)"]
            direction TB
            etcd["etcd"]
            apiserver["kube-apiserver"]
            scheduler["kube-scheduler"]
            controller["kube-controller-manager"]
        end

        subgraph LeftWorker["ワーカーノード<br>(kind-worker2)"]
            direction TB
            kubeletLeft["kubelet"]
            proxyLeft["kube-proxy"]
        end

        subgraph RightWorker["ワーカーノード<br>(kind-worker)"]
            direction TB
            kubeletRight["kubelet"]
            proxyRight["kube-proxy"]
        end

        ControlPlane ~~~ LeftWorker
        ControlPlane ~~~ RightWorker
    end
    classDef default fill:transparent,stroke:#6b7280
    style Cluster fill:transparent,stroke:#6b7280
    style ControlPlane fill:transparent,stroke:#6b7280
    style LeftWorker fill:transparent,stroke:#6b7280
    style RightWorker fill:transparent,stroke:#6b7280

KubernetesではPodやServiceなどのリソースをYAMLのマニフェストで定義できます。kindで作成したクラスタのコントロールプレーンでも、kube-apiserverやetcdなどを起動するためのマニフェストがコンテナ内に置かれています。(詳しい役割や中身の説明は後ほどします)

docker exec -it kind-control-plane bash
root@kind-control-plane:/# ls etc/kubernetes/manifests/
# etcd.yaml  kube-apiserver.yaml  kube-controller-manager.yaml  kube-scheduler.yaml

各種コンポーネントのYAMLがそれぞれコントロールプレーンのコンテナ (kind-control-plane) 内のetc/kubernetes/manifests/に存在することがわかります。これらコンポーネントのマニフェストは、この段階で紹介するには比較的複雑なので割愛しますが、ひとまずKubernetesはマニフェストというYAMLファイルで構成を管理することの紹介でした。

ではマニフェストを書いて、実際にPodを作ってみます。改めてですがPodはKubernetesの最小実行単位です。青い部分を作っていきます。

flowchart LR
    subgraph Node1["ワーカーノード<br>(kind-worker)"]
        direction TB
        kubelet1["kubelet"]
        proxy1["kube-proxy"]

        subgraph Pod["Pod: nginx"]
            direction LR
            container1_1["Container: nginx"]
        end

        kubelet1 ~~~ Pod
        proxy1 ~~~ Pod
    end
    classDef default fill:transparent,stroke:#6b7280
    style Node1 fill:transparent,stroke:#6b7280
    style Pod fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
  • マニフェストの作成
cat > pod-nginx.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: nginx
spec:
  nodeSelector:
    kubernetes.io/hostname: kind-worker
  containers:
  - name: nginx
    image: nginx
EOF

kubernetes.io/hostnameの値はクラスタ作成時のノード名に依存するため、各自の環境ではkubectl get nodeで確認した名前を指定してください。kindではデフォルトでkind-workerkind-worker2のように、クラスタ名 (デフォルトはkind) がノード名のプレフィックスになります。24

動きの理解を重視したいので、この記事ではマニフェストの中身にはあまり立ち入らず、最小限の解説に留めます。

  • kind: オブジェクトの種類です。Podや、これから説明していくReplicaSet、Deploymentなど指定します。
  • spec: オブジェクトの「あるべき姿」です。例えばPodではspec.containers[].nameがコンテナ名、spec.containers[].imageが実行するイメージ名です。25

マニフェストをクラスタに適用し、Podを作成します。

kubectl apply -f pod-nginx.yaml
# pod/nginx created
  • 作成したPodの確認
kubectl get pod nginx -o wide
# NAME    READY   STATUS    RESTARTS   AGE   IP            NODE                 NOMINATED NODE   READINESS GATES
# nginx   1/1     Running   0          xs    10.244.0.10   kind-worker   <none>           <none>

nginxという名前の単一Podがkind-workerノードで動作している状態 (STATUSRunning) であることを示しています。Podの80番ポートをローカルの8080番に転送し、アクセスしてみます。

  • ポートフォワードで待ち受け
kubectl port-forward pod/nginx 8080:80
# Forwarding from 127.0.0.1:8080 -> 80
# Forwarding from [::1]:8080 -> 80
  • 別ターミナルからアクセス
curl http://127.0.0.1:8080
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# <style>
# html { color-scheme: light dark; }
# body { width: 35em; margin: 0 auto;
# font-family: Tahoma, Verdana, Arial, sans-serif; }
# </style>
# </head>
# <body>
# <h1>Welcome to nginx!</h1>
# <p>If you see this page, the nginx web server is successfully installed and
# working. Further configuration is required.</p>

# <p>For online documentation and support please refer to
# <a href="http://nginx.org/">nginx.org</a>.<br/>
# Commercial support is available at
# <a href="http://nginx.com/">nginx.com</a>.</p>

# <p><em>Thank you for using nginx.</em></p>
# </body>
# </html>

nginxのイメージに入っているindex.htmlの内容が確認できました。

ひとまず最小実行単位であるPodを作ってみました。Podは以下のような役割を持っています。

  • Kubernetesの最小実行単位であり、1つ以上のコンテナをまとめる
  • 同一Pod内のコンテナはネットワーク名前空間やVolumeを共有する。26
  • kubeadmでブートストラップされたクラスタ (kindなどのローカル環境を含む) では、kube-apiserverやkube-schedulerなどコントロールプレーンの主要コンポーネントはstatic Pod (kube-apiserverを介さず、ノード上の特定ディレクトリに置かれたマニフェストからkubeletが直接起動するPod) として動きます27。一方、GKEなどのマネージド環境ではコントロールプレーンはクラウドベンダーが管理するので、ユーザー Podとしては見えなくなっています

Podを直接管理することは基本的にありません。上位概念としてReplicaSetとDeploymentが存在し、それらを通じて間接的に管理されます。

flowchart TB
    D["Deployment"]
    RS["ReplicaSet"]
    P1["Pod"]
    P2["Pod"]

    D --> RS
    RS --> P1
    RS --> P2
    classDef default fill:transparent,stroke:#6b7280

そのため、ここで作ったPodはひとまず削除しておきます。

kubectl delete pod nginx
# pod "nginx" deleted from default namespace

ReplicaSet を作る

次にReplicaSetを作ります。ReplicaSetは指定された数のPodを複製・実行するオブジェクトです。

flowchart TB
    D["Deployment"]
    RS["ReplicaSet"]
    P1["Pod"]
    P2["Pod"]

    D --> RS
    RS --> P1
    RS --> P2

    classDef default fill:transparent,stroke:#6b7280
    style RS fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

前述の通り、あとで説明するDeploymentがReplicaSetを管理しているため、基本的にDeploymentの方を触ります。(とはいえ段階的な理解の促進のために、ReplicaSetも軽く触ります)

  • マニフェストの作成
cat > replica-set.yaml <<'EOF'
apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
EOF

マニフェストの簡単な解説です。

  • spec.replicas: 作成したいPodの数です。
  • spec.selector.matchLabels: このReplicaSetが管理対象とするPodです。上記の例でいうと、app: nginxというラベルがついたPodを管理対象とするということです。
  • spec.template.metadata.labels: このReplicaSetが新規作成するPodに付与するラベルです。つまり、spec.selector.matchLabelsとセットで同じにする必要があり、「自分で作るPodを、自分の管理対象とする」ということになっています。
  • spec.template.spec: ここはPodのマニフェストと同じです。コンテナイメージなどPodのあるべき姿を定義します。28

クラスタに適用します。

kubectl apply -f replica-set.yaml
# replicaset.apps/nginx created
  • 確認
# kubectl get rs でも可
kubectl get replicaset
# NAME             DESIRED   CURRENT   READY   AGE
# nginx            3         3         3       xs

metadata.nameに指定したnginxという名前のReplicaSetが作成されています。spec.replicasを3に指定したので、3つのPodが起動しています。(3つすべて起動する (READYが3になる) までには、少し時間がかかります)

  • Podを直接確認
kubectl get pod
# NAME                   READY   STATUS    RESTARTS   AGE
# nginx-aaaaa            1/1     Running   0          xs
# nginx-bbbbb            1/1     Running   0          xs
# nginx-ccccc            1/1     Running   0          xs

prefixにnginx-とつくPodが3つあることが確認できます。kubectl scaleでレプリカの数を増やしたりもできます。

kubectl scale --replicas=5 -f replica-set.yaml
# replicaset.apps/nginx scaled

kubectl get pod
# NAME                   READY   STATUS    RESTARTS   AGE
# nginx-ddddd            1/1     Running   0          xs
# nginx-aaaaa            1/1     Running   0          xm
# nginx-bbbbb            1/1     Running   0          xm
# nginx-ccccc            1/1     Running   0          xm
# nginx-eeeee            1/1     Running   0          xs

Podの数が5に増えていることが確認できました。

改めてですが、このようにReplicaSetは指定個のPodを常に満たすようにする役割を持っています。人間が実際に触るのは次に紹介するDeploymentが基本であるため、削除しておきます。

kubectl delete replicaset nginx
# replicaset.apps "nginx" deleted from default namespace

Deployment を作る

DeploymentはReplicaSetを管理して、Podのローリングアップデートやロールバックなどを担います。

flowchart TB
    D["Deployment"]
    RS["ReplicaSet"]
    P1["Pod"]
    P2["Pod"]

    D --> RS
    RS --> P1
    RS --> P2

    classDef default fill:transparent,stroke:#6b7280
    style D fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

早速マニフェストを作っていきます。

cat > deployment.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  strategy:
    type: RollingUpdate
    rollingUpdate:
      maxSurge: 50%
      maxUnavailable: 0%
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
        ports:
        - containerPort: 80
EOF

ReplicaSetのマニフェストを再掲しますが、DeploymentはReplicaSetを拡張している感じです。そのため、差分に絞って解説します。

apiVersion: apps/v1
kind: ReplicaSet
metadata:
  name: nginx
spec:
  replicas: 3
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - image: nginx
        name: nginx
  • spec.strategy.rollingUpdate.maxSurge: spec.replicasの数に対して更新中に追加で作って良いPod数 (パーセンテージでも指定可) です。
  • spec.strategy.rollingUpdate.maxUnavailable: spec.replicasの数に対して更新中に一時的に利用不可になって良いPod数 (同じくパーセンテージでも指定可) です。

つまり上記の例ではmaxSurgeは50%、spec.replicasは2であるため、その150% である3までは更新中にPodを増やしても良く、またmaxUnavailableは0% なので、常に2つのPodを維持するという設定になっています。なお、maxSurgemaxUnavailableのデフォルトはどちらも25% で、両方を同時に0にはできません。

spec.strategy.typeRollingUpdateRecreateが選択可能で、デフォルトはRollingUpdateです。RollingUpdateは上記で紹介したように段階的なアップデートを行います。Recreateは更新前に古いPodをすべて停止してから新しいPodを作ります。(一時的に停止時間が発生します。つまりデータの書き込みや互換性の問題で新旧Podの同時稼働が許容できない場合などはRecreateを選択する必要があります)29

  • クラスタへの適用
kubectl apply -f deployment.yaml
# deployment.apps/nginx created
  • 作成したDeploymentの確認
# kubectl get deploy でも可
kubectl get deployment
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# nginx   2/2     2            2           xs

nginxという名前のDeploymentが確認できました。spec.replicasを2に設定したため、Podが2つ作成されています。

  • Podを直接確認
kubectl get pod
# NAME                     READY   STATUS    RESTARTS   AGE
# nginx-aaaaaaaaaa-aaaaa   1/1     Running   0          xm
# nginx-aaaaaaaaaa-bbbbb   1/1     Running   0          xm

次に、ロールアウトとロールバックを確認してみます。以下のコマンドでDeploymentの状態をwatchします。

kubectl get deployment nginx -n default -w
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# nginx   2/2     2            2           xm

READY2/2となっており、これは2つのPodが稼働していることを示します。ではnginxのイメージを暗黙のlatestからバージョン1.29を明示するように変更してみましょう。別ターミナルで以下のコマンドを実行します。

kubectl set image deployment/nginx nginx=nginx:1.29 -n default
# deployment.apps/nginx image updated

最初のターミナルに戻ると、以下のようにPodが切り替わっている様子が確認できます。

kubectl get deployment nginx -n default -w
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# nginx   2/2     2            2           xh
# nginx   2/2     2            2           xh
# nginx   2/2     2            2           xh
# nginx   2/2     0            2           xh
# nginx   2/2     1            2           xh
# nginx   3/2     1            3           xh
# nginx   2/2     1            2           xh
# nginx   2/2     2            2           xh
# nginx   2/2     2            2           xh
# nginx   3/2     2            3           xh
# nginx   2/2     2            2           xh
# nginx   2/2     2            2           xh

READYが3/2になっているタイミングがあります。これはマニフェストの説明と重複しますが、maxSurgeを50% に指定 = Podの数が150% の3まで増えることを許容したために起きた動きです。(AVAILABLE= 利用可能なPodの数も2を維持していることがわかります)

ReplicaSetやPodのレベルでもこの動きは確認可能です。(興味のある方はkubectl get pod -n default -w --show-labelsなどで試してみてください)

以下のようにしてロールバックもできます。(ログは各自確認してみてください)

kubectl rollout undo deployment/nginx -n default
# deployment.apps/nginx rolled back

Reconciliation Loop を体感する

ここでは、Kubernetesの特徴の1つであるReconciliation Loopを確認します。

その前に、Kubernetesクラスタの状態がどこに保存されるかについて触れておきます。保存を担っているのは (基本的に) etcd30 というkey-value型データストアのコンポーネントです。

flowchart TB
    kubectl["kubectl"]

    subgraph Cluster["クラスタ"]

        subgraph ControlPlane["コントロールプレーン<br>"]
            direction TB
            etcd["etcd"]
            apiserver["kube-apiserver"]
            scheduler["kube-scheduler"]
            controller["kube-controller-manager"]
        end
    end

    kubectl -->|API リクエスト| apiserver
    apiserver -->|状態の保存| etcd

    classDef default fill:transparent,stroke:#6b7280
    style Cluster fill:transparent,stroke:#6b7280
    style ControlPlane fill:transparent,stroke:#6b7280
    style etcd fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

このetcdには以下のような情報が保存されています。

  • Deployment、PodなどのKubernetesオブジェクトの情報
  • Podの配置情報 (どのノードに紐づくかなど)

Kubernetesでは、クラスタの状態がkube-apiserver経由で上記のetcdに保存されます。controllerやkube-scheduler、kubeletなど (後述します) は、そのetcdに保存されている「マニフェストに定義された理想の状態 (desired state)」と「現在 (実際) の状態 (current state)」の差分を見て、desired stateの方に収束させます。

まず動きの確認をしてみます。Deploymentを作って、Podを削除します。

# Pod が常に 2 つは維持されるように設定
kubectl create deployment web --image=nginx --replicas=2
# Pod のステータスを表示
kubectl get pods -w
# NAME                   READY   STATUS    RESTARTS   AGE
# web-aaaaaaaaaa-aaaaa   1/1     Running   0          xs
# web-aaaaaaaaaa-bbbbb   1/1     Running   0          xs
  • 別ターミナルでPodを削除
kubectl delete pod -l app=web
  • 最初のターミナルを確認
kubectl get pods -w
# NAME                   READY   STATUS    RESTARTS   AGE
# web-aaaaaaaaaa-aaaaa   1/1     Running             0          xs
# web-aaaaaaaaaa-bbbbb   1/1     Running             0          xs
# web-aaaaaaaaaa-aaaaa   1/1     Terminating         0          ys
# web-aaaaaaaaaa-bbbbb   1/1     Terminating         0          ys
# web-aaaaaaaaaa-ccccc   0/1     Pending             0          0s
# web-aaaaaaaaaa-aaaaa   1/1     Terminating         0          ys
# web-aaaaaaaaaa-ccccc   0/1     Pending             0          0s
# web-aaaaaaaaaa-ddddd   0/1     Pending             0          0s
# web-aaaaaaaaaa-ddddd   0/1     Pending             0          0s
# web-aaaaaaaaaa-bbbbb   1/1     Terminating         0          ys
# web-aaaaaaaaaa-ccccc   0/1     ContainerCreating   0          0s
# web-aaaaaaaaaa-ddddd   0/1     ContainerCreating   0          0s
# web-aaaaaaaaaa-bbbbb   0/1     Completed           0          ys
# web-aaaaaaaaaa-aaaaa   0/1     Completed           0          ys
# web-aaaaaaaaaa-bbbbb   0/1     Completed           0          ys
# web-aaaaaaaaaa-bbbbb   0/1     Completed           0          ys
# web-aaaaaaaaaa-aaaaa   0/1     Completed           0          ys
# web-aaaaaaaaaa-aaaaa   0/1     Completed           0          ys
# web-aaaaaaaaaa-ccccc   1/1     Running             0          xs
# web-aaaaaaaaaa-ddddd   1/1     Running             0          xs

削除はRunning ⇒ Terminating ⇒ Completed、作成はPending ⇒ ContainerCreating ⇒ RunningとSTATUSがそれぞれ変化しています。つまり、もともとあったPodは削除され、新しいPodが作成された結果、最終的にPodが2つに戻っていることが確認できます。(STATUSの遷移はあくまで今回の例です。詳細は公式ドキュメント31を確認してください)

この裏ではkube-controller-manager内で動く各種controllerと、kube-scheduler、kubeletが以下のような役割で連携しています。32

  • Podオブジェクトの不足分補充: 主にReplicaSet controller (kube-controller-manager内)
  • ノードへの割り当て: kube-scheduler
  • ノード上でのコンテナ起動 / 状態報告: kubelet
flowchart LR
    subgraph Cluster["クラスタ"]

        subgraph ControlPlane["コントロールプレーン<br>(kind-control-plane)"]
            direction TB
            etcd["etcd"]
            apiserver["kube-apiserver"]
            scheduler["kube-scheduler"]
            controller["kube-controller-manager"]
        end

        subgraph Node1["ワーカーノード"]
            direction TB
            kubelet1["kubelet"]
            proxy1["kube-proxy"]

            subgraph Pod1["Pod: web-*"]
                direction LR
                container1_1["Container: nginx"]
            end

            subgraph Pod2["Pod: web-*"]
                direction LR
                container2_1["Container: nginx"]
            end

            kubelet1 ~~~ Pod1
            proxy1 ~~~ Pod1
            kubelet1 ~~~ Pod2
            proxy1 ~~~ Pod2
        end
        ControlPlane ~~~ Node1
    end

    classDef default fill:transparent,stroke:#6b7280
    style Cluster fill:transparent,stroke:#6b7280
    style ControlPlane fill:transparent,stroke:#6b7280
    style Node1 fill:transparent,stroke:#6b7280
    style Pod1 fill:transparent,stroke:#6b7280
    style Pod2 fill:transparent,stroke:#6b7280
    style scheduler fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
    style controller fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
    style kubelet1 fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

「ReplicaSet controller」という言葉が出てきましたが、controllerは特定のリソースを監視し、現在の状態をdesired stateに近づけるための常駐プロセス / ロジックです。ReplicaSet controller以外にも、Deployment controllerやnode controllerなどが存在します。kube-controller-managerは、それらのbuilt-in controllerをまとめて動かすという役割です。32

改めてですが、先ほどコマンドで確認した「Deployment配下のPodが2つ消えた時」の連携フローは以下のようになります。

  1. kubectl delete podでPodの削除を要求
  2. ReplicaSet controller(kube-controller-manager内)がdesiredとactualの差分を検知
  3. ReplicaSet controllerが不足分のPodを新しく作成
  4. kube-schedulerがそのPodの配置先ノードを決定
  5. 割り当て先ノードのkubeletがコンテナを起動し、状態をkube-apiserverに反映
  6. 新しいPodがReadyになると、希望レプリカ数に戻る
sequenceDiagram
    autonumber
    participant U as kubectl
    participant API as kube-apiserver
    participant RSC as ReplicaSet controller
    participant SCH as kube-scheduler
    participant K as kubelet (割り当て先 node)

    U->>API: Pod を削除
    API-->>RSC: Watch でレプリカ不足を検知
    Note right of RSC: ReplicaSet controller は kube-controller-manager 内で動作
    RSC->>API: 代替 Pod を作成
    API-->>SCH: 未割り当て Pod を検知
    SCH->>API: 配置先 node を決定
    API-->>K: Pod 定義を反映
    K->>K: イメージ取得・コンテナ起動
    K->>API: Pod Running / Ready

Deploymentが管理するのはReplicaSetであるため、Deployment controllerは前面に出てこず、主役はReplicaSet controllerです3334

確認用に作ったweb Deploymentはここで削除しておきます。

kubectl delete deployment web
# deployment.apps "web" deleted

ここまでで、Pod、ReplicaSet、Deploymentは以下のような役割分担でレイヤーが分離されていることを確認してきました。

  • PodはKubernetesの最小単位であり、1つ以上のコンテナをグルーピングする
  • ReplicaSetは指定数のPodを常に満たすように維持する
  • DeploymentはReplicaSetのローリングアップデート / ロールバック / 履歴管理などの更新戦略を管理する
flowchart TB
    D["Deployment"]
    RS["ReplicaSet"]
    P1["Pod"]
    P2["Pod"]

    D --> RS
    RS --> P1
    RS --> P2
    classDef default fill:transparent,stroke:#6b7280

Service を作る

Serviceは、変動するPodに対する安定した接続先と、L4の負荷分散を提供します。

クラスタ内のすべてのPodはそれぞれ独自のIPアドレスを持っており、それを使ってアプリケーションに直接アクセスできます。しかしDeploymentによる更新や障害復旧ではPodが置き換わることがあり、個々のIPアドレスを接続先として前提にはできません。そこでServiceがPod群への安定した接続先の役割を担います。

flowchart LR
    podA["Pod-A"] --> svc["Service<br/>10.1.0.2"]

    subgraph backends[" "]
        direction TB
        podB1["Pod-B"]
        podB2["Pod-C"]
        podB3["Pod-D"]
    end

    svc --> podB1
    svc --> podB2
    svc --> podB3

    classDef default fill:transparent,stroke:#6b7280
    style backends fill:transparent,stroke:transparent

ServiceはClusterIP、NodePort、LoadBalancer、ExternalNameの4種類です。typeフィールドの値で指定し、省略した場合のデフォルトはClusterIPです。ClusterIP、NodePort、LoadBalancerはPod群の公開方法の違いで、ExternalNameは少し性質の異なる特殊なServiceです。今回は記事の流れの都合上、ClusterIPのみ紹介します。35

なお、spec.clusterIP: Noneを指定する「ヘッドレスサービス」もあり、これはtypeとは別軸の概念です。通常のServiceが仮想IP (ClusterIP) を持ちロードバランスを介してPodに振り分けるのに対し、ヘッドレスサービスは仮想IPを持たず、DNS名を引くと配下のPodのIPが直接 (複数のAレコードとして) 返ってきます。StatefulSetで個々のPodを区別して接続したい場合や、クライアント側でロードバランスを行いたい場合などに使われます。36

まず前提として、Serviceがトラフィックを転送する先のPodが必要ですが、先ほど作ったものが残っている場合はそのまま使います。(作っていない方は Deployment を作るの手順で作ってください)

kubectl get deployment
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# nginx   2/2     2            2           23hs

このDeploymentのPodを選択するServiceを作ります。

cat > service.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: nginx
spec:
  type: ClusterIP
  selector:
    app: nginx
  ports:
  - port: 80
    targetPort: 80
EOF

spec.selectorは、Service転送先のPodの条件です。これはDeploymentが作成するPodのラベル (spec.template.metadata.labels) と一致させます。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  strategy:
    ...
  template:
    metadata:
      labels:
        app: nginx # Pod のラベル。これを用いて Service の転送先として指定する
    spec:
      ...

また、先ほどServiceにはClusterIPやNodePortなど4種類あるとお伝えしましたが、spec.typeを指定しないとデフォルトでClusterIPになります。

  • クラスタへの適用
kubectl apply -f service.yaml
# service/nginx created
  • 作成したServiceの確認
# kubectl get svc でも可
kubectl get service
# NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   xm
# nginx        ClusterIP   10.96.205.113   <none>        80/TCP    xs

kubernetes Service と kube-apiserver の関係

明示的に作ったnginx以外にkubernetesというServiceが存在します。これはクラスタ内からKubernetes APIのサーバー (= kube-apiserver) に到達するための固定エンドポイントで、今回触っているkindのローカル環境だとバックエンドにkube-apiserverのPodが存在します。(例えばGKEではGoogle Cloudが管理するコントロールプレーンのエンドポイントになります)

つまり今回のkindの環境においてkube-apiserverはPodであり、Service (kubernetes) がそのフロントに立っているということです。

厳密には、default/kubernetes Serviceは通常のServiceと異なりselectorを持たず、kube-apiserver自身がエンドポイントを直接登録する特殊なServiceです (pkg/controlplane/reconcilers)。本記事では「Serviceの前段にkube-apiserver Podがある」というイメージのまま読み進めて問題ないです。

flowchart LR
    subgraph Cluster["クラスタ"]

        subgraph ControlPlane["コントロールプレーン<br>(kind-control-plane)"]
            direction TB
            etcd["etcd"]
            apiserver["kube-apiserver"]
            scheduler["kube-scheduler"]
            controller["kube-controller-manager"]
        end

        subgraph LeftWorker["ワーカーノード<br>(kind-worker2)"]
            direction TB
            kubeletLeft["kubelet"]
            proxyLeft["kube-proxy"]
        end

        subgraph RightWorker["ワーカーノード<br>(kind-worker)"]
            direction TB
            kubeletRight["kubelet"]
            proxyRight["kube-proxy"]
        end

        ControlPlane ~~~ LeftWorker
        ControlPlane ~~~ RightWorker
    end

    classDef default fill:transparent,stroke:#6b7280
    style Cluster fill:transparent,stroke:#6b7280
    style ControlPlane fill:transparent,stroke:#6b7280
    style LeftWorker fill:transparent,stroke:#6b7280
    style RightWorker fill:transparent,stroke:#6b7280
    style apiserver fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

その点についてちょっとだけ深掘りしてみます。kubernetes Serviceのマニフェストを見てみましょう。

kubectl -n default get service kubernetes -o yaml
# apiVersion: v1
# kind: Service
# metadata:
#   ...
# spec:
#   clusterIP: 10.96.0.1
#   clusterIPs:
#   - 10.96.0.1
#   internalTrafficPolicy: Cluster
#   ipFamilies:
#   - IPv4
#   ipFamilyPolicy: SingleStack
#   ports: # ここに注目
#   - name: https
#     port: 443
#     protocol: TCP
#     targetPort: 6443
#   sessionAffinity: None
#   type: ClusterIP
# status:
#   loadBalancer: {}

spec.ports[]が、Serviceが公開するポートの定義です。このkubernetes Serviceは443/TCPへのアクセスを受け、バックエンドの6443に転送することを期待した設定になっています。転送先のバックエンドの設定も確認してみます。

kubectl get endpointslice -n default -l kubernetes.io/service-name=kubernetes -o yaml
# apiVersion: v1
# items:
# - addressType: IPv4
#   apiVersion: discovery.k8s.io/v1
#   endpoints:
#   - addresses:
#     - 172.18.0.5 # バックエンドの IP アドレス
#     conditions:
#       ready: true
#   kind: EndpointSlice
#   metadata:
#     creationTimestamp: "<TIMESTAMP>"
#     generation: 3
#     labels:
#       kubernetes.io/service-name: kubernetes
#     name: kubernetes
#     namespace: default
#     resourceVersion: "<RESOURCE_VERSION>"
#     uid: <UID>
#   ports:
#   - name: https
#     port: 6443
#     protocol: TCP
# kind: List
# metadata:
#   resourceVersion: ""

items[].endpoints[].addresses[]がバックエンドのIPアドレスです。この環境ではkube-apiserverのPodを指しています。実際にPodを見てみます。

$ kubectl -n kube-system get pod -l component=kube-apiserver -o wide
# NAME                                READY   STATUS    RESTARTS   AGE   IP           NODE                 NOMINATED NODE   READINESS GATES
# kube-apiserver-kind-control-plane   1/1     Running   0          xm    172.18.0.5   kind-control-plane   <none>           <none>

kube-apiserver-kind-control-planeというPodが1つ存在することが確認できました。まとめると、今回のkindの環境においてはkube-apiserverは以下のようになっているということです。

flowchart LR
    podA["Pod-X"] --> svc["Service(kubernetes)<br/>10.96.0.1:443"]

    subgraph backends[" "]
        direction TB
        podB1["kube-apiserverのPod<br/>(kube-apiserver-kind-control-plane)<br/>172.18.0.5:6443"]
    end

    svc --> podB1

    classDef default fill:transparent,stroke:#6b7280
    style backends fill:transparent,stroke:transparent

クラスタ内から Service を経由して Pod に疎通する

話を本筋に戻して、先ほど作ったnginx Serviceにクラスタ内から通信してみましょう。alpineという名前のPodを作って、コンテナに入ります。

kubectl run alpine -it --rm --image alpine -- ash
# All commands and output from this session will be recorded in container logs, including credentials and sensitive information passed through the command prompt.
# If you don't see a command prompt, try pressing enter.
# / #

別ターミナルから確認するとalpineのPodが確認できます。

kubectl get pod
# NAME                     READY   STATUS    RESTARTS   AGE
# alpine                   1/1     Running   0          xs
# nginx-aaaaaaaaaa-ccccc   1/1     Running   0          xm
# nginx-aaaaaaaaaa-ddddd   1/1     Running   0          xm

Kubernetesクラスタでは通常CoreDNS (Kubernetesと同様にCNCFで管理されるOSS37) がDNSサーバーとして動作しており、PodからのDNS問い合わせに応答します。

実際にドメイン名からIPアドレスを引いてみましょう。

/ # nslookup nginx
# Server:         10.96.0.10
# Address:        10.96.0.10:53

# ** server can't find nginx.cluster.local: NXDOMAIN

# Name:   nginx.default.svc.cluster.local
# Address: 10.96.120.128

# ** server can't find nginx.cluster.local: NXDOMAIN


# ** server can't find nginx.svc.cluster.local: NXDOMAIN

# ** server can't find nginx.svc.cluster.local: NXDOMAIN

10.96.120.128が返ってきていることがわかります。これが先ほど作ったnginx Service (ClusterIP) です。

kubectl get service
# NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   xm
#                          # ↓これ
# nginx        ClusterIP   10.96.120.128   <none>        80/TCP    xs

nslookupで指定したnginxは、FQDNではなく短縮名 (相対名) です。alpine PodはdefaultのNamespaceに存在するため、/etc/resolv.confsearchの順に試され、nginx.default.svc.cluster.localが解決されます。ここでいうNamespaceとはLinuxのNamespaceではなく、リソースを論理的に分割するためのKubernetesの機能です。

kubectl exec alpine -- cat /etc/resolv.conf
# search default.svc.cluster.local svc.cluster.local cluster.local
# nameserver 10.96.0.10
# options ndots:5

上記のようにalpine PodのDNSリゾルバを確認してみると10.96.0.10にDNS問い合わせをする設定になっています。なお、10.96.0.10はkind / kubeadmのデフォルトService CIDR (10.96.0.0/12) におけるCoreDNSのClusterIPで、クラスタのService CIDR設定によって変わります。38

また、前述のCoreDNSのPod前段にはkube-dnsというServiceがあり、それはkube-systemという別のNamespaceに存在します。(kube-systemはシステムコンポーネントを配置するNamespaceで、運用や権限管理に使用するようなコンポーネントをアプリ用ワークロードと分離することなどに使われます)

# -n で Namespace を指定
kubectl -n kube-system get service kube-dns
# NAME       TYPE        CLUSTER-IP   EXTERNAL-IP   PORT(S)                  AGE
# kube-dns   ClusterIP   10.96.0.10   <none>        53/UDP,53/TCP,9153/TCP   xh

図で流れをまとめると、nslookup nginxでは裏で以下が行われていたということです。

flowchart TD
  subgraph NS_DEFAULT["Namespace: default"]
    A["Pod: alpine"]
    S["Service: nginx<br/>ClusterIP: 10.96.120.128:80"]
    N1["Pod: nginx-*"]
    N2["Pod: nginx-*"]
    S --> N1
    S --> N2
  end

  subgraph NS_KSYS["Namespace: kube-system"]
    D["Service: kube-dns<br/>ClusterIP: 10.96.0.10:53"]
    C1["Pod: coredns-*"]
    C2["Pod: coredns-*"]
    D --> C1
    D --> C2
  end

  subgraph CP["Control Plane"]
    API["kube-apiserver"]
  end

  A -- "1: nslookup nginx<br/>DNS query" --> D
  C1 -. "Service/Endpoint情報をwatch" .-> API
  C2 -. "Service/Endpoint情報をwatch" .-> API
  C1 -- "2: Aレコード応答<br/>nginx.default.svc.cluster.local<br/>= 10.96.120.128" --> A
  A -- "3: wget nginx:80" --> S
  classDef default fill:transparent,stroke:#6b7280
  style NS_DEFAULT fill:transparent,stroke:#6b7280
  style NS_KSYS fill:transparent,stroke:#6b7280
  style CP fill:transparent,stroke:#6b7280

このような仕組みに基づいてwget nginxでService経由の疎通確認ができるようになっています。

/ # wget -O - nginx
# Connecting to nginx (10.96.120.128:80)
# writing to stdout
# <!DOCTYPE html>
# <html>
# <head>
# <title>Welcome to nginx!</title>
# ...
# </html>
# -                    100% |****************************************************|   615  0:00:00 ETA
# written to stdout

nginxのイメージに入っているindex.htmlの内容が確認できました。

確認が終わったら、alpineのシェルを終了します。--rmを付けて起動しているため、Podも自動で削除されます。

/ # exit

このようにServiceの中でもClusterIPはPodのIPアドレスを抽象化し、安定した接続先を提供する役割を持っています。

ここまで紹介してきませんでしたが、kubectl get allとするとService、Deployment、ReplicaSet、Podが一覧で取得できます。(Deploymentがロールアウトの履歴を保持するため、DESIREDが0であるReplicaSetも表示されます)

kubectl get all
# NAME                         READY   STATUS    RESTARTS   AGE
# pod/nginx-aaaaaaaaaa-ccccc   1/1     Running   0          xm
# pod/nginx-aaaaaaaaaa-ddddd   1/1     Running   0          xm

# NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# service/kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   xh
# service/nginx        ClusterIP   10.96.120.128   <none>        80/TCP    xm

# NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/nginx   2/2     2            2           xh

# NAME                               DESIRED   CURRENT   READY   AGE
# replicaset.apps/nginx-aaaaaaaaaa   2         2         2       xh
# replicaset.apps/nginx-bbbbbbbbbb   0         0         0       xh

Gateway を作る

途中から始めたい方は、サンプルリポジトリ zenn-k8s-handson をcloneしてmake ckpt-05を実行すると、この節の前提状態 (kindクラスタ + nginxのDeployment / Service) を再現できます。

Gateway APIはKubernetesのL4/L7ルーティングを定義するAPI群です39。先ほどまで紹介してきたServiceは、Podの集合に対する安定した接続先を提供するAPIで、今回紹介するGateway APIの転送先にもなります。ServiceもLoadBalancerやNodePortによって外部公開できますが、その責務は基本的に「どのPod群に届けるか」を表すことです。これに対してGateway APIは、その手前で「どこで受けて、どのルールで、どのServiceに流すか」を定義します。

この章ではGateway APIのHTTP/HTTPSのL7ルーティングに絞って解説していきます。(Ingress APIは新機能の追加が止まっており (frozen)、Kubernetesプロジェクトとしては新規実装にはGateway APIの使用が推奨されています40)

まず今回使用するリソースについて紹介します。(これがすべてではなく、他にもあります4142)

  • GatewayClass: Gatewayを処理する実装 (controller) の定義
  • Gateway: どこで受けるか (リスナーやポート) の定義
  • HTTPRoute: 転送ルールの定義

Kubernetesクラスタがリクエストを受ける際の流れとして、概念レベルでシンプルに表現すると以下のようになります。

flowchart LR
  C([client])
  G[Gateway]
  R[HTTPRoute]
  S[Service]
  P1[Pod]
  P2[Pod]

  C -. "HTTP(S) request" .-> G
  G --> R
  R -- "Routing rule" --> S
  S --> P1
  S --> P2
  classDef default fill:transparent,stroke:#6b7280

Gateway APIのリソースは、通信の入口と転送ルールを宣言する設定です。controller (常駐型のプロセス) がGatewayClass / Gateway / HTTPRouteと参照先のServiceなどを監視し、待ち受けポート、ホストやパスのマッチ条件、転送先Serviceといった内容を実装側のデータプレーン (後述します) に反映します。

ここでは実際にGateway APIの各種リソースとロードバランサ (以後、LBと省略します) を作って、クラスタ外部からの通信を確認していきます。最終的に以下図の構成を作ります。(技術的な詳細を省略した概念的な図です)

flowchart TD
  C[Client] --> EGP["External Gateway<br/>(LB + Data Plane)"]
  GC[Gateway Controller]

  subgraph INSIDE["Kubernetes Cluster"]
    GW["Gateway"]
    GWC[GatewayClass]
    HR1["HTTPRoute"]
    HR2["HTTPRoute"]
    N1["Pod: nginx-*"]
    N2["Pod: nginx-*"]
    E1["Pod: echo-*"]
    E2["Pod: echo-*"]
  end

  HR1 -. attaches to .-> GW
  HR2 -. attaches to .-> GW
  GC -. provisions/programs .-> EGP
  GWC -. selects controller .-> GC
  GW -. configured by .-> GC
  EGP -->|Host nginx.local| N1
  EGP -->|Host nginx.local| N2
  EGP -->|Host echo.local| E1
  EGP -->|Host echo.local| E2
  classDef default fill:transparent,stroke:#6b7280
  style INSIDE fill:transparent,stroke:#6b7280

Gateway APIという「外部向けの受け口」を実際に動かすためにはLBの実装が必要です。kindはローカル検証向けであるためLBの実装を持っておらず、今回はその役割を担うcloud-provider-kindというcontrollerを追加します。

前述のように、Gateway APIの各リソースはあくまで宣言であり、仕様のようなものです。cloud-provider-kindはcontrollerとして各リソースをwatchし、以下を用意することで外部から通信を受けられる状態にします。

  • 外部受け口に相当するトンネル / ポートマッピング (LBに相当)
  • EnvoyというL7ルーティングを行うプロキシツール43のコンテナ
  • GatewayClassなどのKubernetesリソース

つまりは図の以下のあたりを用意してくれるということです。

flowchart TD
  C[Client] --> EGP["External Gateway<br/>(LB + Data Plane)"]
  GC[Gateway Controller]

  subgraph INSIDE["Kubernetes Cluster"]
    GW["Gateway"]
    GWC[GatewayClass]
    HR1["HTTPRoute"]
    HR2["HTTPRoute"]
    N1["Pod: nginx-*"]
    N2["Pod: nginx-*"]
    E1["Pod: echo-*"]
    E2["Pod: echo-*"]
  end

  HR1 -. attaches to .-> GW
  HR2 -. attaches to .-> GW
  GC -. provisions/programs .-> EGP
  GWC -. selects controller .-> GC
  GW -. configured by .-> GC
  EGP -->|Host nginx.local| N1
  EGP -->|Host nginx.local| N2
  EGP -->|Host echo.local| E1
  EGP -->|Host echo.local| E2

  classDef default fill:transparent,stroke:#6b7280
  style INSIDE fill:transparent,stroke:#6b7280
  style EGP fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
  style GC fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
  style GWC fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

なおEnvoyやkube-proxyなど通信を処理・転送する実行系のコンポーネントを「データプレーン」と呼ぶことが一般的なようで、上記の図中などでは既に登場していますが、この記事でも以後Envoyが絡むところでデータプレーンという表現を使っていきます。

cloud-provider-kind の起動と GatewayClass の確認

早速cloud-provider-kindを起動します。なお、cloud-provider-kind v0.10.0とkind v0.31.0 / Kubernetes v1.35の組み合わせで動作確認しています。

docker rm -f cloud-provider-kind 2>/dev/null || true
docker run -d --name cloud-provider-kind --rm \
  --network host \
  --cap-add NET_ADMIN \
  -v /var/run/docker.sock:/var/run/docker.sock \
  registry.k8s.io/cloud-provider-kind/cloud-controller-manager:v0.10.0

個人的にはややこしく感じた部分ですが、この1つのコンテナを動かすだけで、裏では先ほど紹介したEnvoyコンテナの起動やGatewayClassの作成などが行われています4445

動いていることを確認します。

docker ps --filter name=cloud-provider-kind
# CONTAINER ID   IMAGE                                                                  COMMAND                  CREATED         STATUS         PORTS     NAMES
# xxxxxxxxxxxx   registry.k8s.io/cloud-provider-kind/cloud-controller-manager:v0.10.0   "/bin/cloud-provider…"   xs ago          Up xs                  cloud-provider-kind

次に、GatewayClassを確認します。前述のように、起動したcloud-provider-kindが自動で同名のGatewayClassを作成してくれています。

kubectl get gatewayclass
# NAME                  CONTROLLER                            ACCEPTED   AGE
# cloud-provider-kind   kind.sigs.k8s.io/gateway-controller   True       xh

マニフェストも見てみます。

kubectl get gatewayclass -o yaml
# apiVersion: v1
# items:
# - apiVersion: gateway.networking.k8s.io/v1
#   kind: GatewayClass
#   metadata:
#     creationTimestamp: "<TIMESTAMP>"
#     generation: 1
#     name: cloud-provider-kind
#     resourceVersion: "<RESOURCE_VERSION>"
#     uid: <UID>
#   spec:
#     controllerName: kind.sigs.k8s.io/gateway-controller # Gateway を処理する controller
#     description: cloud-provider-kind gateway API
#   status:
#     conditions:
#     - lastTransitionTime: "<TIMESTAMP>"
#       message: GatewayClass is accepted by this controller.
#       observedGeneration: 1
#       reason: Accepted
#       status: "True"
#       type: Accepted # 処理される前提が整っている
# kind: List
# metadata:
#   resourceVersion: ""

GatewayClassは「どの実装 (controller) がGatewayを処理するかを表すクラス」と紹介しました。上記マニフェストでいうと、items.spec.controllerNamekind.sigs.k8s.io/gateway-controllerがGatewayを処理するcontrollerであるということです。ただしcontrollerは常駐型のプロセスであり、「controller」というリソースが存在してその名前を示しているわけではありません。これはむしろ、そのプロセスを照合するためのIDのような意味合いが強いです。

また、status.conditions.typeAcceptedになっており、これは既にcontrollerがこのGatewayClassを認識し、処理される前提が整っていることを示しています。

flowchart TD
  C[Client] --> EGP["External Gateway<br/>(LB + Data Plane)"]
  GC[Gateway Controller]

  subgraph INSIDE["Kubernetes Cluster"]
    GW["Gateway"]
    GWC[GatewayClass]
    HR1["HTTPRoute"]
    HR2["HTTPRoute"]
    N1["Pod: nginx-*"]
    N2["Pod: nginx-*"]
    E1["Pod: echo-*"]
    E2["Pod: echo-*"]
  end

  HR1 -. attaches to .-> GW
  HR2 -. attaches to .-> GW
  GC -. provisions/programs .-> EGP
  GWC -. selects controller .-> GC
  GW -. configured by .-> GC
  EGP -->|Host nginx.local| N1
  EGP -->|Host nginx.local| N2
  EGP -->|Host echo.local| E1
  EGP -->|Host echo.local| E2

  classDef default fill:transparent,stroke:#6b7280
  style INSIDE fill:transparent,stroke:#6b7280
  style GC fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px
  style GWC fill:#006cff88,fill-opacity:0.53,stroke:#006cff88,stroke-width:2px

echo の Service と Deployment を追加する

次に、L7レイヤーでのルーティングを確認するため、ServiceとDeploymentを新しく追加します。(nginxの一式も前章で作成済みのものをそのまま使う想定です)

cat > deploy-svc.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: echo
spec:
  type: ClusterIP
  selector:
    app: echo
  ports:
  - port: 80
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
spec:
  replicas: 2
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
      - name: echo
        image: registry.k8s.io/echoserver:1.10
        ports:
        - containerPort: 8080
EOF

今更ですが、Kubernetesのマニフェストは1ファイル1リソースではなく、1ファイルに複数リソースの定義ができます。(同一マニフェスト内ではServiceを先に書くことが推奨されています46)

  • クラスタへの適用
kubectl apply -f deploy-svc.yaml
# deployment.apps/echo created
# service/echo created

現時点で、以下のようになっている想定です。(echoという名前のServiceとDeploymentを作りました)

kubectl get deployment,service
# NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/echo    2/2     2            2           xh
# deployment.apps/nginx   2/2     2            2           xh

# NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# service/echo         ClusterIP   10.96.27.137    <none>        80/TCP    xh
# service/kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   xh
# service/nginx        ClusterIP   10.96.120.128   <none>        80/TCP    xh

Gateway と HTTPRoute を作成して外部から疎通させる

次に、GatewayとHTTPRouteを作ります。前述のようにGatewayは受け口としてのリスナーやポートを、HTTPRouteは転送ルールを定義します。

  • マニフェスト作成
cat > gateway-routing.yaml <<'EOF'
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: web-gateway
spec:
  gatewayClassName: cloud-provider-kind
  listeners:
  - name: http
    protocol: HTTP
    port: 80
    allowedRoutes:
      namespaces:
        from: Same
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-route-nginx
spec:
  parentRefs:
  - name: web-gateway
  hostnames:
  - nginx.local
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: nginx
      port: 80
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: web-route-echo
spec:
  parentRefs:
  - name: web-gateway
  hostnames:
  - echo.local
  rules:
  - matches:
    - path:
        type: PathPrefix
        value: /
    backendRefs:
    - name: echo
      port: 80
EOF
  • クラスタへの適用
kubectl apply -f gateway-routing.yaml
# gateway.gateway.networking.k8s.io/web-gateway created
# httproute.gateway.networking.k8s.io/web-route-nginx created
# httproute.gateway.networking.k8s.io/web-route-echo created
  • 作成したリソースの確認
kubectl get gateway,httproute
# NAME                                            CLASS                 ADDRESS      PROGRAMMED   AGE
# gateway.gateway.networking.k8s.io/web-gateway   cloud-provider-kind   172.18.0.3   True         xs

# NAME                                                  HOSTNAMES         AGE
# httproute.gateway.networking.k8s.io/web-route-echo    ["echo.local"]    xs
# httproute.gateway.networking.k8s.io/web-route-nginx   ["nginx.local"]   xs

GatewayのPROGRAMMEDTrueであり、ADDRESSが付与されていれば、外部から到達できる受け口が構成済みです。

echoの方を例に、HTTPRouteも確認してみます。

kubectl describe httproute web-route-echo
# Name:         web-route-echo
# Namespace:    default
# Labels:       <none>
# Annotations:  <none>
# API Version:  gateway.networking.k8s.io/v1
# Kind:         HTTPRoute
# Metadata:
#   Creation Timestamp:  <TIMESTAMP>
#   Generation:          1
#   Resource Version:    <RESOURCE_VERSION>
#   UID:                 <UID>
# Spec:
#   Hostnames:
#     echo.local
#   Parent Refs:
#     Group:  gateway.networking.k8s.io
#     Kind:   Gateway
#     Name:   web-gateway
#   Rules:
#     Backend Refs:
#       Group:
#       Kind:    Service
#       Name:    echo
#       Port:    80
#       Weight:  1
#     Matches:
#       Path:
#         Type:   PathPrefix
#         Value:  /
# Status:
#   Parents:
#     Conditions:
#       Last Transition Time:  <TIMESTAMP>
#       Message:               Route is accepted.
#       Observed Generation:   1
#       Reason:                Accepted
#       Status:                True
#       Type:                  Accepted
#       Last Transition Time:  <TIMESTAMP>
#       Message:               All references resolved
#       Observed Generation:   1
#       Reason:                ResolvedRefs
#       Status:                True
#       Type:                  ResolvedRefs
#     Controller Name:         kind.sigs.k8s.io/gateway-controller
#     Parent Ref:
#       Group:  gateway.networking.k8s.io
#       Kind:   Gateway
#       Name:   web-gateway
# Events:       <none>

status.parents[].conditions[].TypeAcceptedResolvedRefsになっていますが、これは先ほど作成したHTTPRouteがバックエンド (echo Service) の参照解決に成功していることを意味しています。(ここがBackendNotFoundになっている場合は、Service名やポート番号の参照先が誤っている可能性が高いです)

実際にリクエストを送ってみます。

GW_IP=$(kubectl get gateway web-gateway -o jsonpath='{.status.addresses[0].value}')
echo "$GW_IP"
# kubectl get gateway の ADDRESS で表示された 172.18.0.3

curl -s -H 'Host: nginx.local' "http://${GW_IP}/" | head
curl -s -H 'Host: echo.local'  "http://${GW_IP}/"

nginx.localでnginxのHTML、echo.localでechoのレスポンスが返れば成功です。

cloud-provider-kindで付与されたGatewayのADDRESSは、利用環境によってはそのままホストから到達できない場合があります。私のWSL2環境ではネットワーク経路の問題によるものか、curl http://${GW_IP}が失敗することがありました。その場合は以下のようにGateway用に作成されたEnvoyコンテナ内から疎通確認するのが確実です。

Gateway関連リソースを作成してからEnvoyコンテナが起動するまで数秒〜数十秒かかる場合があります。docker ps --filter 'name=kindccm-gw'でコンテナが起動していることを確認してから以下を実行してください。

GW_C=$(docker ps --filter 'name=kindccm-gw' --format '{{.Names}}')

docker exec "$GW_C" bash -lc '
  exec 3<>/dev/tcp/127.0.0.1/80
  printf "GET / HTTP/1.1\r\nHost: nginx.local\r\nConnection: close\r\n\r\n" >&3
  cat <&3 | head -n 20
'

これでExternal Gateway ⇒ nginx / echo Podの流れで、クラスタ外部からPodまで到達できることが確認できました。

構成の振り返りと EndpointSlice

改めて、ここまでで確認した構成です。(:の後に記載しているのが作成してきたリソース名です)

flowchart TD
  C[Client] --> EGP["External Gateway<br/>(LB + Data Plane):<br/>kindccm-gw-*<br/>(Envoy container)"]
  GC[Gateway Controller:<br/>cloud-provider-kind]

  subgraph INSIDE["Kubernetes Cluster"]
    GW["Gateway:<br/>web-gateway"]
    GWC[GatewayClass:<br/>cloud-provider-kind]
    HR1["HTTPRoute:<br/>web-route-nginx"]
    HR2["HTTPRoute:<br/>web-route-echo"]
    N1["Pod: nginx-*"]
    N2["Pod: nginx-*"]
    E1["Pod: echo-*"]
    E2["Pod: echo-*"]
  end

  HR1 -. attaches to .-> GW
  HR2 -. attaches to .-> GW
  GC -. provisions/programs .-> EGP
  GWC -. selects controller .-> GC
  GW -. configured by .-> GC
  EGP -->|Host nginx.local| N1
  EGP -->|Host nginx.local| N2
  EGP -->|Host echo.local| E1
  EGP -->|Host echo.local| E2
  classDef default fill:transparent,stroke:#6b7280
  style INSIDE fill:transparent,stroke:#6b7280

上記の図は、説明の簡略化のために技術的に割愛している部分がいくつかありました。本当は以下のようになっています。

flowchart TD
  C[Client]
  CPK["Gateway controller:<br/>cloud-provider-kind"]
  GDP["External Gateway<br/>(LB + Data Plane):<br/>kindccm-gw-*<br/>(Envoy container)"]

  subgraph INSIDE["Kubernetes Cluster"]
    KAS[kube-apiserver]

    GWC["GatewayClass:<br/>cloud-provider-kind"]
    GW["Gateway:<br/>web-gateway"]
    HR1["HTTPRoute:<br/>web-route-nginx"]
    HR2["HTTPRoute:<br/>web-route-echo"]

    SVCN["Service:<br/>nginx"]
    SVCE["Service:<br/>echo"]
    EPSN["EndpointSlice:<br/>nginx"]
    EPSE["EndpointSlice:<br/>echo"]

    N1["Pod: nginx-*"]
    N2["Pod: nginx-*"]
    E1["Pod: echo-*"]
    E2["Pod: echo-*"]
  end

  C --> GDP

  GWC -. controllerName .-> CPK
  GW -. gatewayClassName .-> GWC
  HR1 -. parentRefs .-> GW
  HR2 -. parentRefs .-> GW

  HR1 -->|backendRef| SVCN
  HR2 -->|backendRef| SVCE

  SVCN --> EPSN
  SVCE --> EPSE
  EPSN --> N1
  EPSN --> N2
  EPSE --> E1
  EPSE --> E2

  CPK -. watches GatewayClass/Gateway/HTTPRoute/Service/EndpointSlice via API .-> KAS
  CPK -. programs dataplane with xDS .-> GDP

  GDP -->|Host nginx.local via EndpointSlice| N1
  GDP -->|Host nginx.local via...| N2
  GDP -->|Host echo.local via EndpointSlice| E1
  GDP -->|Host echo.local via...| E2
  classDef default fill:transparent,stroke:#6b7280
  style INSIDE fill:transparent,stroke:#6b7280

Gateway controllerは各リソースではなくkube-apiserverをwatchしており、Gateway、Service、EndpointSliceの状態をデータプレーンに反映しています。

EndpointSliceはServiceのバックエンド (PodのIPアドレス・ポート番号・Ready状態など) を保持するAPIリソースであり、これを見れば、どのPodに通信を流せるか確認できます。(EndpointSliceはv1.21でGAとなった、Endpoints (v1) の後継APIです。Kubernetesのバージョン1.33でEndpointsが公式にdeprecated扱いとなりましたが、削除予定はなく、現状は読み書き時に警告が出ます47)

以下のコマンドで、EndpointSlice経由でService配下のPodのIPアドレスを確認できます。

kubectl get endpointslice -l kubernetes.io/service-name=nginx -o wide
# NAME          ADDRESSTYPE   PORTS   ENDPOINTS               AGE
# nginx-xxxxx   IPv4          80      10.244.1.2,10.244.2.2   xh

kubectl get endpointslice -l kubernetes.io/service-name=echo -o wide
# NAME         ADDRESSTYPE   PORTS   ENDPOINTS               AGE
# echo-yyyyy   IPv4          8080    10.244.2.3,10.244.1.2   xh

最後に、今回の流れを簡潔にまとめると次の通りです。

  1. cloud-provider-kindを起動する
  2. GatewayClassが自動作成される
  3. Gatewayで受け口 (ポート / リスナー) を定義する
  4. HTTPRouteでHost / Path毎のバックエンドを定義する
  5. GatewayのADDRESSあてにHostヘッダ付きでリクエストし、到達を確認する

これでL7ルーティングを宣言、外部公開し、状態 (status) で検証するというGateway APIの基本サイクルを一通り確認できました。

クリーンアップ

ここで作ったもののうち、後続の章でもそのまま使うのはecho / nginxのDeploymentとServiceです。一方で、Gateway APIの確認のために作ったGateway、HTTPRoute、そしてcloud-provider-kindcloud-provider-kindが自動作成したGatewayClassはこの先では基本的に使いません。以降も使うecho / nginxは残しつつ、Gateway API固有のものだけ削除しておきます。

kubectl delete httproute web-route-nginx web-route-echo
kubectl delete gateway web-gateway
docker rm -f cloud-provider-kind
kubectl delete gatewayclass cloud-provider-kind

※GatewayClassはcloud-provider-kindが起動したままだと再作成される可能性があるため、上記のように先にコンテナを落としてから実行するのが安全です。

これでGatewayを作る章は終わりです。GatewayClass / Gateway / HTTPRouteによるL7ルーティングを宣言し、GatewayのADDRESS経由でPodまで外部から到達できることまで確認しました。

まとめ

この章ではkind上でPod / ReplicaSet / Deployment / Service / Gatewayを一通り動かし、Reconciliation Loopの挙動も確認しました。次章では、これらの上に乗るIstio・Helm・Prometheusなどの周辺ツールが何を担うのか、概念レベルで整理します。

Kubernetes 周辺用語とツール

Kubernetesについて調べていると、「サービスメッシュ」「Istio」「GitOps」といった言葉を目にすることがあります。最初のうちはそれが設計上の概念なのか、具体的なツールなのか、あるいはどの領域の話なのかわからずツラいです。この章では、そうしたKubernetes周辺の用語について、まずは全体の中での位置づけをつかめるようになることを意識して、概要レベルで整理していきます。

サービスメッシュと Istio、Envoy

サービスメッシュは、マイクロサービス間通信の制御をアプリケーション自身ではなくインフラ側で横断的に扱う仕組みです。Kubernetesクラスタ内のPodに、通信を担うコンテナ (サイドカーと表現されることがあります) を追加し、すべてのネットワークトラフィックをインターセプトするのが代表的な方法です。(以下の図のようにサービス間の相互通信が網 (=メッシュ) のように張り巡らされることからサービスメッシュと呼ばれます)

サービスメッシュのアーキテクチャ Red Hat 公式ブログより

このサイドカーはプロキシとしてサービス間の通信を透過的に処理し、トラフィックの管理 / 分散、暗号化、認証、モニタリング、ロギングなど、様々な機能を提供します。

サービスメッシュを実装する代表的なOSSとしてIstioがあります。コントロールプレーンとデータプレーンという2つのプレーンから成るアーキテクチャになっています。

Istio のアーキテクチャ Istio 公式ドキュメントより

今回紹介するサイドカー形式のIstioではEnvoyがデータプレーンを担います。コントロールプレーンでは、各Podでサイドカーとして動くEnvoyの設定などを行います。なお、Istioではambient modeという各Podにサイドカーを入れないパターンもあります。48

Google CloudではCloud Service Mesh49 というサービスがあり、Istioベースのマネージドなサービスメッシュとして存在します。Cloud Service Mesh で紹介します。

監視、サービスディスカバリと Prometheus、Grafana

Kubernetesはクラスタ自体はもちろんのこと、それを構成するノードやPod、コンテナなど、監視対象が多岐にわたります。さらに、スケールやローリングアップデートなどで、Podの名前やIPアドレスも頻繁に入れ替わります。

ここで重要になるのが「サービスディスカバリ」です。サービスディスカバリは「いま監視すべき対象はどれか」を自動で見つけ続ける仕組みで、上述のように動的なKubernetesの環境では必須の考え方です。(Podに変化が起きるたびに手動で設定更新をするのは非現実的です)

Prometheusは、このサービスディスカバリをKubernetesと組み合わせて実現できる、CNCFにホストされたOSSです50。Prometheusは従来型の「ホストに監視エージェントを追加してメトリクスを監視基盤にPushする方式」とは異なり、Prometheusのサーバー自身が監視対象のメトリクス情報を定期的に取得するPull型です。これによってPod、ノード、Serviceなどの情報を自動的に検出・追従でき、設定を監視側で一元的に管理しやすいのが特徴です。

Prometheus のアーキテクチャ Prometheus 公式ドキュメントより

また、Prometheusなどの監視ツールに保存された時系列データをダッシュボードで可視化できるGrafanaというツールも存在します51

先ほど紹介したIstioと同じく、Managed Service for PrometheusというマネージドなPrometheusがGoogle Cloudで提供されています52Managed Service for Prometheus で紹介します。

マニフェスト管理と Helm、Kustomize

マニフェストをkubectlを使って個別に手動管理していると、Kubernetesの利用が拡大していったときに以下のような課題が出てきます。

  • 環境ごとに手作業が必要で、管理が大変
  • バージョン管理が大変
  • 変更やレビュー、ロールバックなど、運用の負荷が高い
  • 同じKubernetesクラスタの環境を他でも再利用したい

そういった文脈で出てくるツールがHelmとKustomizeです。

Helm

HelmはKubernetesのパッケージマネージャーです。「Chart」という形式でKubernetesアプリケーション (DeploymentやServiceなどのリソース) を再利用可能なパッケージとして扱い、クラスタへの導入・更新・ロールバックを容易にします。また、以下のようなシーンで、その配布・導入を担います。

  • OSS / ベンダーが「このアプリをKubernetesに入れたい人」向けに配る
  • 社内Platformチームが「社内標準のアプリ構成」を各チームに配る
  • 同じアプリを設定違いで複数クラスタに展開する

「OSS / ベンダーが「このアプリをKubernetesに入れたい人」向けに配る」については、雑に表現するとJavaScriptのnpm install xx、Goのgo install xxの要領で、helm install xxするようなイメージです。実際はより複雑で、Helmで外部ツールを入れる場合、以下の手順を踏みます。

  1. Chart保管場所 (Repository) をローカルの設定ファイルに追加 (helm repo add)
  2. 追加したRepositoryから、Chart一覧やバージョン情報などを取得してローカルにキャッシュ (helm repo update)
  3. Chartを取得・展開し、クラスタの実体に反映 (helm install)

例えば、先ほど紹介したサービスメッシュのツールであるIstioは以下のようにインストールして使用します53。(後ほどハンズオンの章でインストール手順含めで紹介するため、コマンド実行は不要です。あくまで手順のイメージの紹介と捉えてください)

# Istio の Repository URL をローカルの設定ファイルに追加
helm repo add istio https://istio-release.storage.googleapis.com/charts
# Istio の Repository から、Chart 一覧やバージョン情報などを取得してローカルにキャッシュ
helm repo update

ローカルに追加されたそれぞれのファイルを簡単に見てみます。

  • それぞれの場所を確認
helm env | grep -E 'HELM_REPOSITORY_(CONFIG|CACHE)'
# HELM_REPOSITORY_CACHE="/home/xxx/.cache/helm/repository"
# HELM_REPOSITORY_CONFIG="/home/xxx/.config/helm/repositories.yaml"
  • helm repo add istio https://istio-release.storage.googleapis.com/chartsでIstioのRepositoryのURLを追加した設定ファイル
cat ~/.config/helm/repositories.yaml
# apiVersion: ""
# generated: "0001-01-01T00:00:00Z"
# repositories:
# - caFile: ""
#   certFile: ""
#   insecure_skip_tls_verify: false
#   keyFile: ""
#   name: istio
#   pass_credentials_all: false
#   password: ""
#   url: https://istio-release.storage.googleapis.com/charts
#   username: ""
  • helm repo updateでローカルキャッシュした、IstioのRepositoryで配布されるChartの一覧やバージョン情報
$ head -n 40 ~/.cache/helm/repository/istio-index.yaml
# apiVersion: v1
# entries:
#   ambient:
#   - apiVersion: v2
#     appVersion: 1.29.0
#     created: "2026-02-16T18:39:04.801438006Z"
#     dependencies:
#     - name: base
#       repository: file://../../charts/base
#       version: 1.29.0
#     - name: cni
#       repository: file://../../charts/istio-cni
#       version: 1.29.0
#     - condition: istiod.enabled
#       name: istiod
#       repository: file://../../charts/istio-control/istio-discovery
#       version: 1.29.0
#     - name: ztunnel
#       repository: file://../../charts/ztunnel
#       version: 1.29.0
#     description: Helm umbrella chart for ambient
#     digest: <DIGEST>
#     icon: https://istio.io/latest/favicons/android-192x192.png
#     keywords:
#     - istio-cni
#     - istio
#     - ambient
#     kubeVersion: '>= 1.23.0-0'
#     name: ambient
#     sources:
#     - https://github.com/istio/istio
#     type: application
#     urls:
#     - https://istio-release.storage.googleapis.com/charts/samples/ambient-1.29.0.tgz
#     version: 1.29.0
#   - apiVersion: v2
#     appVersion: 1.29.0-rc.3
#     created: "2026-02-11T11:21:17.292156895Z"
#     dependencies:
#     - name: base

これらのChartをクラスタにインストールします。

# クラスタにインストール
helm install istio-base istio/base --namespace istio-system --create-namespace
  • istio-base: インストール実体につける任意の管理名 (helm upgrade/delete/statusなどに使用する)
  • istio/base: インストールするパッケージ名
  • --namespace istio-system: インストール先のNamespace

Helmではこのような流れでパッケージを取得し、クラスタに適用していきます。

Kustomize

Kustomizeはマニフェストの差分を管理するKubernetesネイティブなツールで、kubectlからデフォルトで使用できます。Kustomizeは「完成済みの既存マニフェストに対してパッチを当てる」のようなニュアンスがあり、環境差を分離しやすくなります。例えば以下のようにして環境差を管理します。

kustomize/
├─ base/
│  ├─ deployment.yaml
│  └─ kustomization.yaml
└─ overlays/
   └─ dev/
      ├─ kustomization.yaml
      └─ patch-deployment.yaml
   └─ stg/
      ├─ kustomization.yaml
      └─ patch-deployment.yaml

「完成済みの既存マニフェストに対してパッチを当てる」のようなニュアンスとお伝えしましたが、具体的には「base/が完成済みのマニフェスト、overlays/でパッチを当てる」という構成です。

例えば以下のbase/deployment.yamlはKustomize関係なくDeploymentのマニフェストとして成立しています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: ghcr.io/example/myapp:1.0.0
        env:
        - name: LOG_LEVEL
          value: info

base/kustomization.yamlでbaseで扱うパッチ対象のマニフェストを宣言します。

resources:
- deployment.yaml

overlays/dev/kustomization.yamlで、どのbaseを取り込むか、どのパッチを当てるかを宣言します。

resources:
- ../../base
patches:
- path: patch-deployment.yaml

そしてoverlays/dev/patch-deployment.yamlでDeploymentに対する差分そのものを記述します。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1
  template:
    spec:
      containers:
      - name: app
        image: ghcr.io/example/myapp:1.0.0-dev
        env:
        - name: LOG_LEVEL
          value: debug

このようにして、既存の完成形マニフェストに対するアドオンで環境差異を管理できるのがKustomizeの特徴の1つです。

Helm と Kustomize は競合する存在ではなく、併用する

どちらのツールも「適用可能なKubernetesマニフェストを作る」という観点では役割が重複していて、例えば環境差異を管理するだけならどちらかを使えば良いです。(おそらく、それだけならKustomizeの方が学習コストも低く、手軽ではあります)

ただし先ほどもHelmの紹介でお伝えしたように、外部ツールの導入方法としてHelmは有力な選択肢で、配布・更新・ロールバックをまとめて扱いたい場面でよく使われます。つまり以下のような役割分担での併用が起き得るということです。

  • Helm: アプリを配る / 入れる
  • Kustomize: 同じ構成を環境ごとにズラす

マニフェスト管理の観点のみでの比較であれば、以下の記事がわかりやすかったです。

https://zenn.dev/yokoo_an209/articles/6d23ee506bc007

GitOps と Argo CD、Flux

GitOpsは、誤解を恐れず簡潔に表現すると、インフラのデプロイ方法についてのトピックです。根本として押さえておくべきなのは「Push型か、Pull型か」です。

  • CIOps: CIツールがKubernetesクラスタ外部からPush型でデプロイする
  • GitOps: Gitなどのバージョン管理システムをSource of Truth (SoT) として、クラスタ内部からPull型で同期、デプロイする

※CIOpsという言葉はGitOpsとの対比で使われることはありますが、OpenGitOps (CNCFのワーキンググループ54) や各ツールの公式ドキュメントで広く標準用語として扱われているわけではなさそうなので、本記事ではPush型CDを指す便宜的な表現として使います。

GitOpsは単に「Gitを使ってデプロイすること」ではなく、OpenGitOpsでは「宣言的に記述されたdesired stateをバージョン管理されたSoTに置き、ソフトウェアエージェントが自動的に取得して、実際の状態と継続的にreconcileする」という考え方として整理されています55。CNCF Glossary (CNCFが管理するクラウド技術に関する用語集) でも、GitOpsは「Gitなどのバージョン管理システムに定義されたdesired stateとactual stateを継続的に比較・調整するプラクティス」として説明されています。56

mainブランチへのマージや特定ブランチへのプッシュなど、git pushを起点としてGitHub Actionsなどのワークフローでクラスタのデプロイ (kubectl applyhelm upgrade) まで実行されるCDは、Push型としてCIOpsに分類されます。CIOpsでは、CI実行基盤が本番クラスタを変更できる権限を持つことになります。GitHub ActionsのOIDCやGoogle CloudのWorkload Identity Federationを使えば長期鍵を置かずに認証できますが、発行元リポジトリやブランチなどの条件設定が広すぎると、意図しないワークフローから権限を使われるリスクがあります。5758

flowchart LR
    Dev[開発者] -->|git push| Repo[Git Repository]
    Repo -->|トリガー| CI[CI/CD]
    CI -->|Pushで適用| Cluster[Kubernetes Cluster]
    classDef default fill:transparent,stroke:#6b7280

GitOpsは、Kubernetesクラスタ (正確にはその内部で動くcontroller) がリポジトリの変更をPullします。そのため、デプロイに必要な権限をクラスタ外部のCI基盤から分離できます。

flowchart LR
    Dev[開発者] -->|git push| Repo[Git Repository]
    Cluster[Kubernetes Cluster] -->|Pullで同期| Repo
    Controller[GitOps Controller] --- Cluster
    classDef default fill:transparent,stroke:#6b7280

また、GitHub Actionsのワークフローなどによる標準的なCD (=CIOps) では、このワークフロー以外から適用された変更のドリフト検知 / 自動修復は、デプロイを担うワークフローの標準的な責務には含まれません。例えばクラスタに対する直接のkubectl applyによる変更は、基本的に次のデプロイまで検知・修正されないということです。GitOpsでは、KubernetesのReconciliation Loopの考え方を取り入れ、Gitなどに置かれたdesired stateと実クラスタとの差分をcontrollerが継続的に確認し、同期します。(同期は必ずしも自動とは限らず、場合によっては手動にしておくこともあります)

GitOpsを推すようなことばかり書いてしまいましたが、CIOpsはデプロイ手順の柔軟性や、Kubernetesを含めたデプロイフローを1つのCI/CDで管理できる点など、シンプルさでは優位です。そのため、環境やクラスタ、サービスの増加で差分管理がツラくなってきたタイミングや、組織としてアプリとインフラ / プラットフォーム / SREなどでチームが分かれるタイミングで検討すると良さそうだと感じました。

Kubernetes環境でGitOpsを実現するツールとしての有名どころがArgo CDとFluxです。(Spinnakerというツールもありますが、これはKubernetes特化ではありません)

マニフェストへの制約と Gatekeeper、Kyverno

Kubernetesの設定はマニフェストの記述によって行われます。つまり、クラスタに適用されるマニフェストの内容に制約を設けることは、セキュリティやガバナンスにおける重要な一側面です。前提として、Kubernetesクラスタに対するCREATEUPDATE系の変更が適用されるまでには以下のような流れをたどります。59

  1. kube-apiserverでAPIリクエストを受ける
  2. 認証 / 認可
  3. 事前設定したルールに基づきオブジェクトを変更
  4. オブジェクトとAPIスキーマの一致 (Kubernetesのオブジェクトとして正しいか) を検証
  5. 事前設定したルールに基づきオブジェクトを検証
  6. 上記すべてが通過した場合のみetcdに保存
  7. APIレスポンス

図で表現すると以下の通りです。(未解説の単語を多数出していますが、話の筋には影響しないのでひとまず割愛します)

flowchart TD
    A[API リクエスト] --> B[認証]
    B --> C["認可<br/>(RBAC etc.)"]

    C --> D["オブジェクト変更<br/>(LimitRangeなどbuilt-inのもの)"]
    D --> DQ{"変更webhookが設定されていて条件と一致するか"}
    DQ -->|Yes| DW["変更Webhook(s)の呼び出し<br/>(例: Gatekeeper/Kyverno)"]
    DQ -->|No| E
    DW --> E["オブジェクトスキーマ検証<br/>(組み込みOpenAPI/CRDスキーマ)"]

    E --> V["オブジェクト検証<br/>(LimitRanger/PSA/VAPなどbuilt-inのもの)"]
    V --> VQ{"オブジェクト検証 webhookが設定されていて条件と一致するか"}
    VQ -->|Yes| VW["検証webhook(s)の呼び出し<br/>(例: Gatekeeper/Kyverno)"]
    VQ -->|No| F
    VW --> F["etcdに永続化<br/>(すべて成功した場合のみ)"]

    F --> G[API レスポンス]
    classDef default fill:transparent,stroke:#6b7280

※後ほど紹介するツールであるGatekeeperはv3.10から変更 (Mutation) もstableに昇格しており60、実運用では検証 (Validating) が中心と考えられますがMutatingのところにも記載しています。

以上の流れを簡単にまとめると、Kubernetesクラスタに変更が適用される (etcdに保存される) までには、ルールに基づいてマニフェストの変更 (Mutating) と検証 (Validating) が実施できるので、意図せぬ変更が適用されないように、その2フェーズで制約を設けましょうということです。例えば、以下のようなシーンでマニフェストへの制約を加えたくなります。

  • 特定Namespaceのリソースには特定のラベルを必須で付与したいが、手動だと漏れがあるので自動化したい
  • 信頼できる特定のコンテナレジストリのみ使用できるようにしたい
  • 機密データを扱う特定Namespaceでは、特定グループに属する開発者のみPodの作成や更新、コンテナへの接続ができるようにしたい
  • Ingressのホスト名が他のチームと重複していたら作成を拒否したい
  • 特権モードで動作するコンテナを作成できないようにしたい

これらはKubernetesのbuilt-in機能 (LimitRange61 やPSA62、VAP63 など) で対応できるものもありますが、できないものもあります。そういった場合に外部ツールを使います。その有名どころがGatekeeperやKyvernoといったツールです。

こういったツールはクラスタ内部にWebhookのサーバーを立て、オブジェクト変更や検証のタイミングでクラスタから呼び出すように設定して使います。例えば以下は、Kyvernoのアーキテクチャです。

Kyverno のアーキテクチャ Kyverno 公式ドキュメントより

ここまで、サービスメッシュ (Istio / Cloud Service Mesh / Envoy)、監視 (Prometheus / Grafana / Managed Service for Prometheus)、マニフェスト管理 (Helm / Kustomize)、GitOps (Flux / Argo CD)、マニフェストへの制約 (Admission Controller / Gatekeeper / Kyverno) の5つの領域を概観してきました。次の章では、これらをkind上で実際にHelm ⇒ Istio ⇒ Prometheus ⇒ Kustomize ⇒ Flux ⇒ Gatekeeperの順に導入・動作確認していきます。

Helm/Istio/Prometheus/Kustomize/Gatekeeper/Flux を kind で動かす

先ほど紹介してきた各種ツールをkindでローカルで試していきましょう。ローカルで触る Kubernetesで作成・使用してきたクラスタや各種リソースをそのまま使っていきます。

前提

各種ツールのインストール

この章では、前半で導入済みのDocker、kind、kubectlに加えて、以下のツールを使用します。

  • helm: Helm Chartの取得とインストールに使用
  • flux: Fluxのbootstrapや同期状態の確認に使用
  • istioctl: Istioの状態確認に使用

インストール方法は色々ありますが、ここでは公式ドキュメントに沿った方法で進めます。646566

Helm

Helmは公式インストールスクリプトを使います。

curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-4
chmod 700 get_helm.sh
./get_helm.sh

Flux CLI

Flux CLIは公式インストーラを使います。

curl -s https://fluxcd.io/install.sh | sudo bash

istioctl

istioctlは公式のインストールスクリプトを使います。この記事では後続の手順でIstio 1.29.0を使うため、ここでもそのバージョンを揃えておきます。なお、シェルがBashである前提で~/.bashrcを更新しています。

ISTIO_VERSION=1.29.0
curl -sL https://istio.io/downloadIstioctl | ISTIO_VERSION=${ISTIO_VERSION} sh -

# istioctl が `$HOME/.istioctl/bin` 配下に展開されていることを確認
ls $HOME/.istioctl/bin

grep -qxF 'export PATH="$HOME/.istioctl/bin:$PATH"' ~/.bashrc || \
  echo 'export PATH="$HOME/.istioctl/bin:$PATH"' >> ~/.bashrc
source ~/.bashrc

ツールのバージョン

  • 以下バージョンがこの章の前提です。
kubectl version --client
# Client Version: v1.35.1
# Kustomize Version: v5.7.1

kind version
# kind v0.31.0 go1.25.5 linux/amd64

docker version --format '{{.Client.Version}}'
# 28.4.0

helm version
# version.BuildInfo{Version:"v4.1.1", GitCommit:"<GIT_COMMIT>", GitTreeState:"clean", GoVersion:"go1.25.7", KubeClientVersion:"v1.35"}

flux --version
# flux version 2.8.3

istioctl version --remote=false
# client version: 1.29.0

クラスタの状態

  • 以下の状態がこの章の前提です。
kubectl get deployment,service,pod
# NAME                    READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/echo    2/2     2            2           xh
# deployment.apps/nginx   2/2     2            2           xh

# NAME                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
# service/echo         ClusterIP   10.96.27.137    <none>        80/TCP    xh
# service/kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   xh
# service/nginx        ClusterIP   10.96.120.128   <none>        80/TCP    xh

# NAME                         READY   STATUS    RESTARTS      AGE
# pod/echo-dddddddddd-ccccc    1/1     Running   0             xh
# pod/echo-dddddddddd-ddddd    1/1     Running   0             xh
# pod/nginx-aaaaaaaaaa-ccccc   1/1     Running   0             xh
# pod/nginx-aaaaaaaaaa-ddddd   1/1     Running   0             xh

Istio によるサービスメッシュ

途中から始めたい方は、サンプルリポジトリ zenn-k8s-handson をcloneしてmake ckpt-10を実行すると、この章のクラスタ前提状態 (kindクラスタ + nginx / echoのDeployment / Service) を再現できます。なお、この章で使うCLI (helm / flux / istioctl) は本章冒頭の前提節を先に確認してインストールしてください。

まず、おさらいです。

  • サービスメッシュはマイクロサービス間の通信制御を、アプリケーション自身ではなくインフラ側で横断的に担うレイヤー
  • 通信を担うコンテナ (=サイドカー) をPodに追加し、ネットワークトラフィックをインターセプトする方法が代表的
  • サイドカーはトラフィックの管理 / 分散、暗号化、認証、モニタリング、ロギングなど、様々な機能を提供する

この章ではIstioでサービスメッシュを導入して「トラフィックの管理 / 分散」をピックアップして紹介します。

istio-base / istiod を Helm でクラスタに導入する

まずHelmを用いてIstioをクラスタに反映していきます。これもおさらいですが、HelmはKubernetesのパッケージマネージャーです。Chartという形式でKubernetesアプリケーションを再利用可能なパッケージとして扱い、クラスタへの導入・更新・ロールバックを容易にします。また、以下の手順でクラスタに反映します。(詳しくはマニフェスト管理と Helm、Kustomizeの章を確認してください)

  1. Chart保管場所 (Repository) をローカルの設定ファイルに追加
  2. 追加したRepositoryから、Chart一覧やバージョン情報などを取得してローカルにキャッシュ
  3. Chartを取得・展開し、クラスタの実体に反映

ではさっそくIstioをクラスタに反映していきます。

# Istio の Repository URL をローカルの設定ファイルに追加
helm repo add istio https://istio-release.storage.googleapis.com/charts
# Istio の Repository から、Chart 一覧やバージョン情報などを取得してローカルにキャッシュ
helm repo update
# ローカルにキャッシュされた Istio のバージョン確認
helm search repo istio/istiod --versions | head -n 5
# NAME                    CHART VERSION   APP VERSION     DESCRIPTION
# istio/istiod            1.29.0          1.29.0          Helm chart for istio control plane
# istio/istiod            1.28.4          1.28.4          Helm chart for istio control plane
# istio/istiod            1.28.3          1.28.3          Helm chart for istio control plane
# istio/istiod            1.28.2          1.28.2          Helm chart for istio control plane

再現性を担保するため、バージョンを固定しておきます。

ISTIO_VERSION=1.29.0

まずはIstioの基盤となるistio/baseというChartをupgrade --installコマンドでクラスタに反映します。upgrade --installはクラスタに既にChartが存在すれば更新、なければ新規作成するコマンドです。

# istio-system という Namespace を新規作成して Chart を反映
helm upgrade --install istio-base istio/base \
  -n istio-system --create-namespace \
  --set defaultRevision=default \
  --version ${ISTIO_VERSION} --wait
# Release "istio-base" does not exist. Installing it now.
# NAME: istio-base
# LAST DEPLOYED: <DATETIME>
# NAMESPACE: istio-system
# STATUS: deployed
# REVISION: 1
# DESCRIPTION: Install complete
# TEST SUITE: None
# NOTES:
# Istio base successfully installed!

# To learn more about the release, try:
#   $ helm status istio-base -n istio-system
#   $ helm get all istio-base -n istio-system

こういった外部パッケージを使用する際などに出てくるのがCustom Resource Definition (CRD) で、これはKubernetes APIに新しいリソースの型を追加する仕組みです。例えば、DeploymentやServiceなど、ローカルで触る Kubernetesで紹介してきたリソースはKubernetes組み込みですが、それ以外のリソースを追加したい場合に使用されます。(実はGateway を作るで紹介したGatewayも組み込みではなくカスタムリソースです)

istio/baseで追加したCRDは、後ほど紹介しますがVirtualService、DestinationRule、Gatewayなどです。これらを先に追加しておかないと、実体のインストールができません。

次に、istiodというコントロールプレーンを追加します。

helm upgrade --install istiod istio/istiod \
  -n istio-system \
  --version ${ISTIO_VERSION} \
  --wait
# Release "istiod" does not exist. Installing it now.
# NAME: istiod
# LAST DEPLOYED: <DATETIME>
# NAMESPACE: istio-system
# STATUS: deployed
# REVISION: 1
# DESCRIPTION: Install complete
# TEST SUITE: None
# NOTES:
# "istiod" successfully installed!

# To learn more about the release, try:
#   $ helm status istiod -n istio-system
#   $ helm get all istiod -n istio-system

# Next steps:
#   * Deploy a Gateway: https://istio.io/latest/docs/setup/additional-setup/gateway/
#   * Try out our tasks to get started on common configurations:
#     * https://istio.io/latest/docs/tasks/traffic-management
#     * https://istio.io/latest/docs/tasks/security/
#     * https://istio.io/latest/docs/tasks/policy-enforcement/
#   * Review the list of actively supported releases, CVE publications and our hardening guide:
#     * https://istio.io/latest/docs/releases/supported-releases/
#     * https://istio.io/latest/news/security/
#     * https://istio.io/latest/docs/ops/best-practices/security/

# For further documentation see https://istio.io website

上記コマンドで、以下画像におけるコントロールプレーンのistiodを追加したということです。(istiodについては後ほど簡単に説明します)

Istio のアーキテクチャ Istio 公式ドキュメントより

コントロールプレーンのPodが存在しているか確認します。

kubectl get pod -n istio-system
# NAME                      READY   STATUS    RESTARTS   AGE
# istiod-gggggggggg-aaaaa   1/1     Running   0          xm

istiod-...という名前のPodが確認できました。

サイドカー注入を有効化して既存 Pod を更新する

次に、defaultのNamespaceに作成されるPodにIstioのサイドカーコンテナが自動で注入 (Istio公式でInjectionという表現が使われています) されるようにistio-injection=enabledというラベルを付けます。67

kubectl label namespace default istio-injection=enabled --overwrite
# namespace/default labeled

既存のPodには自動でサイドカーが注入されないため、再作成します。

kubectl rollout restart deployment/echo deployment/nginx -n default
kubectl rollout status deployment/echo -n default
kubectl rollout status deployment/nginx -n default
# deployment.apps/echo restarted
# deployment.apps/nginx restarted
# Waiting for deployment "echo" rollout to finish: 1 out of 2 new replicas have been updated...
# Waiting for deployment "echo" rollout to finish: 1 out of 2 new replicas have been updated...
# Waiting for deployment "echo" rollout to finish: 1 out of 2 new replicas have been updated...
# Waiting for deployment "echo" rollout to finish: 1 old replicas are pending termination...
# Waiting for deployment "echo" rollout to finish: 1 old replicas are pending termination...
# deployment "echo" successfully rolled out
# Waiting for deployment "nginx" rollout to finish: 1 old replicas are pending termination...
# Waiting for deployment "nginx" rollout to finish: 1 old replicas are pending termination...
# deployment "nginx" successfully rolled out

Podにサイドカーが注入されたか確認します。

kubectl get pod -n default -l app=echo \
  -o jsonpath='{range .items[*]}{.metadata.name}{" => init:"}{.spec.initContainers[*].name}{" / app:"}{.spec.containers[*].name}{"\n"}{end}'

kubectl get pod -n default -l app=nginx \
  -o jsonpath='{range .items[*]}{.metadata.name}{" => init:"}{.spec.initContainers[*].name}{" / app:"}{.spec.containers[*].name}{"\n"}{end}'
# echo-eeeeeeeeee-aaaaa => init:istio-init istio-proxy / app:echo
# echo-eeeeeeeeee-bbbbb => init:istio-init istio-proxy / app:echo
# nginx-ffffffffff-aaaaa => init:istio-init istio-proxy / app:nginx
# nginx-ffffffffff-bbbbb => init:istio-init istio-proxy / app:nginx

istio-proxyというコンテナがそれぞれのPodに存在することが確認できました。これが注入されたサイドカーです。※今回の環境ではistio-proxyinitContainersとして確認できましたが、クラスタによってはcontainers側に表示される場合もあります。差異がある場合はkubectl describe pod -l app=echoなどで確認してください。

istioctl と xDS でメッシュの状態を確認する

IstioのCLIツールであるistioctlでメッシュ全体の状況を確認します。未インストールの場合は、この章の前提を先に実施してください。

istioctl proxy-status
# NAME                              CLUSTER        ISTIOD                      VERSION     SUBSCRIBED TYPES
# echo-eeeeeeeeee-aaaaa.default     Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# echo-eeeeeeeeee-bbbbb.default     Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# nginx-ffffffffff-aaaaa.default    Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# nginx-ffffffffff-bbbbb.default    Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)

これは、echonginxのサイドカーがistiod-gggggggggg-aaaaaという名前のistiodに接続していて、4種類のxDSリソース (CDSLDSEDSRDS) を購読していることを表しています。

xDSは、istiodが各サイドカーにいるEnvoyに設定を配るための制御用API群の総称で、「どの宛先に流すか」「どのリスナー / ルート / エンドポイントを使うか」をEnvoyに配ります。上記で見えていたのは以下です。(xDSのDSはDiscovery Serviceの略です68)

  • CDS: Cluster Discovery Service
  • LDS: Listener Discovery Service
  • EDS: Endpoint Discovery Service
  • RDS: Route Discovery Service

istiodはこれらの制御用APIを用いてServiceやPod、Istioのカスタムリソース (VirtualService、DestinationRule、Gateway) などをwatchします。そしてEnvoyが理解できるxDSの設定に変換したうえで各Proxyに配ったり、mTLS用の証明書を配ったりを担当しています69。詳細は割愛しますが、kubectl logs -n istio-system deployment/istiod --tail=30などのコマンドでistiodのログが出力できます。

Ingress Gateway と VirtualService で外部公開する

次に、外部からのアクセス用にGatewayを作ります。Gateway を作るではGateway、GatewayClass、HTTPRouteを作りましたが、前述のようにIstioでは専用のAPIが存在します。役割としては似ているため、そこまで詳細に解説しません。まずはGateway関連のChartを適用します。

helm upgrade --install istio-ingressgateway istio/gateway \
  -n istio-ingress --create-namespace \
  --version ${ISTIO_VERSION}
# Release "istio-ingressgateway" does not exist. Installing it now.
# NAME: istio-ingressgateway
# LAST DEPLOYED: <DATETIME>
# NAMESPACE: istio-ingress
# STATUS: deployed
# REVISION: 1
# DESCRIPTION: Install complete
# TEST SUITE: None
# NOTES:
# "istio-ingressgateway" successfully installed!

# To learn more about the release, try:
#   $ helm status istio-ingressgateway -n istio-ingress
#   $ helm get all istio-ingressgateway -n istio-ingress

# Next steps:
#   * Deploy an HTTP Gateway: https://istio.io/latest/docs/tasks/traffic-management/ingress/ingress-control/
#   * Deploy an HTTPS Gateway: https://istio.io/latest/docs/tasks/traffic-management/ingress/secure-ingress/

以下のDeployment、Service、Podが作成されます。

kubectl get deployment,service,pod -n istio-ingress
# NAME                                   READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/istio-ingressgateway   1/1     1            1           xm

# NAME                           TYPE           CLUSTER-IP      EXTERNAL-IP   PORT(S)                                      AGE
# service/istio-ingressgateway   LoadBalancer   10.96.110.245   <pending>     15021:30896/TCP,80:31151/TCP,443:32709/TCP   xm

# NAME                                       READY   STATUS    RESTARTS   AGE
# pod/istio-ingressgateway-hhhhhhhhh-aaaaa   1/1     Running   0          xm

次にGatewayとVirtualServiceのリソースを作ります。Gatewayは通信を受けるポートと扱うホストの定義70、VirtualServiceは入ってきたリクエストをどこに流すかのルール71です。上記Chartで適用したのがDeploymentなどの実体であり、その設定やルールを定義するイメージです。

cat > gateway-basic.yaml <<'EOF'
apiVersion: networking.istio.io/v1
kind: Gateway
metadata:
  name: web-gateway
  namespace: default
spec:
  selector:
    istio: ingressgateway
  servers:
  - port:
      number: 80
      name: http
      protocol: HTTP
    hosts:
    - "*"
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: web-route
  namespace: default
spec:
  hosts:
  - "*"
  gateways:
  - web-gateway
  http:
  - match:
    - uri:
        prefix: /nginx
    rewrite:
      uri: /
    route:
    - destination:
        host: nginx.default.svc.cluster.local
        port:
          number: 80
  - match:
    - uri:
        prefix: /echo
    rewrite:
      uri: /
    route:
    - destination:
        host: echo.default.svc.cluster.local
        port:
          number: 80
EOF

クラスタに適用します。

kubectl apply -f gateway-basic.yaml
# gateway.networking.istio.io/web-gateway created
# virtualservice.networking.istio.io/web-route created

外部から叩けるか確認してみましょう。

kubectl -n istio-ingress port-forward service/istio-ingressgateway 18080:80
# Forwarding from 127.0.0.1:18080 -> 80
# Forwarding from [::1]:18080 -> 80

別ターミナルから以下を実行して、中身が見えることを確認します。

curl -s http://localhost:18080/nginx | head
curl -s http://localhost:18080/echo | head

改めてメッシュの状態を確認してみます。

istioctl proxy-status
# NAME                                                   CLUSTER        ISTIOD                      VERSION     SUBSCRIBED TYPES
# echo-eeeeeeeeee-aaaaa.default                          Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# echo-eeeeeeeeee-bbbbb.default                          Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# istio-ingressgateway-hhhhhhhhh-aaaaa.istio-ingress     Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# nginx-ffffffffff-aaaaa.default                         Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)
# nginx-ffffffffff-bbbbb.default                         Kubernetes     istiod-gggggggggg-aaaaa     1.29.0      4 (CDS,LDS,EDS,RDS)

先ほどistio/gatewayで追加したPod (istio-ingressgateway-...) もIstioに認識されていることがわかります。

DestinationRule と subset で重み付きルーティングする

次にDestinationRuleというリソースを追加して、重み付きのルーティングを設定してみます。今回はversionがv1とv2のDeploymentを作り、v1に90%、v2に10% というルールのルーティングを試します。説明のわかりやすさのため、Gateway以外の一式を新規で作ります。

cat > echo-split.yaml <<'EOF'
apiVersion: v1
kind: Service
metadata:
  name: echo-split
  namespace: default
spec:
  type: ClusterIP
  selector:
    app: echo-split
  ports:
  - name: http
    port: 80
    targetPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-split-v1
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo-split
      version: v1
  template:
    metadata:
      labels:
        app: echo-split
        version: v1
    spec:
      containers:
      - name: echo
        image: hashicorp/http-echo:1.0.0
        args: ["-listen=:8080", "-text=v1"]
        ports:
        - containerPort: 8080
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo-split-v2
  namespace: default
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo-split
      version: v2
  template:
    metadata:
      labels:
        app: echo-split
        version: v2
    spec:
      containers:
      - name: echo
        image: hashicorp/http-echo:1.0.0
        args: ["-listen=:8080", "-text=v2"]
        ports:
        - containerPort: 8080
---
apiVersion: networking.istio.io/v1
kind: DestinationRule
metadata:
  name: echo-split
  namespace: default
spec:
  host: echo-split.default.svc.cluster.local
  subsets:
  - name: v1
    labels:
      version: v1
  - name: v2
    labels:
      version: v2
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: echo-split-mesh
  namespace: default
spec:
  hosts:
  - echo-split
  - echo-split.default.svc.cluster.local
  http:
  - route:
    - destination:
        host: echo-split.default.svc.cluster.local
        subset: v1
        port:
          number: 80
      weight: 90
    - destination:
        host: echo-split.default.svc.cluster.local
        subset: v2
        port:
          number: 80
      weight: 10
---
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: echo-split-gateway
  namespace: default
spec:
  hosts:
  - "*"
  gateways:
  - web-gateway
  http:
  - match:
    - uri:
        prefix: /split
    route:
    - destination:
        host: echo-split.default.svc.cluster.local
        subset: v1
        port:
          number: 80
      weight: 90
    - destination:
        host: echo-split.default.svc.cluster.local
        subset: v2
        port:
          number: 80
      weight: 10
EOF

DestinationRuleは「特定の宛先Serviceに対して、どう接続するか」を定義するリソースです72。VirtualServiceが「どこへ流すか」を決めるのに対しDestinationRuleは「その宛先の中身をどう扱うか」を決めるイメージです。今回の例で具体的にすると以下です。

  • DestinationRule: echo-splitというhostの中にversion=v1version=v2subsetがあると定義する
  • VirtualService: DestinationRuleの設定をもとにecho-split Serviceへの通信をversion=v1に90、version=v2には10というルールで振り分けると定義する

DestinationRuleでは、1つのhost配下にあるエンドポイント群 (≠ Pod群) を、ラベルで分類された「名前付きの部分集合」としてsubsetと呼びます。hostが「どのサービスか」、subsetが「そのサービスの中のどのバージョンか」です。例えば今回はhost: echo-split.default.svc.cluster.localに対して、version=v1なPod群をsubset: v1version=v2なPod群をsubset: v2と名付けています。

では上記のマニフェストを適用します。

kubectl apply -f echo-split.yaml
# service/echo-split created
# deployment.apps/echo-split-v1 created
# deployment.apps/echo-split-v2 created
# destinationrule.networking.istio.io/echo-split created
# virtualservice.networking.istio.io/echo-split-mesh created
# virtualservice.networking.istio.io/echo-split-gateway created

VirtualServiceを2つ作っていますが、これはメッシュ内部とクラスタ外部、それぞれからのルーティングを設定しているためです。というのも今回作っている環境には、大きく分けて以下の2系統のEnvoyが存在しています。

  • 各Podにつくサイドカー用
  • クラスタ外部からのリクエストを最初に受けるIngress Gateway用

IstioではVirtualServiceのgatewaysフィールドで「このルーティングルールをどのEnvoyに適用するか」を切り替えます。gatewaysを省略したecho-split-meshのVirtualServiceはメッシュ内のサイドカーに適用され、gateways: ["web-gateway"]を指定したecho-split-gatewayの方はIngress Gateway (istio/gatewayで追加されたService / Deployment / Podの一式) に適用されます。つまり、以下2つのルールということです。

  1. メッシュ内からecho-splitを呼ぶためのルール
  2. クラスタ外部から/splitを呼ぶためのルール
...
---
# gateways 未指定のメッシュ内用ルール
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: echo-split-mesh
  namespace: default
spec:
  hosts:
  - echo-split
  - echo-split.default.svc.cluster.local
  ...
---
# gateways を指定した外部用ルール
apiVersion: networking.istio.io/v1
kind: VirtualService
metadata:
  name: echo-split-gateway
  namespace: default
spec:
  hosts:
  - "*"
  gateways:
  - web-gateway
  http:
  - match:
    - uri:
        prefix: /split
    ...

なお、技術的には1つのVirtualServiceにmeshweb-gatewayの両方を書いてまとめることもできますが、説明用として「内部通信用のルール」と「入口用のルール」の違いをわかりやすくするため、あえて2つに分けました。

ではこのルーティングが適用されているか確認してみましょう。まずはメッシュ内から叩くための一時的なクライアントPodを作ります。

kubectl run curl -n default \
  --image=curlimages/curl:8.7.1 \
  --restart=Never \
  --command -- sleep 3600

クライアントPodが使える状態か確認します。

# pod/curl condition met が返ってくれば OK
kubectl wait --for=condition=Ready pod/curl -n default --timeout=120s

作成したクライアントPodからメッシュ内通信を確認します。

for i in $(seq 1 20); do
  kubectl exec -n default curl -- curl -s http://echo-split
done | sort | uniq -c
    #  18 v1
    #   2 v2

次にIngress Gateway経由の確認です。(kubectl -n istio-ingress port-forward service/istio-ingressgateway 18080:80未実行の場合は再度実行してください)

for i in $(seq 1 20); do
  curl -s http://localhost:18080/split
done | sort | uniq -c
    #  19 v1
    #   1 v2

数字は実行のたびに変わりますが、おおむね9:1でルーティングされていることが確認できました。

検証用に作成した一時的なクライアントPodは不要なので削除しておきます。

kubectl delete pod curl -n default

この章で作った構成のまとめ

この章ではIstioを導入し、「トラフィックの管理 / 分散」にフォーカスしてサービスメッシュを実装してみました。図で視覚的に表すと、最終的に以下の状態になっています。(nginx-...のPodの部分などを一部省略しています)

flowchart TD
  EXT["Client"] --> IGSVC

  subgraph CLUSTER["Kubernetes Cluster"]
    subgraph NS_ISTIO_SYSTEM["Namespace: istio-system"]
      ISTIOD["Pod: istiod-*"]
    end

    subgraph NS_ISTIO_INGRESS["Namespace: istio-ingress"]
      IGSVC["Service: istio-ingressgateway"]
      IGPOD["Pod: istio-ingressgateway-*<br/>Envoy"]
      IGSVC --> IGPOD
    end

    subgraph NS_DEFAULT["Namespace: default"]
      GW["Gateway: web-gateway"]
      VSWEB["VirtualService: web-route"]
      VSGW["VirtualService: echo-split-gateway"]
      VSMESH["VirtualService: echo-split-mesh"]
      DR["DestinationRule: echo-split<br/>subsets: v1 / v2"]

      APPSVC["Service: nginx / echo"]
      SPLITSVC["Service: echo-split"]

      APPPODS["Pod 群: nginx-* / echo-*<br/>app + istio-proxy"]
      V1POD["Pod: echo-split-v1-*<br/>app + istio-proxy"]
      V2POD["Pod: echo-split-v2-*<br/>app + istio-proxy"]

      APPSVC --> APPPODS
      SPLITSVC --> V1POD
      SPLITSVC --> V2POD
    end

    GW -. "selector: istio=ingressgateway" .-> IGPOD
    VSWEB -. "gateways: web-gateway" .-> GW
    VSGW -. "gateways: web-gateway" .-> GW

    IGPOD -->|/nginx, /echo| VSWEB
    IGPOD -->|/split| VSGW
    VSWEB --> APPSVC
    VSGW -->|90 / 10| SPLITSVC

    APPPODS -->|mesh 内通信| VSMESH
    VSMESH -->|90 / 10| SPLITSVC
    DR -. "v1 / v2 subset" .-> SPLITSVC

    ISTIOD -. "xDS / 証明書配布" .-> IGPOD
    ISTIOD -. "xDS / 証明書配布" .-> APPPODS
    ISTIOD -. "xDS / 証明書配布" .-> V1POD
    ISTIOD -. "xDS / 証明書配布" .-> V2POD
  end
  classDef default fill:transparent,stroke:#6b7280
  style CLUSTER fill:transparent,stroke:#6b7280
  style NS_ISTIO_SYSTEM fill:transparent,stroke:#6b7280
  style NS_ISTIO_INGRESS fill:transparent,stroke:#6b7280
  style NS_DEFAULT fill:transparent,stroke:#6b7280

Prometheus での監視

途中から始めたい方は、サンプルリポジトリ zenn-k8s-handson をcloneしてmake ckpt-20を実行すると、この節の前提状態 (Istio一式 + Gateway / VirtualService / DestinationRule + echo-split v1/v2まで導入済み) を再現できます。

前の章ではIstioのVirtualServiceとDestinationRuleを使って、echo-splitへの通信をv1に90%、v2に10% で振り分けました。ここでは、その「通信が制御できていること」をレスポンスの見た目だけでなく、メトリクスでも確認してみます。

Prometheusは、監視対象から定期的にメトリクスを取得して保存するPull型の監視ツールです。Kubernetesと組み合わせると、いま存在しているPodやServiceを自動で見つけながらメトリクスを収集できます。

今回は学習用として、Istio公式が配布しているPrometheusのサンプル構成をそのまま使います。(これはデモ用途の最小構成であり、本番運用向けではないです73)

Prometheus を導入してターゲットを確認する

まずはPrometheusをクラスタに反映します。

kubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.29/samples/addons/prometheus.yaml
# serviceaccount/prometheus created
# configmap/prometheus created
# clusterrole.rbac.authorization.k8s.io/prometheus created
# clusterrolebinding.rbac.authorization.k8s.io/prometheus created
# service/prometheus created
# deployment.apps/prometheus created

作成されたPodを確認してみます。(必要に応じてkubectl rollout status deployment/prometheus -n istio-systemで反映を待ってください)

kubectl get pod -n istio-system
# NAME                          READY   STATUS    RESTARTS       AGE
# istiod-gggggggggg-aaaaa       1/1     Running   0              xh
# prometheus-iiiiiiiiii-aaaaa   2/2     Running   0              xs

prometheus-...というPodが1つ起動し、その中にコンテナが2つあることが確認できました。ローカルから画面を確認できるようにポートフォワードします。

kubectl -n istio-system port-forward service/prometheus 9090:9090
# Forwarding from 127.0.0.1:9090 -> 9090
# Forwarding from [::1]:9090 -> 9090

ブラウザでhttp://localhost:9090を開いてください。以下のような画面が表示されれば問題ありません。これがPrometheusの画面です。 Prometheus のトップ画面 Prometheus のトップ画面

Prometheusの画面では監視対象のステータスや収集しているメトリクスの一覧が確認できます。まずは画面上部のタブからStatus > Target Healthを確認します。(Target = 監視対象です)

Prometheus の Target Health の画面 Target Health の画面

この画面ではkube-apiserverやノード、Podなど、Prometheusが監視している各種ターゲットのエンドポイントが一覧表示されます。簡単に解説すると、表示されている情報は以下のような意味を持っています。

  • Endpoint: 対象のエンドポイント
  • Labels: スクレイプ対象のラベル
  • Last scrape: 最終スクレイプ時刻や、スクレイプにかかった時間など
  • State: ステータス (UPDOWN)

※Prometheusにおいて「スクレイプ」という言葉は、各サービスからメトリクスをPullすることを表現します。

kubernetes-podsでPrometheusが監視対象としているPodの一覧が確認できます。Labelsを見てみると、app="echo-split"app=echo、下の方までいくとapp=istiodなど、これまで作ってきたPodの名前が存在することがわかります。StateUPになっていれば、監視対象を見つけて正常にスクレイプできているという状態です。

サービスディスカバリで監視対象が決まる仕組み

先に結論を書くと、Prometheusはkube-apiserverにPodなど各種リソースの情報を問い合わせ、その結果をrelabel_configsでフィルタリングして残ったものを監視対象として保存しています。以下では、この流れをConfigMapの中身から具体的に見ていきます。

Prometheusは監視対象を以下のように把握して、情報を保存します。(初出の言葉は追って解説します)

  1. ConfigMapに記述されたkubernetes_sd_configsに基づき、kube-apiserverに対して対象リソースの情報を問い合わせ
  2. kube-apiserverから取得した情報に対し、relabel_configsの条件を適用し、実際に監視する対象をフィルタリング
  3. 監視対象のエンドポイントに対してHTTPでアクセスし、メトリクスを収集
  4. Prometheusサーバー内に時系列データとして保存

このうち、kube-apiserverから監視対象候補を見つけ、relabel_configsで実際のターゲットを決める部分をいわゆる「サービスディスカバリ」と表現したりします。順番に詳しく見ていきます。

まず、ここまで機会がなかったので紹介してきませんでしたが、ConfigMapは、他のオブジェクトが使うための非機密の設定データを保持するAPIオブジェクトです74。Prometheusをクラスタに反映したkubectl apply -f https://raw.githubusercontent.com/istio/istio/release-1.29/samples/addons/prometheus.yamlのコマンドでConfigMapも既にクラスタに反映済みでした。以下でマニフェストを確認してみます。

kubectl -n istio-system get configmap prometheus -o yaml
# apiVersion: v1
# data:
#   alerting_rules.yml: |
#     {}
#   alerts: |
#     {}
#   allow-snippet-annotations: "false"
#   prometheus.yml: |
#     global:
#       evaluation_interval: 1m
#       scrape_interval: 15s
#       scrape_timeout: 10s
#     scrape_configs:
#     - job_name: kubernetes-api-servers
#       bearer_token_file: /var/run/secrets/kubernetes.io/serviceaccount/token
#       kubernetes_sd_configs:
#       - role: endpoints
#       relabel_configs:
#       - action: keep
#         regex: default;kubernetes;https
#         source_labels:
#         - __meta_kubernetes_namespace
#         - __meta_kubernetes_service_name
#         - __meta_kubernetes_endpoint_port_name
#       scheme: https
#       tls_config:
#         ca_file: /var/run/secrets/kubernetes.io/serviceaccount/ca.crt
#        ...

このConfigMapのdata.prometheus.ymlにPrometheusの設定が入っています。クラスタ内で動くPrometheusのサーバーは、この設定を元にkube-apiserverに問い合わせ、監視候補となるPodやServiceなどの情報を取得します。

kube-apiserverから返ってくる情報は大量であり、また監視にあたっては不要なもの (例えば、未起動のPodの情報など) も多いため、絞り込みを行います (正確にはKubernetesのラベルをPrometheus用にマッピングしていたりもします)。その絞り込み条件が上記マニフェストにおけるdata.prometheus.yml.scrape_configs[].relabel_configs[]です。

例えば以下は未起動 / 完了済み / 起動失敗のPodは監視対象から除外するといったことを表しています。

- job_name: kubernetes-pods
      honor_labels: true
      kubernetes_sd_configs:
      - role: pod # 対象は Pod
      relabel_configs:
      ...
      - action: drop # 一致する場合は監視対象から除外
        regex: Pending|Succeeded|Failed|Completed # Pod が未起動 / 完了済み / 起動失敗
        source_labels:
        - __meta_kubernetes_pod_phase
...

絞り込みを行って監視対象を決めたら、その監視対象のエンドポイントにHTTPでアクセスしてメトリクスを収集します。ここでいうエンドポイントとはStatus > Target Healthで見たEndpointに表示されているものです。echo-splitのPod (v1) は私の環境ではhttp://10.244.1.7:15020/stats/prometheusになっています (Pod IPは環境ごとに異なるため、各自の環境ではkubectl -n default get pod -l app=echo-split,version=v1 -o wideで確認してください)。このエンドポイントも上記の流れで組み立てられているので、一例として深掘りしてみます。

まず前提として、今回の環境ではIstioのサイドカーがPodに注入された際に、いくつかのannotationsが付与されています。また、PodのIPアドレスは10.244.1.7であることも確認できます。

kubectl -n default get pod -l app=echo-split,version=v1 -o yaml
# apiVersion: v1
# items:
# - apiVersion: v1
#   kind: Pod
#   metadata:
#     annotations:
#       ...
#       prometheus.io/path: /stats/prometheus
#       prometheus.io/port: "15020"
#       prometheus.io/scrape: "true"
#   ...
#   status:
#     podIP: 10.244.1.7
...

prometheus.io/から始まるannotationsが3つ存在することがわかります。改めてPrometheusのConfigMapを抜粋します。(kubectl -n istio-system get configmap prometheus -o yamlで確認可能です)

job_name: kubernetes-pods
      honor_labels: true
      kubernetes_sd_configs:
      - role: pod
      ...
      relabel_configs:
      - action: keep
        regex: true
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_scrape
      ...
      - action: replace
        regex: (https?)
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_scheme
        target_label: __scheme__
      - action: replace
        regex: (.+)
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_path
        target_label: __metrics_path__
      - action: replace
        regex: (\d+);((([0-9]+?)(\.|$)){4})
        replacement: $2:$1
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_port
        - __meta_kubernetes_pod_ip
        target_label: __address__
...

それぞれのrelabelステップは以下のように動いています。

relabel ステップ何をしているか
action: keep (prometheus_io_scrape)prometheus.io/scrape: "true"の Pod のみを監視対象に残す
action: replace (prometheus_io_scheme__scheme__)prometheus.io/scheme annotation を URL のスキーム (http/https) に反映。未指定なら http
action: replace (prometheus_io_path__metrics_path__)prometheus.io/path annotation を URL のパスに反映。未指定なら/metrics
action: replace (prometheus_io_port + pod_ip__address__)Pod IP とprometheus.io/portを組み合わせて URL のホスト:ポートを組み立て
各relabelブロックをYAMLレベルで深掘り

順番に詳しく見ていきます。まず以下の部分はprometheus.io/scrape: "true"のPodのみが監視対象となることが表現されています。

      - action: keep
        regex: true
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_scrape

そして以下はprometheus.io/schemeがあればURLの先頭に使用します。なければデフォルトはhttpです。URLの末尾のパスはprometheus.io/pathがあればそれを使用し、なければ/metricsです。今回はPodのannotationsにて前者は未指定のためhttp、後者はprometheus.io/path: /stats/prometheusとなっているため、それが適用されます。

      - action: replace
        regex: (https?)
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_scheme
      - action: replace
        regex: (.+)
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_path
        target_label: __metrics_path__

そして以下でprometheus.io/port: "15020"podIP: 10.244.1.7を使ってURLの本体を組み立てます。

      - action: replace
        regex: (\d+);((([0-9]+?)(\.|$)){4})
        replacement: $2:$1
        source_labels:
        - __meta_kubernetes_pod_annotation_prometheus_io_port
        - __meta_kubernetes_pod_ip
        target_label: __address__

まとめると、Prometheusはkube-apiserverにPodなど各種リソースの情報を問い合わせ、その結果をフィルタリングして残ったものを監視対象として画面に表示していたということです。そのフィルタリングの過程をエンドポイントのURLを例にとって紹介しました。Prometheusではこのような仕組みでKubernetesを継続的に監視し、メトリクスをPullして情報を収集しています。

ちなみに、これまでUIで確認してきた情報はAPI経由でも取得可能です。

# 例えば Status > Target Health で見た情報
curl -s http://localhost:9090/api/v1/targets
# レスポンスの JSON が大量に出力

Flux での GitOps と Kustomize での環境差異管理

途中から始めたい方は、サンプルリポジトリ zenn-k8s-handson をcloneしてmake ckpt-30を実行すると、この節の前提状態 (Istio + Prometheusまで導入済み) を再現できます。

これまでの章ではkubectl applyhelm upgrade --installを使い、クラスタに対する変更を人間が直接適用してきました。ここでは、その適用の起点をGitに移します。つまり、今まで作ってきた各種リソースのマニフェストをGitで管理し、Fluxがそれをクラスタ内からPullして同期するというGitOpsの流れを確認していきます。あわせて、Kustomizeを使いつつ「環境」の考え方も登場させて、「開発環境はmainの状態を自動反映、本番環境は自動反映せず手動反映」のようなこともします。

おさらいですが、GitOpsとはGitなどのバージョン管理システムを理想的な状態 (desired state) の拠り所として扱い、その内容に実クラスタを (自動で) 収束させる運用方法です。実現させるツールとしてArgo CDやFluxなどがありますが、これらを用いると一定間隔でGitの内容を取得し、クラスタとの差分を適用できます。人間がkubectl applyなどでクラスタ側を直接変更しても、次の同期でGitの状態に戻されます。つまり、Gitを起点にしたReconciliation Loopです。75

Kustomize

Fluxの前に、まず既存のマニフェストをKustomizeで整理します。引き続き、実際に今まで作ってきたリソースをそのまま題材にします。

おさらいすると、Kustomizeはマニフェストの差分を管理するKubernetesネイティブなツールで、kubectlからそのまま使えます。基本の考え方は、共通の完成形マニフェストをbase/に置き、環境ごとの差分だけをoverlays/で上書きして最終的なマニフェストを組み立てるというものです。

サンプルを簡単に再掲します。(必要であればマニフェスト管理と Helm、Kustomizeに戻ってください)

  • 共通の完成形マニフェスト
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 2
  selector:
    matchLabels:
      app: myapp
  template:
    metadata:
      labels:
        app: myapp
    spec:
      containers:
      - name: app
        image: ghcr.io/example/myapp:1.0.0
        env:
        - name: LOG_LEVEL
          value: info
  • 上書き
apiVersion: apps/v1
kind: Deployment
metadata:
  name: myapp
spec:
  replicas: 1 # 2 ⇒ 1
  template:
    spec:
      containers:
      - name: app
        image: ghcr.io/example/myapp:1.0.0-dev # 開発用イメージ
        env:
        - name: LOG_LEVEL
          value: debug # info ⇒ debug

今回は開発環境と本番環境の2つを用意していきますが、これをKustomizeなしでやろうとすると既存のマニフェストが以下のようになります。

manifests/
├─ dev/
│ ├─ deployment.yaml
│ ├─ service.yaml
│ ├─ deploy-svc.yaml
│ ├─ gateway-basic.yaml
│ └─ echo-split.yaml
└─ prod/
 ├─ deployment.yaml
 ├─ service.yaml
 ├─ deploy-svc.yaml
 ├─ gateway-basic.yaml
 └─ echo-split.yaml

マニフェストの数や環境の数が増えると、この構成では管理が大変になってきます。Kustomizeを用いた最終的な構成イメージは以下の通りです。

.
└─ manifests/
   ├─ base/
   │  ├─ deployment.yaml
   │  ├─ service.yaml
   │  ├─ deploy-svc.yaml
   │  ├─ gateway-basic.yaml
   │  ├─ echo-split.yaml
   │  └─ kustomization.yaml
   └─ overlays/
      ├─ dev/
      │  ├─ kustomization.yaml
      │  ├─ patch-nginx-replicas.yaml
      │  └─ patch-traffic-split.yaml
      └─ prod/
         ├─ kustomization.yaml
         ├─ patch-nginx-replicas.yaml
         └─ patch-traffic-split.yaml

まずはディレクトリを切って、これまで作ってきた環境共通のマニフェストをbase/へ移動します。

mkdir -p manifests/base manifests/overlays/dev manifests/overlays/prod
mv deployment.yaml service.yaml deploy-svc.yaml gateway-basic.yaml echo-split.yaml manifests/base/

base/には環境共通の完成形マニフェストを置き、overlays/devoverlays/prodでは差分だけを持たせます。今回は「devではレプリカ数を少なく、prodでは多めにする」という差分を例に確認していきます。

まずはbase/側のkustomization.yamlです。これでKustomizeの管理対象を宣言します。

cat > manifests/base/kustomization.yaml <<'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
  - deployment.yaml
  - service.yaml
  - deploy-svc.yaml
  - gateway-basic.yaml
  - echo-split.yaml
EOF

次に、dev環境のoverlayです。今回は環境差分をNamespaceで分離します。namespace: devによって、DeploymentやServiceなどNamespaceスコープのリソースにはdev Namespaceが自動付与されます。ただしKustomizeがNamespace自体を自動作成するわけではないため、適用先のNamespaceを別途マニフェストとして用意します。

  • ../../baseの取り込みと差分用のパッチ
cat > manifests/overlays/dev/kustomization.yaml <<'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: dev
resources:
  - ../../base
  - namespace.yaml
patches:
  - path: patch-nginx-replicas.yaml
EOF
  • dev Namespace
cat > manifests/overlays/dev/namespace.yaml <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: dev
EOF

例えばdevnginxreplicasを2から1に落とす場合、以下のようなパッチを置きます。

cat > manifests/overlays/dev/patch-nginx-replicas.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 1
EOF

prodも同じようにoverlayを作っておきます。対象は同じくnginxで、こちらはreplicasを2から4に増やします。

cat > manifests/overlays/prod/kustomization.yaml <<'EOF'
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod
resources:
  - ../../base
  - namespace.yaml
patches:
  - path: patch-nginx-replicas.yaml
EOF

cat > manifests/overlays/prod/namespace.yaml <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: prod
EOF

cat > manifests/overlays/prod/patch-nginx-replicas.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 4
EOF

この時点で、環境ごとの最終マニフェストが組み立てられるようになりました。まず最終マニフェストの中身を以下のコマンドで確認できます。

kubectl kustomize manifests/overlays/dev
# ...
# ---
# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   name: nginx
#   namespace: dev # default ではなく dev になっている
# spec:
#   replicas: 1 # 2 ⇒ 1 になっている
#   ...
# ---
# ...

nginxのDeploymentで作成されるPodが1になっていることが確認できました。

開発環境の内容をクラスタに反映する場合は以下のコマンドです。(これまでdevというNamespaceは存在しなかったので、色々とcreatedと出力されます)

kubectl apply -k manifests/overlays/dev
# namespace/dev created
# service/echo created
# service/echo-split created
# service/nginx created
# deployment.apps/echo created
# deployment.apps/echo-split-v1 created
# deployment.apps/echo-split-v2 created
# deployment.apps/nginx created
# destinationrule.networking.istio.io/echo-split created
# gateway.networking.istio.io/web-gateway created
# virtualservice.networking.istio.io/echo-split-gateway created
# virtualservice.networking.istio.io/echo-split-mesh created
# virtualservice.networking.istio.io/web-route created

Kustomizeでの差分管理は以上です。このように「ベースとなる内容にパッチを当てる」ことで環境ごとの差分を管理しやすくするツールです。

では次はFluxで上記を同期対象にしていきましょう。

Flux のセットアップ

今回は公式のbootstrapコマンド76でFluxをクラスタに導入します。念のため前提として、GitHubのOrganization配下ではない個人リポジトリを使って進めていきます。(GitHub以外だったりリポジトリの性質でコマンドが変わります)

まず事前準備としてGitHubでPersonal Access Token (PAT) を取得します (お好みで他の方法でも大丈夫です)。前提として、検証で使えるリポジトリも必要です。手順例を簡単に記載しておきます。

本記事は学習目的のため個人のPATを使いますが、本番運用での採用は推奨しません。本番ではFlux公式の GitHub App ベースの bootstrap やSSH Deploy Key、Workload Identity Federationなどの利用を推奨します。あわせて、クラスタ内のシークレット管理にはSOPS / Sealed Secrets / External Secretsなどの仕組みの検討が必要です。(Flux Security)

  1. GitHub右上のプロフィール画像からSettings
  2. Developer settings
  3. Personal access tokens ⇒ Fine-grained tokens
  4. Generate new token
    1. Token name: flux-bootstrap-spot
    2. Expiration: 7 days
    3. Description: 空で良い
    4. Resource owner: 自分
    5. Repository access: Only select repositories
      1. Selected repositories: 検証用リポジトリ (作ってない場合は作ってください)
    6. Permissions:
      1. Administration: Read-only
      2. Contents: Read and write
      3. Metadata: Read-only

作成したPATとユーザー名、リポジトリ名を環境変数に入れます。

export GITHUB_TOKEN=<YOUR_PAT_TOKEN>
GITHUB_USER=<YOUR_USER_NAME>
GITHUB_REPO=<YOUR_REPO_NAME>

以下のコマンドで「このクラスタはFluxを入れて良い状態か」をチェックします。

flux check --pre
# ► checking prerequisites
# ✔ Kubernetes 1.35.0 >=1.33.0-0
# ✔ prerequisites checks passed

上記のような表示が出れば問題ありません。ではクラスタにFluxを導入していきます。(説明簡略化のためmainブランチをそのまま使います。なお、個人リポジトリ前提なので--personalを付けています)

mkdir -p clusters/kind

# GITHUB_TOKEN は暗黙で読み込まれる
flux bootstrap github \
 --token-auth \
 --owner=${GITHUB_USER} \
 --repository=${GITHUB_REPO} \
 --branch=main \
 --path=./clusters/kind \
 --personal
# ► connecting to github.com
# ► cloning branch "main" from Git repository "https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git"
# ✔ cloned repository
# ► generating component manifests
# ✔ generated component manifests
# ✔ committed component manifests to "main" ("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
# ► pushing component manifests to "https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git"
# ► installing components in "flux-system" namespace
# ✔ installed components
# ✔ reconciled components
# ► determining if source secret "flux-system/flux-system" exists
# ► generating source secret
# ► applying source secret "flux-system/flux-system"
# ✔ reconciled source secret
# ► generating sync manifests
# ✔ generated sync manifests
# ✔ committed sync manifests to "main" ("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb")
# ► pushing sync manifests to "https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git"
# ► applying sync manifests
# ✔ reconciled sync configuration
# ◎ waiting for GitRepository "flux-system/flux-system" to be reconciled
# ✔ GitRepository reconciled successfully
# ◎ waiting for Kustomization "flux-system/flux-system" to be reconciled
# ✔ Kustomization reconciled successfully
# ► confirming components are healthy
# ✔ helm-controller: deployment ready
# ✔ kustomize-controller: deployment ready
# ✔ notification-controller: deployment ready
# ✔ source-controller: deployment ready
# ✔ all components are healthy

上記出力のうち、主要な部分のみ解説します。以下のようなことが起きています。

  1. 指定したリポジトリを一時ディレクトリにclone (cloning branch "main" from Git repository "https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git")
  2. Flux controller本体のマニフェストを追加して、mainにプッシュ (pushing component manifests to "https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git")
  3. マニフェストをクラスタに適用 (installing components in "flux-system" namespace)
  4. Gitの認証情報 (前述のPATがここで使用される) をSecretとしてクラスタに適用 (applying source secret "flux-system/flux-system")
  5. リポジトリとの同期用マニフェストを追加して、mainにプッシュ、クラスタに適用 (applying sync manifests)
  6. (ここからは追加したcontrollerが動く) リポジトリを取得し、クラスタ内にアーティファクト生成 (waiting for GitRepository "flux-system/flux-system" to be reconciled)
  7. アーティファクトを読んでビルドし、クラスタに適用。ここでFluxが自分自身をGitで管理する状態になる (waiting for Kustomization "flux-system/flux-system" to be reconciled)
  8. 各controllerがReadyか確認 (confirming components are healthy)

さらに簡単にまとめると、以下です。

  • 一時cloneしたリポジトリにFlux用マニフェストを書いてGitHubにプッシュ
  • クラスタに初回適用
  • FluxにGitの運用を引き継ぎ

GitHubにプッシュされたFlux用のマニフェストをローカルに落として軽く確認してみます。

git pull
# remote: Enumerating objects: 14, done.
# ...
#  .../gotk-components.yaml          | 6428 +++++++++
#  .../flux-system/gotk-sync.yaml    |   27 +
#  .../kustomization.yaml            |    5 +
#  3 files changed, 6460 insertions(+)
#  create mode 100644 clusters/kind/flux-system/gotk-components.yaml
#  create mode 100644 clusters/kind/flux-system/gotk-sync.yaml
#  create mode 100644 clusters/kind/flux-system/kustomization.yaml

現状、以下のようになっています。

.
├ clusters/
|   └─ kind/
|      └─ flux-system/
|         ├─ gotk-components.yaml
|         ├─ gotk-sync.yaml
|         └─ kustomization.yaml
└ manifests/
    ├ ...

gotk-components.yamlがFlux本体です。6,500行弱あるので中身の紹介は割愛しますが、flux-systemというNamespaceやGitRepository、Kustomizationなどの各種CRD、source-controllerという名前のFlux実体となるDeploymentなどが定義されています。(prefixのgotkはGitOps ToolKitの略です77)

クラスタに作成された各種リソースも確認してみます。

kubectl -n flux-system get gitrepositories,kustomizations,deployments,secrets
# NAME                                                 URL                                                        AGE   READY   STATUS
# gitrepository.source.toolkit.fluxcd.io/flux-system   https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git   xm    True    stored artifact for revision 'main@sha1:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb'

# NAME                                                    AGE   READY   STATUS
# kustomization.kustomize.toolkit.fluxcd.io/flux-system   xm    True    Applied revision: main@sha1:bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb

# NAME                                      READY   UP-TO-DATE   AVAILABLE   AGE
# deployment.apps/helm-controller           1/1     1            1           xm
# deployment.apps/kustomize-controller      1/1     1            1           xm
# deployment.apps/notification-controller   1/1     1            1           xm
# deployment.apps/source-controller         1/1     1            1           xm

# NAME                 TYPE     DATA   AGE
# secret/flux-system   Opaque   2      xm

先ほど紹介した流れとマッピングすると、以下がgotk-components.yamlで追加されたdeployment.apps/source-controllerdeployment.apps/kustomize-controllerです。(helm-controllernotification-controllerも同じタイミングですが説明の流れ的に登場させられないので割愛します)

  1. Flux コントローラー本体のマニフェストファイルを追加して、main にプッシュ (pushing component manifests to "https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git")
  2. マニフェストをクラスタに適用 (installing components in "flux-system" namespace)

そして以下がgotk-sync.yamlで追加されたgitrepository.source.toolkit.fluxcd.io/flux-systemkustomization.kustomize.toolkit.fluxcd.io/flux-systemです。

  1. リポジトリとの同期用のマニフェストファイルを追加して、main にプッシュ、クラスタに適用 (applying sync manifests)

source-controllergitrepository.source.toolkit.fluxcd.io/flux-systemを元に、クラスタ内で使えるアーティファクトを生成します。このgitrepository.source...はGitRepositoryというカスタムリソース (gotk-components.yamlで定義されたCRDによるもの) であり、「どのGitリポジトリの、どのブランチから取得するか」などを定義します (ほかにも同列の概念でOCIRepositoryやBucketなどが存在していて、sourceとして指定可能です78)。

そしてkustomize-controllerは、source-controllerが作ったアーティファクトを材料にして、FluxのKustomization (GitRepositoryと同じくカスタムリソース) をreconcileし、マニフェストをビルド / 適用します。Kustomizationは「どのsourceを参照し、そこから生成されたアーティファクトのどのパスを、どの間隔でクラスタに適用するか (加えて、不要リソースを削除 (prune) するか)」を定義します。(Kustomize紹介後なのでややこしいですが、KustomizationはFluxのカスタムリソースであり、内部的に使用されてはいますが、ツールとして紹介したKustomizeとは別物です)

まとめると、source-controllerがGitRepositoryをreconcileしてリポジトリの内容を取得・アーティファクトを生成し、kustomize-controllerがKustomizationの設定を見てクラスタへ反映するという役割分担になっています。

GitRepositoryとKustomizationはgotk-sync.yamlとして生成されています。

# This manifest was generated by flux. DO NOT EDIT.
---
apiVersion: source.toolkit.fluxcd.io/v1
kind: GitRepository
metadata:
  name: flux-system
  namespace: flux-system
spec:
  interval: 1m0s # source-controller が Git リポジトリを fetch する間隔
  ref:
    branch: main # 対象ブランチ
  secretRef:
    name: flux-system
  url: https://github.com/<YOUR_USER_NAME>/<YOUR_REPO_NAME>.git # 対象リポジトリ
---
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: flux-system
  namespace: flux-system
spec:
  interval: 10m0s # kustomize-controller が Kustomization を reconcile する間隔
  path: ./clusters/kind
  prune: true # マニフェストからなくなったオブジェクトなどを削除する
  sourceRef:
    kind: GitRepository # 参照する source
    name: flux-system # 参照する source の名前

続いて、dev overlayを同期するためのFluxのKustomizationを追加します。コマンドで生成し、その結果をGitに置くのがわかりやすいです79

flux create kustomization apps-dev \
  --namespace=flux-system \
  --source=GitRepository/flux-system \
  --path="./manifests/overlays/dev" \
  --prune=true \
  --interval=1m \
  --wait=true \
  --export > clusters/kind/apps-dev.yaml

生成されるマニフェストは以下のようになります。(バージョンなどで内容がブレる可能性があるため、あいまいな表現にしています)

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps-dev
  namespace: flux-system
spec:
  interval: 1m0s
  path: ./manifests/overlays/dev
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  wait: true

これで、mainブランチにマージされた変更のうち、manifests/overlays/devに書かれた内容だけがkindクラスタへ自動で反映されるようになります。

一方で、prodは自動反映させず、人間が確認してから反映するようにします。

flux create kustomization apps-prod \
  --namespace=flux-system \
  --source=GitRepository/flux-system \
  --path="./manifests/overlays/prod" \
  --prune=true \
  --interval=1m \
  --wait=true \
  --export > clusters/kind/apps-prod.yaml

spec.suspend: trueはオプションがないため、手動で追加して止めておきます。80

apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
  name: apps-prod
  namespace: flux-system
spec:
+ suspend: true
  interval: 1m0s
  path: ./manifests/overlays/prod
  prune: true
  sourceRef:
    kind: GitRepository
    name: flux-system
  wait: true

prodを反映したくなったら、suspendを外してcommitします。ちなみに、以下のコマンドで手動での再開 / 停止もできます。

# 反映再開
flux resume kustomization apps-prod -n flux-system

# 反映停止
flux suspend kustomization apps-prod -n flux-system

ではDevとProdをクラスタに反映していきましょう。

git add clusters/kind/apps-dev.yaml clusters/kind/apps-prod.yaml
git commit -m "Add Flux Kustomizations for dev and prod"
git push

Flux導入後は、以下のコマンドで同期状態を確認できます。

flux get kustomizations -A
# NAMESPACE       NAME            REVISION                SUSPENDED       READY   MESSAGE
# flux-system     apps-dev        main@sha1:cccccccc      False           True    Applied revision: main@sha1:cccccccc
# flux-system     apps-prod                               True            False   waiting to be reconciled
# flux-system     flux-system     main@sha1:cccccccc      False           True    Applied revision: main@sha1:cccccccc

ProdについてはSUSPENDEDTrueとなっており、クラスタへの反映は行われていないことが確認できます。

Flux の動作確認

ここまででKustomizeとFluxのセットアップが完了したので、実際にGitOpsの挙動を確認します。「Gitの変更がクラスタへ自動で反映されること」と「クラスタ側の手動変更がGitの状態へ自動で戻る (ドリフトが収束する) こと」の2点を順に見ていきます。

Git の変更が自動で反映されることを確認する

Gitを変更したときにクラスタが追従することを確認します。Kustomizeの章で準備した開発環境のnginx DeploymentでPodを3つにしてみましょう。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
-  replicas: 1
+  replicas: 3

変更前は以下のようになっています。

kubectl get deployment nginx -n dev -o yaml
# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   annotations:
#     deployment.kubernetes.io/revision: "3"
#   ...
#   generation: 2
#   labels:
#     kustomize.toolkit.fluxcd.io/name: apps-dev
#     kustomize.toolkit.fluxcd.io/namespace: flux-system
#   name: nginx
#   namespace: dev
#   ...
# spec:
#   ...
#   replicas: 1
#   ...

spec.replicasが1になっています。おまけ的に、Fluxによってラベル (metadata.labels) が付与されていることも確認できます。

変更したらGitにcommit / pushします。

git add manifests/overlays/dev/patch-nginx-replicas.yaml
git commit -m "Adjust dev nginx pod to 3"
git push origin main

しばらく待つとFluxがその差分を検知し、devクラスタ側のnginx Deploymentを更新します。(以下のコマンドで手動でreconcileを促すこともできます)

flux reconcile source git flux-system -n flux-system
flux reconcile kustomization apps-dev -n flux-system

改めてマニフェストを見て反映結果を確認してみます。

kubectl get deployment nginx -n dev -o yaml
# apiVersion: apps/v1
# kind: Deployment
# metadata:
#   annotations:
#     deployment.kubernetes.io/revision: "3"
#   creationTimestamp: "<TIMESTAMP>"
#   generation: 3
#   ...
# spec:
#   ...
#   replicas: 3
#   ...

spec.replicasが3になっていることが確認できました。

手動変更のドリフトが戻ることを確認する

次に、kubectl applyでクラスタに手動で変更を加えてみます。replicasを5にしてみます。

kubectl scale deployment/nginx -n dev --replicas=5
# deployment.apps/nginx scaled

すぐに以下のコマンドを打ってみます。

kubectl get deployment nginx -n dev
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# nginx   5/5     5            5           xm

replicasが5になっています。しばらく待ってから、もう一度実行してみます。

kubectl get deployment nginx -n dev
# NAME    READY   UP-TO-DATE   AVAILABLE   AGE
# nginx   3/3     3            3           xm

3に戻っていることが確認できました (見るタイミングによっては5が残っている場合もあります)。このようにFluxはReconciliation Loop を体感するで紹介した「宣言した状態に収束させる」という考え方を、KubernetesのDeploymentだけでなく「Gitを起点にしたクラスタ全体の運用」に拡張したもの (= GitOps) だと言えます。

Gatekeeper でのセキュリティ担保

途中から始めたい方は、サンプルリポジトリ zenn-k8s-handsonfork してから cloneし、GITHUB_TOKEN / GITHUB_USER / GITHUB_REPOをexportしたうえでmake ckpt-40を実行すると、この節の前提状態 (Istio + Prometheus + Flux bootstrap完了) を再現できます。詳細は同リポジトリのREADMEを参照してください。

Gitにcommitしたマニフェストが、Fluxによって自動でクラスタに反映されることが確認できました。ただし、Gitに「適用したくない内容が入ってしまった」など、そのまま自動反映されたくないときがあります。そこで最後の関門として置くのがAdmission Controllerです。

マニフェストへの制約と Gatekeeper、Kyverno のおさらいになりますが、KubernetesではCREATEUPDATEのAPIリクエストがkube-apiserverに届くと、認証 / 認可やスキーマ検証を経て、etcdに保存される前に最終チェックされます。この最終チェックをAdmissionと表現し、その段階で「オブジェクトを変更する」「そのまま通す」「拒否する」といった処理を行う仕組みがAdmission Controllerです。Kubernetes組み込みのAdmissionだけでなく、Webhook経由で外部ツールを呼び出して制約をかける方法もあり、GatekeeperやKyvernoはそのためのツールで、クラスタへ保存される直前のオブジェクトを検査し、ルール違反ならそこで止めます。

新しく出てくる言葉がいくつかあるため、上記のAdmissionなども含め、一緒に紹介します。

  • Admission: APIリクエストがetcdに保存される前の検査フェーズ
  • Admission Controller: そのフェーズでオブジェクトを変更したり拒否したりする仕組み
  • ポリシーエンジン: 入力データとルールをもとに、許可 / 拒否などの判定を返す仕組み
  • OPA: Open Policy Agentの略で、汎用のポリシーエンジン
  • Rego: OPAでポリシーを書くための宣言的な言語

OPAはKubernetes専用のツールではなく、CI/CDやAPI Gatewayなどでも使える汎用エンジンです。一方でGatekeeperは、そのOPAをKubernetesのAdmissionに組み込みやすくしたOSSで、ポリシーをConstraintTemplateとConstraintというカスタムリソースとして扱えるようにしています。 ConstraintTemplateの内側では最終的にRegoなどが使われますが、今回は公式のポリシーライブラリを使うのでRego自体は手書きしません。8182

Gatekeeper を kind クラスタに導入する

これまで使ってきたkindのクラスタを引き続き使います。クラスタにOSSのGatekeeperを導入していきます。

まずはHelmを使ってGatekeeperをインストールします。83

helm repo add gatekeeper https://open-policy-agent.github.io/gatekeeper/charts
helm repo update
# 動作確認時の安定版バージョンに固定
GATEKEEPER_VERSION=3.22.2
helm install gatekeeper gatekeeper/gatekeeper \
  --namespace gatekeeper-system \
  --create-namespace \
  --version ${GATEKEEPER_VERSION}
# "gatekeeper" has been added to your repositories
# Hang tight while we grab the latest from your chart repositories...
# ...Successfully got an update from the "gatekeeper" chart repository
# ...Successfully got an update from the "istio" chart repository
# Update Complete. ⎈Happy Helming!⎈
# NAME: gatekeeper
# LAST DEPLOYED: <DATETIME>
# NAMESPACE: gatekeeper-system
# STATUS: deployed
# REVISION: 1
# DESCRIPTION: Install complete
# TEST SUITE: None

Podが起動していることを確認します。

kubectl get pods -n gatekeeper-system
# NAME                                             READY   STATUS    RESTARTS   AGE
# gatekeeper-audit-6d8d9bb8b8-xxxxx                1/1     Running   0          xs
# gatekeeper-controller-manager-7c8f9d4b5d-xxxxx   1/1     Running   0          xs
# gatekeeper-controller-manager-7c8f9d4b5d-yyyyy   1/1     Running   0          xs
# gatekeeper-controller-manager-7c8f9d4b5d-zzzzz   1/1     Running   0          xs

gatekeeper-controller-manager-...RunningであればOKです。次に、ポリシー本体となるConstraintTemplateを適用します。ここではRegoを自作せず、Gatekeeperの公式ポリシーライブラリにあるテンプレートを使います。8485

kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/disallowedtags/template.yaml
kubectl apply -f https://raw.githubusercontent.com/open-policy-agent/gatekeeper-library/master/library/general/containerresources/template.yaml
# constrainttemplate.templates.gatekeeper.sh/k8sdisallowedtags created
# constrainttemplate.templates.gatekeeper.sh/k8srequiredresources created

ConstraintTemplateは「どういうルールで検査するか」の定義で、 Constraintは「そのルールをどこに、どんなパラメータで適用するか」の定義です。今回はAdmissionの挙動確認が主目的であるため、わかりやすさと説明簡略化の観点からdev NamespaceのPodに対してルールをかけます。(実運用ではDeploymentなどに対して早い段階でエラーを返したいことが多いと考えられます)

ではConstraintのマニフェストを作っていきます。今回は以下の2つを試します。

  • latestタグの禁止
  • CPU / Memoryのrequestsパラメータ必須
mkdir -p manifests/policies

cat > manifests/policies/gatekeeper-constraints-dev.yaml <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sDisallowedTags
metadata:
  name: disallow-latest-in-dev
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces:
      - dev
  parameters:
    tags:
      - latest
---
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredResources
metadata:
  name: require-cpu-memory-requests-in-dev
spec:
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Pod"]
    namespaces:
      - dev
  parameters:
    requests:
      - cpu
      - memory
EOF

spec.match.kinds[].kindsに対象のリソース種別を指定し、spec.parametersにConstraintTemplateに渡す設定値を指定します。

なお、マニフェスト先頭のkindは制約の種類そのものです。今回のK8sDisallowedTagsK8sRequiredResourcesは、先ほどクラスタに適用した2つのConstraintTemplateから定義されたConstraintの種類であり、意味は割とそのままですが、「禁止するタグに関するルール」と「必須の計算リソースに関するルール」を表しています。

では上記マニフェストをクラスタに適用します。

kubectl apply -f manifests/policies/gatekeeper-constraints-dev.yaml
# k8sdisallowedtags.constraints.gatekeeper.sh/disallow-latest-in-dev created
# k8srequiredresources.constraints.gatekeeper.sh/require-cpu-memory-requests-in-dev created

latestタグの Pod が拒否されることを確認する

まずはlatestタグ違反だけを起こすPodを作ってみます。(requestsはちゃんと書いておきます)

cat > manifests/policies/pod-latest.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: nginx-latest
  namespace: dev
spec:
  containers:
    - name: nginx
      image: nginx:latest
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
EOF

これをkubectl applyすると、以下のようにAdmissionで拒否されます。

kubectl apply -f manifests/policies/pod-latest.yaml
# Error from server (Forbidden): error when creating "manifests/policies/pod-latest.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [disallow-latest-in-dev] container <nginx> uses a disallowed tag <nginx:latest>; disallowed tags are ["latest"]

requestsがない Pod が拒否されることを確認する

次はlatestを外し、代わりにrequestsを消してみます。

cat > manifests/policies/pod-no-requests.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: nginx-no-requests
  namespace: dev
spec:
  containers:
    - name: nginx
      image: nginx:1.27.5
EOF

これも拒否されます。

kubectl apply -f manifests/policies/pod-no-requests.yaml
# Error from server (Forbidden): error when creating "manifests/policies/pod-no-requests.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [require-cpu-memory-requests-in-dev] container <nginx> does not have <{"cpu", "memory"}> requests defined

逆に、タグもrequestsも問題ないPodは通ります。

cat > manifests/policies/pod-ok.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: nginx-ok
  namespace: dev
spec:
  containers:
    - name: nginx
      image: nginx:1.27.5
      resources:
        requests:
          cpu: 100m
          memory: 128Mi
EOF
kubectl apply -f manifests/policies/pod-ok.yaml
# pod/nginx-ok created

Flux 経由でも同じ制約が効くことを確認する

上記ではkubectl applyで手動適用してきましたが、これはFluxがGitから適用しようとした内容にも同じように効きます。FluxのKustomizationはGit上のマニフェストをビルドし、kube-apiserverに対して検証・適用するため、Admissionを迂回できません。

では同じ違反をGitOps経由で起こしてみます。manifests/overlays/devに違反Podを追加し、overlayのkustomization.yamlから参照させます。

 apiVersion: kustomize.config.k8s.io/v1beta1
 kind: Kustomization
 namespace: dev
 resources:
   - ../../base
   - namespace.yaml
+  - pod-policy-test.yaml
 patches:
   - path: patch-nginx-replicas.yaml
cat > manifests/overlays/dev/pod-policy-test.yaml <<'EOF'
apiVersion: v1
kind: Pod
metadata:
  name: pod-policy-test
  namespace: dev
spec:
  containers:
    - name: nginx
      image: nginx:latest
EOF

これをcommit / pushしたうえで、手動でreconcileを促します。

git add manifests/overlays/dev/kustomization.yaml manifests/overlays/dev/pod-policy-test.yaml
git commit -m "Add invalid pod to test Gatekeeper"
git push origin main

flux reconcile source git flux-system -n flux-system
flux reconcile kustomization apps-dev -n flux-system
# ► annotating GitRepository flux-system in flux-system namespace
# ✔ GitRepository annotated
# ◎ waiting for GitRepository reconciliation
# ✔ fetched revision main@sha1:dddddddddddddddddddddddddddddddddddddddd
# ► annotating Kustomization apps-dev in flux-system namespace
# ✔ Kustomization annotated
# ◎ waiting for Kustomization reconciliation

この状態のまま止まるので、強制終了してください。apps-devの状態を見てみます。

flux get kustomizations -A
# NAMESPACE       NAME            REVISION                SUSPENDED       READY   MESSAGE
# flux-system     apps-dev        main@sha1:dddddddd      False           False   Pod/dev/pod-policy-test dry-run failed (Forbidden): admission webhook "validation.gatekeeper.sh" denied the request: [require-cpu-memory-requests-in-dev] container <nginx> does not have <{"cpu", "memory"}> requests defined
#                                                                                 [disallow-latest-in-dev] container <nginx> uses a disallowed tag <nginx:latest>; disallowed tags are ["latest"]
# flux-system     apps-prod                               True            False   waiting to be reconciled
# flux-system     flux-system     main@sha1:dddddddd      False           True    Applied revision: main@sha1:dddddddd

READYFalseになっています。MESSAGEを見るとrequestscpumemoryがないし、nginx:latestがついていると怒られています。またエラーの主体はFlux固有のものではなく、admission webhook "validation.gatekeeper.sh"になっています。つまりFluxはGitの内容を適用しようとしただけで、その最終判定をしているのはAdmissionということです。また、FluxのKustomizationは定期的にdry-runを実行し、差分や問題を検出するため、Gitに違反マニフェストが入っている限り同期が失敗し続けます。86

では、Git側を修正して復旧させます。

 apiVersion: v1
 kind: Pod
 metadata:
   name: pod-policy-test
   namespace: dev
 spec:
   containers:
     - name: nginx
-      image: nginx:latest
+      image: nginx:1.27.5
+      resources:
+        requests:
+          cpu: 100m
+          memory: 128Mi
git add manifests/overlays/dev/pod-policy-test.yaml
git commit -m "Fix Gatekeeper policy violation"
git push origin main
...

flux reconcile source git flux-system -n flux-system
flux reconcile kustomization apps-dev -n flux-system
# ► annotating GitRepository flux-system in flux-system namespace
# ✔ GitRepository annotated
# ◎ waiting for GitRepository reconciliation
# ✔ fetched revision main@sha1:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee
# ► annotating Kustomization apps-dev in flux-system namespace
# ✔ Kustomization annotated
# ◎ waiting for Kustomization reconciliation
# ✔ applied revision main@sha1:eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee

今度はKustomizationのreconcileも成功します。再度apps-devの状態を見てみます。

flux get kustomizations -A
# NAMESPACE       NAME            REVISION                SUSPENDED       READY
# flux-system     apps-dev        main@sha1:eeeeeeee      False           True    Applied revision: main@sha1:eeeeeeee
# flux-system     apps-prod                               True            False   waiting to be reconciled
# flux-system     flux-system     main@sha1:eeeeeeee      False           True    Applied revision: main@sha1:eeeeeeee

これでGitに書かれたdesired stateのうち、Admissionを通過したものだけがクラスタに反映されることが確認できました。Fluxが継続的に同期し、Gatekeeperがその入口で安全性を担保する、という役割分担です。

まとめ

これで各種ツールをkind上のクラスタで動かしてみる章は終わりです。次の章ではGoogle CloudのGKEに触れつつ、この章で動かしてきたPrometheus / Gatekeeper / サービスメッシュの関心事を、GKEとその周辺のマネージドサービスで見ていきます。

GKE と各種マネージドサービス

ここまではkind上のローカルクラスタにOSSのIstio、Prometheus、Gatekeeperなどを導入し、Kubernetesとその周辺ツールの手触り感を確認してきました。記事の最後として、同じ関心事をGoogle Cloudのマネージドサービスではどう扱うのかを見ていきます。

GKEはGoogle CloudのマネージドKubernetesで、Kubernetes APIやコントロールプレーンの管理をGoogle Cloudが担います87。開発者はワーカーノード上で自分のコンテナを動かすことに集中できます。この章で見ていくサービスの対応関係は以下のイメージです。

  • ローカルで作ったkindクラスタ ⇒ GKE
  • kind上で動かしたPrometheus ⇒ Managed Service for Prometheus
  • kind上で動かしたGatekeeper ⇒ Policy Controller
  • kind上で触ったIstio / サービスメッシュ ⇒ Cloud Service Mesh

なおCloud Service MeshのマネージドコントロールプレーンにはTraffic Director実装とistiod実装があり、新規導入ではデフォルトでTraffic Director実装が使われるため、「マネージドなIstio」とは単純に言い切れません。88

前提

GKE のモード

前提として、GKEにはAutopilotとStandardという2つのモードがあります。ノードの運用やマシンタイプの設定可否などのマネージド具合が異なり8990、Google Cloud推奨はよりマネージドなAutopilotモードです91。ここまでkindではノードやコントロールプレーンの構成を見てきましたが、GKEでは逆に「そのあたりをどこまで自分でやらなくて良いか」を見たいため、ここではAutopilotモードで進めます。(ノードプールやマシンタイプを明示的に管理したい場合や、クラスタ構成そのものを細かく設計したい場合はStandardモードの方が向いています)

Google Cloud の環境など

この章では、以下を満たす検証用のGoogle Cloudプロジェクトがある前提で進めます。

  • 課金が有効になっているGoogle Cloudプロジェクト
  • gcloud auth loginでローカルから操作できる状態
  • APIの有効化、IAMの変更、GKEクラスタの作成ができる権限 (このあたりの必要権限の話も割愛させてください)

なお、本章は以下バージョンのgcloud CLIで動作確認しています。

gcloud --version | head -n 1
# Google Cloud SDK 565.0.0

まず対象プロジェクトを設定しておきます。

PROJECT_ID={YOUR_PROJECT_ID}

gcloud auth login
検証用プロジェクトを新規作成する場合

クリーンなプロジェクトから始める場合の一例です。プロジェクト作成にはroles/resourcemanager.projectCreator、請求先の紐付けにはプロジェクト側と請求先アカウント側の権限が必要です。9293

  • 利用可能な請求先アカウント確認、gcloud CLI認証
# 請求先アカウント確認
gcloud billing accounts list

# gcloud CLI 認証
gcloud auth login
  • プロジェクト作成、請求先アカウント紐づけ
# Project ID はグローバルに一意である必要があるため、日時と乱数を付ける
PROJECT_ID="gke-lab-$(date +%Y%m%d%H%M%S)-${RANDOM}"
# gcloud billing accounts list で出力した ACCOUNT_ID のいずれかを指定
BILLING_ACCOUNT_ID="000000-000000-000000"

# プロジェクトの作成
gcloud projects create ${PROJECT_ID}

gcloud billing projects link "${PROJECT_ID}" \
  --billing-account="${BILLING_ACCOUNT_ID}"

検証が終わったら、このプロジェクトごと削除することで、残っているリソースをまとめて片付けられます。ただし、プロジェクトを削除すると中のリソースも使えなくなるため、必要なものが残っていないことを確認してから実行してください。

gcloud projects delete ${PROJECT_ID}

GKE Autopilotクラスタを作るだけであれば、有効化するAPIはひとまずGoogle Kubernetes Engine APIのみです94。(後続の章でもいくつか有効化しますが、必要なタイミングで都度行います)

gcloud services enable container.googleapis.com \
  --project=${PROJECT_ID}

また、GKEのノードが使うSAには、最低限roles/container.defaultNodeServiceAccountが必要です。ここでは説明簡略化のため、公式ドキュメントでも紹介されているCompute Engineのdefault service accountに必要な権限を付与する方法で進めます95。(本来は専用のSAを作り、クラスタ作成時に--service-accountで指定する方法が推奨です)

以下のコマンドで権限を付与します。

PROJECT_NUMBER=$(gcloud projects describe ${PROJECT_ID} \
  --format="value(projectNumber)")

gcloud projects add-iam-policy-binding ${PROJECT_ID} \
  --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \
  --role="roles/container.defaultNodeServiceAccount"

クラスタ作成

まずはGKEに最小のAutopilotクラスタを1つ作ります。請求先アカウント単位で毎月の無料枠はありますが、GKEはクラスタが存在するだけで課金されるので、途中でやめる場合も環境のクリーンアップは忘れずにお願いします。96

CLUSTER_NAME=gke-lab
CLUSTER_LOCATION=asia-northeast1

# 10 分くらいかかります
gcloud container clusters create-auto ${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --project=${PROJECT_ID}
# Creating cluster gke-lab in asia-northeast1... Cluster is being health-checked (Kubernetes Con
# trol Plane is healthy)...done.
# Created [https://container.googleapis.com/v1/projects/gke-lab-2026xxxxxxxxxxx-xxxxxx/zones/asia-northeast1/clusters/gke-lab].
# To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/asia-northeast1/gke-lab?project=gke-lab-2026xxxxxxxxxxx-xxxxxx
# kubeconfig entry generated for gke-lab.
# NAME     LOCATION         MASTER_VERSION      MASTER_IP      MACHINE_TYPE   NODE_VERSION        NUM_NODES  STATUS   STACK_TYPE
# gke-lab  asia-northeast1  1.35.1-gke.1396002  xx.xxx.xx.xxx  ek-standard-8  1.35.1-gke.1396002  3          RUNNING  IPV4

MACHINE_TYPEがek-standard-8のノードが3つ作成されました。ekはE2の派生マシンシリーズで、Compute Engineのマシンファミリー一覧には登場しないAutopilot専用のマシンタイプです97。(公式ドキュメントに記載はありませんが、ek-standard-8はvCPU 8 / メモリ32 GBとされています98)

上記のクラスタ作成コマンドには引数がほぼありませんでした。一方、Standardモードのクラスタはノードのマシンタイプやノード数、オートスケーリングの設定などを指定して作るのが一般的です。例えば、最小構成のStandardクラスタは以下のようなコマンドになります。

# Standard モードでのクラスタ作成コマンド例
gcloud container clusters create gke-lab-standard \
  --location=asia-northeast1-a \
  --machine-type=e2-medium \
  --num-nodes=3

--machine-type--num-nodesで指定しているような、「マシンタイプを揃えた一定数のノードのまとまり」をGKEでは「ノードプール」と呼びます。これはクラスタ内のノードを構成単位にまとめたもので、マシンタイプ、ブートディスク、オートスケーリング設定などがプール単位で揃います99。Autopilotではこのノードプールをユーザー側で設計しません。DeploymentやPodの定義に書いたCPU / メモリ要求などをもとに、GKEがそれを動かすためのノードを自動で用意・調整します。(必要に応じてマニフェストに定義を追加することで調整は可能です)9190

kubectlからGKEクラスタに接続するには、GKE用の認証プラグインgke-gcloud-auth-pluginが必要です。100

gcloud components install gke-gcloud-auth-plugin
# その他の例: gcloud を apt で管理している場合
sudo apt-get install -y google-cloud-cli-gke-gcloud-auth-plugin

認証情報を取得してkubectlの向き先をkindのクラスタからGKEに変更します。get-credentialsを実行するとkubeconfigが更新され、以降のkubectlは先ほど作ったクラスタを向くようになります。100

gcloud container clusters get-credentials ${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --project=${PROJECT_ID}
# Fetching cluster endpoint and auth data.
# kubeconfig entry generated for gke-lab.

クラスタの情報を確認してみます。

kubectl cluster-info
# Kubernetes control plane is running at https://<CONTROL_PLANE_ENDPOINT>
# GLBCDefaultBackend is running at https://<CONTROL_PLANE_ENDPOINT>/api/v1/namespaces/kube-system/services/default-http-backend:http/proxy
# KubeDNS is running at https://<CONTROL_PLANE_ENDPOINT>/api/v1/namespaces/kube-system/services/kube-dns:dns/proxy
# KubeDNSUpstream is running at https://<CONTROL_PLANE_ENDPOINT>/api/v1/namespaces/kube-system/services/kube-dns-upstream:dns/proxy
# Metrics-server is running at https://<CONTROL_PLANE_ENDPOINT>/api/v1/namespaces/kube-system/services/https:metrics-server:/proxy

# To further debug and diagnose cluster problems, use 'kubectl cluster-info dump'.

確認できました。

fleet に登録する

このあと触るPolicy ControllerやCloud Service Meshはfleetという論理的なまとまりの上で有効化していく機能です。fleetはGKEの複数クラスタを管理するための仕組みですが、1クラスタのみでもfleetレベルの機能を使うために必要です。101

クラスタ作成時に--enable-fleetを付ければ同時に登録できますが、今回は既に作成したクラスタをあとから登録します。102

# API 有効化 (fleet の旧称が GKE Hub であるため、API は gkehub になっている)
gcloud services enable gkehub.googleapis.com \
  --project=${PROJECT_ID}
# Operation "operations/acat.p2-XXXXXXXXXXXX-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" finished successfully.

# fleet に登録
gcloud container clusters update ${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --enable-fleet \
  --project=${PROJECT_ID}
# Updating gke-lab...done.
# Updated [https://container.googleapis.com/v1/projects/gke-lab-2026xxxxxxxxxxx-xxxxxx/zones/asia-northeast1/clusters/gke-lab].
# To inspect the contents of your cluster, go to: https://console.cloud.google.com/kubernetes/workload_/gcloud/asia-northeast1/gke-lab?project=gke-lab-2026xxxxxxxxxxx-xxxxxx

fleetに登録されたクラスタは、membershipとして確認できます。fleetレベルの機能はこのmembership単位で適用します。

gcloud container fleet memberships list \
 --project=${PROJECT_ID}
# NAME     UNIQUE_ID                             LOCATION
# gke-lab  xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx  asia-northeast1

membership名はデフォルトでクラスタ名と同じです。

Managed Service for Prometheus

Prometheus での監視の章ではPrometheusをkind上に自前で入れて、メトリクス収集の仕組みを確認しました。Managed Service for Prometheusは、Prometheusのメトリクス収集・保存・クエリ体験をGoogle Cloud側に寄せたマネージドサービスです。

サンプルアプリをデプロイする

Managed Service for Prometheusの公式ドキュメントでは、prom-exampleというサンプルアプリが使われています103。この記事でもそのまま使います。

kubectl create namespace prom-demo
# namespace/prom-demo created

kubectl -n prom-demo apply -f \
  https://raw.githubusercontent.com/GoogleCloudPlatform/prometheus-engine/v0.17.2/examples/example-app.yaml
# Autopilot ではリソース要求がない Deployment に対してデフォルト値が自動で挿入されるため、以下の Warning が出ます。
# Warning: autopilot-default-resources-mutator:Autopilot updated Deployment prom-demo/prom-example: defaulted unspecified 'cpu' resource for containers [prom-example] (see http://g.co/gke/autopilot-defaults).
# deployment.apps/prom-example created

kubectl -n prom-demo get pods
# NAME                            READY   STATUS    RESTARTS   AGE
# prom-example-xxxxxxxxxx-aaaaa   1/1     Running   0          xm
# prom-example-xxxxxxxxxx-bbbbb   1/1     Running   0          xm
# prom-example-xxxxxxxxxx-ccccc   1/1     Running   0          xm

適用したアプリのマニフェストは以下のようになっています。

apiVersion: apps/v1
kind: Deployment
metadata:
  name: prom-example
  labels:
    app.kubernetes.io/name: prom-example
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: prom-example
  replicas: 3
  template:
    metadata:
      labels:
        app.kubernetes.io/name: prom-example
    spec:
      ...
      containers:
      - image: nilebox/prometheus-example-app@sha256:dab60d038c5d6915af5bcbe5f0279a22b95a8c8be254153e22d7cd81b21b84c5
        name: prom-example
        ports:
        - name: metrics
          containerPort: 1234
        command:
        - "/main"
        - "--process-metrics"
        - "--go-metrics"

以下、主要な点だけ簡単に解説します。

  • spec.replicas: 3つのPodを起動します。
  • spec.template.spec.containers[].image: nilebox/prometheus-example-app というPrometheus形式のメトリクスを公開するGo製のアプリが使われています。Prometheusのメトリクスには4種類のタイプがあり104、そのうちのCounter (累積カウンタ) やHistogram (観測値の分布) などのメトリクスを/metricsエンドポイントで公開します。自分自身にHTTPリクエストを投げ続ける作りになっており、放置するだけでメトリクスの値が変化していくため、スクレイプ動作の確認に便利です。
  • spec.template.spec.containers[].ports: メトリクス公開用ポート (1234) にmetricsという名前が付いています。のちほど作るPodMonitoringからこの名前を参照します。
  • spec.template.spec.containers[].command: アプリ独自のメトリクスに加えて--process-metrics--go-metricsを渡すことで、プロセスやGoランタイムのメトリクスも公開するようになっています。

PodMonitoring を作成する

次に、Managed Service for Prometheusに対してスクレイプ対象のPodを伝えるためのPodMonitoringというカスタムリソースを作ります。105

kubectl -n prom-demo apply -f \
  https://raw.githubusercontent.com/GoogleCloudPlatform/prometheus-engine/v0.17.2/examples/pod-monitoring.yaml
# podmonitoring.monitoring.googleapis.com/prom-example created

kubectl -n prom-demo get podmonitoring
# NAME           AGE
# prom-example   xs

マニフェストは以下のようになっています。

apiVersion: monitoring.googleapis.com/v1
kind: PodMonitoring
metadata:
  name: prom-example
  labels:
    app.kubernetes.io/name: prom-example
spec:
  selector:
    matchLabels:
      app.kubernetes.io/name: prom-example
  endpoints:
  - port: metrics
    interval: 30s

selector.matchLabelsapp.kubernetes.io/name: prom-exampleのラベルが付いたPodを対象としています。そしてendpoints[].portexample-app.yamlが公開しているポート (metrics) を指定して、そのポートをスクレイプするようにしています。

これで数分待てば自動的にメトリクスが取り込まれますが、スクレイプが正常にできる状態であることを能動的に確認するため、デバッグ用途でtarget statusという機能を一時的に有効にします。106

mkdir -p manifests/gke/prometheus

# features.targetStatus で target status を有効化
cat > manifests/gke/prometheus/operatorconfig-targetstatus.yaml <<'EOF'
apiVersion: monitoring.googleapis.com/v1
kind: OperatorConfig
metadata:
  namespace: gmp-public
  name: config
features:
  targetStatus:
    enabled: true
EOF

kubectl apply -f manifests/gke/prometheus/operatorconfig-targetstatus.yaml
# Warning: resource operatorconfigs/config is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
# operatorconfig.monitoring.googleapis.com/config configured

kubectl -n prom-demo describe podmonitorings/prom-example
# ...
# Status:
#   Endpoint Statuses:
#     Active Targets:       3
#     Collectors Fraction:  1
#     ...

Active Targets: 3のように出ていれば、3つのPodがスクレイプ対象として正しく検出されています。確認が終わったらtarget statusは戻しておきます。

cat > manifests/gke/prometheus/operatorconfig-targetstatus.yaml <<'EOF'
apiVersion: monitoring.googleapis.com/v1
kind: OperatorConfig
metadata:
  namespace: gmp-public
  name: config
features:
  targetStatus:
    enabled: false
EOF

kubectl apply -f manifests/gke/prometheus/operatorconfig-targetstatus.yaml
# Warning: resource operatorconfigs/config is missing the kubectl.kubernetes.io/last-applied-configuration annotation which is required by kubectl apply. kubectl apply should only be used on resources created declaratively by either kubectl create --save-config or kubectl apply. The missing annotation will be patched automatically.
# operatorconfig.monitoring.googleapis.com/config configured

PromQL でメトリクスを見てみる

ローカルのkindで動かした際はPrometheusの画面を確認しましたが、Managed Service for Prometheusでは取り込んだメトリクスがCloud Monitoringから確認できます。107 また、記事の流れの都合で Prometheus での監視で説明できなかったのですが、Prometheusの時系列データを検索・集計・変換するための専用クエリ言語をPromQLと言い、Cloud MonitoringはPromQLによるクエリに対応しています。Google Cloudのコンソール画面からMetrics Explorerを開き、CodeタブでPromQLを使って確認してみます。

たとえば以下のようなクエリでサンプルアプリの1秒あたりのリクエスト数を確認できます。

sum(rate(example_requests_total{namespace="prom-demo"}[5m]))

Cloud Monitoring での PromQL Cloud Monitoring での PromQL クエリ実行

ローカルのkindで検証したように、Prometheusを自前で立てるとサーバーのデプロイやデータの保持期間も開発者が気にする必要があります。一方でGKE + Managed Service for Prometheusでは、「メトリクスを出すアプリ」と「それをどのようにスクレイプするか」に集中できるようになります。103

Policy Controller

Gatekeeper でのセキュリティ担保ではGatekeeperをkindのクラスタに直接導入し、latestタグ禁止やrequests必須といった制約をAdmissionで強制しました。Policy Controllerは、その「クラスタにガードレールを入れる」という関心事がGoogle Cloudに統合されたマネージドなサービスです。単一クラスタだけでなく、fleet全体に対しても一貫したポリシーを適用できます。108101

Policy Controller を有効化する

まずPolicy ControllerのAPIを有効化し、次にクラスタに対してPolicy Controllerを有効化します。

AutopilotではPolicy Controllerの機能のうち、ポリシー違反を書き換えるmutation機能と、特定のNamespaceをポリシーから除外する機能が使えません (公式に理由は明示されていませんが、Autopilotではadmission / mutationをGoogleが管理する設計のため、ユーザー側で同種の操作を重ねられない仕様だと推測しています)。ここではValidation (違反の拒否) とAudit (違反の検出) だけを使う最小構成で進めます。109

# Policy Controller の API 有効化
gcloud services enable anthospolicycontroller.googleapis.com \
  --project=${PROJECT_ID}
# Operation "operations/acat.p2-XXXXXXXXXXXX-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" finished successfully.

# クラスタで Policy Controller 有効化
gcloud container fleet policycontroller enable \
  --memberships=${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --project=${PROJECT_ID}
# Waiting for Feature Policy Controller to be created...done.
# Waiting for MembershipFeature projects/gke-lab-2026xxxxxxxxxxx-xxxxxx/locations/asia-northeast1/
# memberships/gke-lab/features/policycontroller to be updated...done.

上記コマンドから1〜2分待つとクラスタでPolicy Controllerが有効化され、以下でPolicy Controllerの構成と現在の状態を確認できます。(内容の簡単な解説はコマンド出力内に書いておきました)

gcloud container fleet policycontroller describe \
  --memberships=${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --project=${PROJECT_ID}
# createTime: '2026-xx-xxTxx:xx:xx.xxxxxxxxxZ'
# membershipSpecs:
#   projects/XXXXXXXXXXXX/locations/asia-northeast1/memberships/gke-lab:
#     policycontroller:
#       policyControllerHubConfig:
#         auditIntervalSeconds: '60' # 60 秒ごとに既存リソースを監査
#         deploymentConfigs:
#           admission:
#             podAffinity: ANTI_AFFINITY # admission 用の複数 Pod を同じノードに配置しないようにする
#         installSpec: INSTALL_SPEC_ENABLED
#         monitoring:
#           backends:
#           - PROMETHEUS # Prometheus の exposition format でメトリクスを公開する
#           - CLOUD_MONITORING # メトリクスを Cloud Monitoring にも送る
#         policyContent:
#           bundles:
#             policy-essentials-v2022: {}
#           templateLibrary:
#             installation: ALL # constraint template library を全部入れる
#       version: 1.23.1
# membershipStates:
#   projects/XXXXXXXXXXXX/locations/asia-northeast1/memberships/gke-lab:
#     policycontroller:
#       componentStates:
#         admission:
#           details: 1.23.1
#           state: ACTIVE
#         audit:
#           details: 1.23.1
#           state: ACTIVE
#         mutation:
#           details: 'deployment not installed: resource is missing'
#           state: NOT_INSTALLED
#       policyContentState:
#         bundleStates:
#           asm-policy-v0.0.1:
#             state: NOT_INSTALLED
#           cis-gke-v1.5.0:
#             state: NOT_INSTALLED
#           cis-k8s-v1.5.1:
#             state: NOT_INSTALLED
#           cost-reliability-v2023:
#             state: NOT_INSTALLED
#           nist-sp-800-190:
#             state: NOT_INSTALLED
#           nist-sp-800-53-r5:
#             state: NOT_INSTALLED
#           nsa-cisa-k8s-v1.2:
#             state: NOT_INSTALLED
#           pci-dss-v3.2.1:
#             state: NOT_INSTALLED
#           pci-dss-v3.2.1-extended:
#             state: NOT_INSTALLED
#           pci-dss-v4.0:
#             state: NOT_INSTALLED
#           policy-essentials-v2022:
#             state: ACTIVE
#           psp-v2022:
#             state: NOT_INSTALLED
#           pss-baseline-v2022:
#             state: NOT_INSTALLED
#           pss-restricted-v2022:
#             state: NOT_INSTALLED
#         referentialSyncConfigState:
#           state: NOT_INSTALLED
#         templateLibraryState:
#           state: ACTIVE
#       state: ACTIVE # Policy Controller 全体としては健全に稼働中 (NOT_INSTALLED もいくつかあるが、、という状態)
#     state:
#       code: OK # fleet から見た membership の状態が OK
#       updateTime: '2026-xx-xxTxx:xx:xx.xxxxxxxxxZ'
# name: projects/gke-lab-2026xxxxxxxxxxx-xxxxxx/locations/global/features/policycontroller
# resourceState:
#   state: ACTIVE
# spec: {}
# updateTime: '2026-xx-xxTxx:xx:xx.xxxxxxxxxZ'

Policy Controllerの有効化が完了すると、事前定義されたポリシー集であるconstraint template libraryもデフォルトでインストールされます (templateLibrary: installation: ALL)110

# 公式ドキュメントに書いてあるラベル付きのコマンド例は環境/バージョンによって合わないことがあるようなのでこちらを実行
kubectl get constrainttemplates
# NAME                                        AGE
# allowedserviceportname                      xm
# asmauthzpolicydisallowedprefix              xm
# asmauthzpolicyenforcesourceprincipals       xm
# asmauthzpolicynormalization                 xm
# asmauthzpolicysafepattern                   xm
# ...

数が多いためすべては解説できませんが、以下のようなConstraintTemplateが入っています。

  • k8srequiredlabels: 指定キーのラベル必須化 (このあと使います)
  • k8sdisallowedtags: 特定タグ (latestなど) 禁止
  • k8srequiredresources: requests / limits必須化
  • k8spssrunasnonroot: rootユーザーでの起動禁止

dryrunで監査してみる

ここでは素朴な例として「Namespaceにはownerラベルを必須にする」という制約を使います。まずは違反を拒否せず、dryrunで監査だけおこないます。111

  • Namespaceとマニフェスト作成 (YAMLの内容から読み取れるため、内容の説明は割愛します)
# owner ラベルなしで Namespace 作成
kubectl create namespace policy-test-audit
# namespace/policy-test-audit created

mkdir -p manifests/gke/policy

cat > manifests/gke/policy/require-owner-label.yaml <<'EOF'
apiVersion: constraints.gatekeeper.sh/v1beta1
kind: K8sRequiredLabels
metadata:
  name: namespaces-must-have-owner
spec:
  enforcementAction: dryrun
  match:
    kinds:
      - apiGroups: [""]
        kinds: ["Namespace"]
  parameters:
    message: All namespaces must have an owner label
    labels:
      - key: owner
        allowedRegex: ".+"
EOF
  • クラスタに適用し、監査結果を確認
kubectl apply -f manifests/gke/policy/require-owner-label.yaml
# k8srequiredlabels.constraints.gatekeeper.sh/namespaces-must-have-owner created

# 監査結果を確認
kubectl describe k8srequiredlabels namespaces-must-have-owner
# ...
# Status:
#   ...
#   Total Violations: 13 # 数値は環境によって異なる可能性があります
#   Violations:
#     ...
#     Enforcement Action:  dryrun
#     Group:
#     Kind:                Namespace
#     Message:             All namespaces must have an owner label
#     Name:                policy-test-audit
#     Version:             v1
#     Enforcement Action:  dryrun
#     ...

policy-test-auditのNamespaceにownerラベルがないため、監査結果に違反として出ます (他12件のNamespaceもラベルがないので出ていますが、説明には関係ないので気にせず進めます)。違反を解消してみます。

kubectl label namespace policy-test-audit owner=morishin --overwrite
# namespace/policy-test-audit labeled

auditのインターバルは60秒なので、反映を待って以下のコマンドを実行します。

kubectl describe k8srequiredlabels namespaces-must-have-owner

# ...
# Status:
# Total Violations: 12
# ...

denyにして拒否してみる

次に、同じ制約をdryrunからdenyに切り替えて、違反したリソースがAdmissionで拒否されることを確認します。111

 apiVersion: constraints.gatekeeper.sh/v1beta1
 kind: K8sRequiredLabels
 metadata:
   name: namespaces-must-have-owner
 spec:
-  enforcementAction: dryrun
+  enforcementAction: deny
   match:
     kinds:
       - apiGroups: [""]
         kinds: ["Namespace"]
   parameters:
     message: All namespaces must have an owner label
     labels:
       - key: owner
         allowedRegex: ".+"
  • クラスタに適用
kubectl apply -f manifests/gke/policy/require-owner-label.yaml
  • 違反するNamespaceを作成し、クラスタに適用
cat > manifests/gke/policy/namespace-no-owner.yaml <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: policy-test-deny
EOF

kubectl apply -f manifests/gke/policy/namespace-no-owner.yaml
# Error from server (Forbidden): error when creating "manifests/gke/policy/namespace-no-owner.yaml": admission webhook "validation.gatekeeper.sh" denied the request: [namespaces-must-have-owner] All namespaces must have an owner label

All namespaces must have an owner labelというメッセージとともにadmission webhookでクラスタへの適用が拒否されたことがわかります。ラベルを付ければ通ります。

cat > manifests/gke/policy/namespace-with-owner.yaml <<'EOF'
apiVersion: v1
kind: Namespace
metadata:
  name: policy-test-ok
  labels:
    owner: morishin
EOF

kubectl apply -f manifests/gke/policy/namespace-with-owner.yaml
# namespace/policy-test-ok created

Gatekeeper でのセキュリティ担保のGatekeeperと見比べて、ConstraintやAdmissionの基本的な考え方は同じです。一方で、GKEではPolicy Controllerをfleet featureとして有効化し、クラスタ横断で使いやすくなっています。108

Cloud Service Mesh

Istio によるサービスメッシュではIstioを通じてサービスメッシュの基本的な考え方を見ました。Cloud Service MeshはGoogle Cloudのマネージドなサービスメッシュで、トラフィック管理、可観測性、セキュリティというサービスメッシュの代表的な関心事をGoogle Cloudと統合された形で扱えます。Autopilotではistiod相当のコントロールプレーンがクラスタ外部にGoogle管理で置かれ (in-cluster型ではない)、サービスメッシュのUIも自動的に有効になります。88112

Cloud Service Mesh を有効化する

Cloud Service Meshを有効化し、このクラスタをautomatic managementにします。automatic managementは、コントロールプレーン (istiod相当) とEnvoyサイドカー、CNI (Container Network Interface) などのアップグレードやスケールをGoogleにフルマネージドさせるモードで、前述の通りAutopilotではin-cluster型が選べないため実質これ一択です。113

# API 有効化
gcloud services enable mesh.googleapis.com \
  --project=${PROJECT_ID}
# Operation "operations/acat.p2-XXXXXXXXXXXX-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" finished successfully.

# fleet の機能として Cloud Service Mesh 有効化
gcloud container fleet mesh enable \
  --project=${PROJECT_ID}
# Waiting for Feature Service Mesh to be created...done.

# クラスタで Cloud Service Mesh 有効化
gcloud container fleet mesh update \
  --management automatic \
  --memberships=${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --project=${PROJECT_ID}
# Waiting for MembershipFeature projects/gke-lab-2026xxxxxxxxxxx-xxxxxx/locations/asia-northeast1/memberships/gke-lab/features/servicemesh to be updated...done.

# Cloud Service Mesh の有効化状態の確認
gcloud container fleet mesh describe \
  --project=${PROJECT_ID}
# ...
# createTime: '2026-xx-xxTxx:xx:xx.xxxxxxxxxZ'
# membershipSpecs:
#   projects/XXXXXXXXXXXX/locations/asia-northeast1/memberships/gke-lab:
#     mesh:
#       management: MANAGEMENT_AUTOMATIC
# membershipStates:
#   projects/XXXXXXXXXXXX/locations/asia-northeast1/memberships/gke-lab:
#     servicemesh:
#       conditions:
#       - code: VPCSC_GA_SUPPORTED
#         details: This control plane supports VPC-SC GA.
#         documentationLink: http://cloud.google.com/service-mesh/docs/managed/vpc-sc
#         severity: INFO
#       controlPlaneManagement:
#         details:
#         - code: REVISION_READY
#           details: 'Ready: asm-managed'
#         implementation: TRAFFIC_DIRECTOR
#         state: ACTIVE
#       dataPlaneManagement:
#         details:
#         - code: OK
#           details: Service is running.
#         state: ACTIVE
#     state:
#       code: OK
#       description: 'Revision ready for use: asm-managed.'
#       updateTime: '2026-xx-xxTxx:xx:xx.xxxxxxxxxZ'
# name: projects/gke-lab-2026xxxxxxxxxxx-xxxxxx/locations/global/features/servicemesh
# resourceState:
#   state: ACTIVE
# spec: {}
# updateTime: '2026-xx-xxTxx:xx:xx.xxxxxxxxxZ'

上記のような状態になればOKです。(10分くらいかかります)

小さな HTTP 通信を流してみる

Cloud Service MeshのテレメトリはHTTP通信のときに特に見やすいため、ここでは極小の2サービス構成を使います。echoサービスを立てて、curl Podから定期的にHTTPリクエストを流します。Namespaceにsidecar auto-injectionのラベルを付けてからデプロイすると、各PodにEnvoy sidecarが注入されます。113114

kubectl create namespace mesh-demo
kubectl label namespace mesh-demo \
  istio.io/rev- istio-injection=enabled --overwrite
# namespace/mesh-demo created
# label "istio.io/rev" not found. ⇐ not found で問題なし
# namespace/mesh-demo labeled

mkdir -p manifests/gke/mesh

cat > manifests/gke/mesh/mesh-demo.yaml <<'EOF'
apiVersion: apps/v1
kind: Deployment
metadata:
  name: echo
  namespace: mesh-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: echo
  template:
    metadata:
      labels:
        app: echo
    spec:
      containers:
        - name: echo
          image: hashicorp/http-echo:1.0.0
          args: ["-text=hello from echo", "-listen=:5678"]
          ports:
            - containerPort: 5678
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 200m
              memory: 256Mi
---
apiVersion: v1
kind: Service
metadata:
  name: echo
  namespace: mesh-demo
spec:
  selector:
    app: echo
  ports:
    - name: http
      port: 80
      targetPort: 5678
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: curl
  namespace: mesh-demo
spec:
  replicas: 1
  selector:
    matchLabels:
      app: curl
  template:
    metadata:
      labels:
        app: curl
    spec:
      containers:
        - name: curl
          image: curlimages/curl:8.8.0
          command: ["/bin/sh", "-c"]
          args:
            - |
              while true; do
                curl -s http://echo.mesh-demo.svc.cluster.local/;
                sleep 2;
              done
          resources:
            requests:
              cpu: 100m
              memory: 128Mi
            limits:
              cpu: 200m
              memory: 256Mi
EOF

kubectl apply -f manifests/gke/mesh/mesh-demo.yaml
kubectl get pods -n mesh-demo
# NAME                    READY   STATUS    RESTARTS   AGE
# curl-xxxxxxxxxx-aaaaa   2/2     Running   0          xm
# echo-xxxxxxxxxx-bbbbb   2/2     Running   0          xm

kubectl get pods -n mesh-demoREADY1/2のまま動かない場合は、gcloud container fleet mesh describedataPlaneManagement.stateACTIVEになっているのを確認したうえで、以下のコマンドでPodを再スタートさせてみてください。

kubectl rollout restart deploy/echo deploy/curl -n mesh-demo

Cloud Service Meshを有効化した直後は、Pod作成時にTraffic Director側のMeshリソースのprovisioningが間に合わず、サイドカーがxDSから構成を貰えずに止まってしまうことがあります。

READY2/2になっていれば、アプリ本体に加えてEnvoy sidecarも入っていることがわかります。通信自体は以下で確認できます。

kubectl logs deploy/curl -n mesh-demo -c curl --tail=3
# hello from echo
# hello from echo
# hello from echo

Google Cloud のコンソールでサービスメッシュの見え方を確認する

数分待つと、Cloud Service Meshの画面でサービス一覧やメトリクスが見えるようになります。 Cloud Service Mesh 画面 (トポロジビュー) Cloud Service Mesh 画面 (トポロジビュー)

Cloud Service Mesh 画面 (echoのインフラストラクチャ) Cloud Service Mesh 画面 (echoのインフラストラクチャ)

Cloud Service MeshはサイドカープロキシがHTTPトラフィックを観測してCloud Monitoring / Cloud Loggingにテレメトリを送るため、アプリ側に追加の計装を書かなくても「どのサービスがどのサービスを呼んでいるか」を見やすいのが利点です。114

Istio によるサービスメッシュではIstioのコンポーネントやsidecar注入の仕組み自体を追いましたが、GKEではそのコントロールプレーンの管理をGoogleに任せ、こちらはメッシュの利用に集中できるのが違いです。

料金の概況

ここまで使ってきたManaged Service for Prometheus、Policy Controller、Cloud Service Meshの3サービスは、運用コストの増え方の性格がそれぞれかなり違います。最後にざっくり比較してまとめておきます。

Managed Service for Prometheus

「Cloud Monitoringに取り込まれたsampleの数」で課金されます115。1データポイントが何sampleにカウントされるかはメトリクスの型で変わり、スカラー値は1サンプル、ヒストグラムは1データポイントあたり2 + 非ゼロのバケット数というカウント方式です。116

単価は階層型で、月500億sampleまでは $0.06 / 100万sample (2023年8月の60% 値下げ後の単価)、それより多い分は段階的に下がっていきます。117 Prometheus形式のsampleベース課金には無料枠が設定されておらず (byteベース課金のCloud Monitoringメトリクスには別途150 MiB / 月の無料枠あり)、データ保持期間は24ヶ月です。115

メトリクス量とscrape間隔に対して線形に増えていくため、不要なメトリクスをmetricRelabelingで落としたり、scrape間隔を伸ばしたりするのが基本的なコスト対策になります。

Policy Controller

GKE上で使う場合は追加料金がかかりません。元々はGKE Enterprise tierに紐づく機能でしたが、2024年9月のGKE料金改定でfleet管理・Config Management・Policy ControllerなどがGKE Standard側に含まれる形に整理され、現在はGKEの基本料金のみで使えます。118

Cloud Service Mesh

「sidecarが注入されたclientインスタンスの数」で課金されます。GKEのPod、Cloud Runのインスタンス、Proxyless gRPCのインスタンスがそれぞれ1 clientとして数えられ、レプリカが増えればその分client数も増えます。単価は $0.0006945 / client / 時間 (= 約 $0.50 / client / 月) で、無料枠はありません。119 Mesh CA、マネージドcontrol plane、標準テレメトリダッシュボードは追加料金なしで含まれます。

レプリカ数に比例するので、サービス数とレプリカ数を抑えれば線形にコストも下がります。

なお、この記事のサンプル構成 (3 Podのprom-exampleecho + curlの小さなCSMデモ) では、いずれも無料枠やごく小さい単価レンジに収まります。検証が済んだら、次節でクリーンアップしておきます。

環境のクリーンアップ

  • プロジェクトを作成した場合
gcloud projects delete ${PROJECT_ID}
  • クラスタのみ削除する場合
gcloud container clusters delete ${CLUSTER_NAME} \
  --location=${CLUSTER_LOCATION} \
  --project=${PROJECT_ID}
  • 検証用Namespaceのみ削除する場合 (クラスタは残す)
kubectl delete namespace mesh-demo prom-demo policy-test-audit policy-test-ok

まとめ

この章ではGKE Autopilot上でManaged Service for Prometheus / Policy Controller / Cloud Service Meshを動作確認し、前章でkindに立てたツール群がGoogle Cloudマネージドではどう変わるかの対応関係を見ました。

おわりに

Kubernetesの基礎的な内容、Kubernetesを取り巻く用語やツール、そしてGoogle CloudのマネージドKubernetesであるGKEを概観してきました。Kubernetes関連のトピックに触れた際に、なんとなくでも「これはこの辺の話だな」くらいはわかるようになりたいという思いで書きましたが、ちょっとスコープを広げすぎた感もあります…。

一方で、私の力量では扱えなかった領域がまだまだあります。例えば以下のようなトピックは本記事では触れられていません。

  • StatefulSet、DaemonSet、Job / CronJobなどのワークロードAPI
  • PersistentVolume / PersistentVolumeClaim / CSIといったストレージ関連
  • NetworkPolicyやPod Security Admissionの本格運用
  • Horizontal / Vertical Pod Autoscaler、Cluster Autoscalerなどのオートスケール
  • 本番運用でのRBAC、マルチテナンシー、コスト最適化

非常に広大で奥深い領域ですが、AIエージェントのサンドボックス環境120としても注目されつつあるため、今後も学んでいきます。

Footnotes

  1. Referring to Kubernetes API resources (Kubernetes公式ドキュメント)

  2. Kubernetes Objects (Kubernetes公式ドキュメント)

  3. Components (Kubernetes公式ドキュメント)

  4. Resource Management for Pods and Containers (Kubernetes公式ドキュメント)

  5. Custom Resources (Kubernetes公式ドキュメント)

  6. Borg, Omega, and Kubernetes (Google Research)

  7. Overview (Kubernetes公式ドキュメント)

  8. Running containers (Docker Docs)

  9. namespaces(7) (Linux man-pages)

  10. cgroups(7) (Linux man-pages) / About cgroup v2 (Kubernetes 公式ドキュメント)

  11. What is a container? (Docker Docs)

  12. Understanding the image layers (Docker Docs) 2

  13. Config (OCI Image Spec)

  14. Storage drivers (Docker Docs)

  15. docker image inspect (Docker Docs)

  16. docker image save (Docker Docs)

  17. OCI Overview (Open Container Initiative公式サイト)

  18. Red Hat OpenShift (Red Hat公式サイト)

  19. minikube (minikube公式ドキュメント)

  20. K3s (K3s公式サイト)

  21. Quick Start (kind公式ドキュメント)

  22. Install Docker Engine on Ubuntu (Docker Docs)

  23. Linux上でのkubectlのインストールおよびセットアップ (Kubernetes公式ドキュメント)

  24. Configuration > Cluster-wide options (kind公式ドキュメント) / Quick Start (kind公式ドキュメント) (kind create cluster後のkubectl get nodes出力にkind-control-plane / kind-worker / kind-worker2などの命名例がある)

  25. Objects In Kubernetes (Kubernetes公式ドキュメント) / Pod API Reference (Kubernetes公式ドキュメント)

  26. Pods (Kubernetes公式ドキュメント)

  27. Configuration (kind公式ドキュメント) / Implementation details (Kubernetes公式ドキュメント)

  28. ReplicaSet API Reference (Kubernetes公式ドキュメント) / ReplicaSet (Kubernetes公式ドキュメント)

  29. Deployment API Reference (Kubernetes公式ドキュメント) / Deployments (Kubernetes公式ドキュメント)

  30. etcd (etcd公式サイト)

  31. Pod Lifecycle (Kubernetes公式ドキュメント)

  32. Components (Kubernetes公式ドキュメント) 2

  33. Deployments (Kubernetes公式ドキュメント)

  34. ReplicaSet (Kubernetes公式ドキュメント)

  35. Service (Kubernetes公式ドキュメント)

  36. Service > Headless Services (Kubernetes公式ドキュメント)

  37. CoreDNS (CoreDNS公式サイト)

  38. DNS for Services and Pods (Kubernetes公式ドキュメント) / Configuring each kubelet in your cluster using kubeadm > clusterDNS: 10.96.0.10 (Kubernetes公式ドキュメント) / Kubernetes Default ServiceCIDR Reconfiguration (Kubernetes公式ドキュメント)

  39. Gateway API (Kubernetes公式ドキュメント)

  40. Ingress (Kubernetes公式ドキュメント)

  41. Gateway API > Resource model (Kubernetes公式ドキュメント)

  42. API Overview (Gateway API公式ドキュメント)

  43. Envoy Proxy (Envoy公式サイト)

  44. README > Mac, Windows and WSL2 support (cloud-provider-kind GitHub)

  45. pkg/gateway/controller.go#L322-L335 (cloud-provider-kind GitHub)

  46. Managing Workloads > Organizing resource configurations (Kubernetes公式ドキュメント)

  47. Kubernetes v1.33: Continuing the transition from Endpoints to EndpointSlices (Kubernetes Blog)

  48. Ambient Mode > Overview (Istio公式ドキュメント)

  49. Cloud Service Mesh (Google Cloud)

  50. Prometheus (Prometheus公式サイト)

  51. Grafana (Grafana公式サイト)

  52. Managed Service for Prometheus (Google Cloud)

  53. Install with Helm (Istio公式ドキュメント)

  54. OpenGitOps

  55. GitOps Principles (OpenGitOps)

  56. GitOps (CNCF Cloud Native Glossary)

  57. Configuring OpenID Connect in cloud providers (GitHub Docs)

  58. Configure Workload Identity Federation with deployment pipelines (Google Cloud)

  59. Admission Control in Kubernetes > Admission control phases (Kubernetes公式ドキュメント)

  60. Mutation (Gatekeeper公式ドキュメント)

  61. Limit Ranges (Kubernetes公式ドキュメント)

  62. Pod Security Admission (Kubernetes公式ドキュメント)

  63. Validating Admission Policy (Kubernetes公式ドキュメント)

  64. Installing Helm (Helm公式ドキュメント)

  65. Flux installation (Flux公式ドキュメント)

  66. Download Istio Release (Istio公式ドキュメント)

  67. Installing the Sidecar > Automatic sidecar injection (Istio公式ドキュメント)

  68. Dynamic configuration (Envoy公式ドキュメント)

  69. Architecture > Istiod (Istio公式ドキュメント)

  70. Gateway (Istio公式ドキュメント)

  71. Virtual Service (Istio公式ドキュメント)

  72. Destination Rule (Istio公式ドキュメント)

  73. Prometheus > Installation > Option 1: Quick start (Istio公式ドキュメント)

  74. ConfigMaps (Kubernetes公式ドキュメント)

  75. Core Concepts > Reconciliation (Flux公式ドキュメント)

  76. Flux bootstrap for GitHub (Flux公式ドキュメント)

  77. Core Concepts > GitOps Toolkit (gotk) (Flux公式ドキュメント)

  78. Core Concepts > Sources (Flux公式ドキュメント)

  79. flux create kustomization (Flux公式ドキュメント)

  80. Kustomization > Suspending and resuming (Flux公式ドキュメント)

  81. Introduction (Gatekeeper公式ドキュメント)

  82. Open Policy Agent Introduction (OPA公式ドキュメント)

  83. Installation (Gatekeeper公式ドキュメント)

  84. Disallow tags (Gatekeeper Policy Library)

  85. Required Resources (Gatekeeper Policy Library)

  86. Kustomization (Flux公式ドキュメント)

  87. GKE overview (Google Cloud)

  88. Cloud Service Mesh の概要 (Google Cloud) 2

  89. About GKE modes of operation (Google Cloud)

  90. Compare features in Autopilot and Standard clusters (Google Cloud) 2

  91. GKE Autopilot overview > Autopilot clusters and workloads (Google Cloud) 2

  92. Creating and managing projects (Google Cloud)

  93. Enable, disable, or change billing for a project (Google Cloud)

  94. Create an Autopilot cluster (Google Cloud)

  95. About service accounts in GKE (Google Cloud)

  96. Google Kubernetes Engine pricing

  97. About Balanced and Scale-Out ComputeClasses in Autopilot clusters (Google Cloud)

  98. ek-standard-8 (DevZero)

  99. About node pools (Google Cloud)

  100. Install kubectl and configure cluster access (Google Cloud) 2

  101. Fleet management overview (Google Cloud) 2

  102. Register a cluster on Google Cloud to your fleet (Google Cloud)

  103. Get started with managed collection > Deploy the example application (Google Cloud Observability) 2

  104. Metric types (Prometheus 公式ドキュメント)

  105. PodMonitoring

  106. Get started with managed collection > Enabling the target status feature

  107. PromQL for Cloud Monitoring (Google Cloud)

  108. Policy Controller overview (Google Cloud) 2

  109. Install Policy Controller (Google Cloud)

  110. Constraint template library (Google Cloud)

  111. Auditing using constraints (Google Cloud) 2

  112. Supported platforms (Google Cloud)

  113. Provision a managed Cloud Service Mesh control plane on GKE (Google Cloud) 2

  114. Observability overview (Google Cloud) 2

  115. Pricing | Google Cloud Observability 2

  116. Cost controls and attribution (Google Cloud Observability)

  117. Managed Service for Prometheus price cut (Google Cloud Blog)

  118. GKE gets new pricing and capabilities on 10th birthday (Google Cloud Blog)

  119. Pricing | Cloud Service Mesh (Google Cloud)

  120. Agent Sandbox のご紹介: Kubernetes と GKE 上のエージェント AI 向けの強力なガードレール

※本記事は、ジーアイクラウド株式会社の見解を述べたものであり、必要な調査・検討は行っているものの必ずしもその正確性や真実性を保証するものではありません。

※リンクを利用する際には、必ず出典がGIC dryaki-blogであることを明記してください。
リンクの利用によりトラブルが発生した場合、リンクを設置した方ご自身の責任で対応してください。
ジーアイクラウド株式会社はユーザーによるリンクの利用につき、如何なる責任を負うものではありません。