Kubernetes入門 第4回:宣言的定義(YAML)と「あるべき姿」

Kubernetes入門
第4回:宣言的定義(YAML)と「あるべき姿」


前回までで、私たちは kubectl createkubectl expose といったコマンドを使い、Podを作り、サービスを公開してきました。手を動かすたびに、K8sが確かに動いている実感が湧いてきたのではないでしょうか。

しかし、ふと立ち止まって考えてみてください。

「この環境、もう一度ゼロから作れと言われたら、どうする?」

VMの世界なら、あなたはきっとこう答えるでしょう。「手順書を見ながら、同じコマンドを同じ順番で打てばいい」と。でも、その手順書が100行あったら?途中で打ち間違えたら?前任者が退職して、手順書の「なぜ」が誰にもわからなくなっていたら?

今回は、そんな「手順書地獄」から私たちを解放してくれる、K8sの真骨頂——宣言的定義(Declarative Configuration) の世界に足を踏み入れます。


4.1 手順書(Imperative) vs 設計図(Declarative)

4.1.1 Shellスクリプトによる構築の限界と、YAMLによる宣言の利点

VMエンジニアとして、あなたは何度「構築手順書」を書いてきたでしょうか。

# 典型的なVM構築手順書の一部(イメージ)
# 1. パッケージをインストール
yum install -y httpd
# 2. 設定ファイルを編集
vi /etc/httpd/conf/httpd.conf  # ← ここで何を変えるかは別紙参照
# 3. サービスを起動
systemctl start httpd
systemctl enable httpd
# 4. ファイアウォールを開放
firewall-cmd --add-service=http --permanent
firewall-cmd --reload

この手順書には、いくつかの根本的な問題があります。

問題点具体例
順序依存3番を先に実行するとエラー。順番を間違えると詰む
状態依存すでにhttpdがインストール済みだと、1番が想定外の動作をすることも
暗黙知の塊「別紙参照」の中身は、書いた人の頭の中にしかない
冪等性がない2回実行すると、2回目で何が起きるか保証されない

これが 命令的(Imperative) アプローチの限界です。「Aをしろ、次にBをしろ、それからCだ」という手順(How) を記述しているため、途中で失敗したら、どこまで進んだかを人間が判断し、手動でリカバリーするしかありません。

一方、K8sの 宣言的(Declarative) アプローチは、まったく異なる哲学に基づいています。

# K8sのマニフェスト(設計図)
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80

このYAMLは「手順」ではありません。「あるべき姿(What)」 を宣言しているのです。

「nginxコンテナが1台動いていて、ポート80で待ち受けている状態にしてくれ」

K8sに対して私たちが伝えるのは、これだけです。どうやってその状態を実現するかは、K8sが考えます。

VMとK8sの思考モデルの違い

観点VM(命令的)K8s(宣言的)
伝えること「何をするか(How)」「どうあるべきか(What)」
責任の所在人間が手順の正しさを保証K8sが状態の一致を保証
失敗時の対応手順書のどこで止まったか調査再度applyすれば、差分だけ適用
ドキュメント手順書+設定値+暗黙知YAMLファイルがすべてを語る

4.1.2 コントロールプレーンの役割:理想(YAML)と現実(現状)の差を埋める「ループ」

第2回で、K8sには「コントロールプレーン」と「ワーカーノード」があることを学びました。ここで、コントロールプレーンの核心的な動作原理を理解しましょう。

K8sのコントロールプレーンは、常に以下のループを回し続けています。

┌────────────────────────────┐
│                                                        │
│    ① 理想状態を読む(etcdに保存されたマニフェスト)   │
│                          ↓                            │
│    ② 現実状態を観測する(実際に動いているPod等)      │
│                          ↓                            │
│    ③ 差分を検出する(理想 − 現実 = やるべきこと)     │
│                          ↓                            │
│    ④ 差分を埋めるアクションを実行                     │
│                          ↓                            │
│    ⑤ ①に戻る(無限ループ)                           │
│                                                        │
└────────────────────────────┘

これを Reconciliation Loop(調停ループ) と呼びます。

VMの世界で喩えるなら、「24時間365日、設計図と現場を見比べて、ズレがあれば自動で直してくれる現場監督」が常駐しているようなものです。しかも、この監督は疲れを知らず、休憩も取らず、ミスもしません。

具体例:Podが死んだとき

  1. あなたが replicas: 3 と宣言した(理想状態)
  2. 現在、Podは3つ動いている(現実状態)
  3. 突然、1つのPodがクラッシュした
  4. コントロールプレーンが検知:「理想は3、現実は2。差分は1」
  5. 自動的に新しいPodを1つ起動
  6. 再び理想と現実が一致

あなたは何もしていません。深夜に電話が鳴ることもありません。

VMの世界では、この「Podが死んだ」に相当する事態——たとえばApacheプロセスのクラッシュ——が起きたとき、監視ツールがアラートを飛ばし、誰かがSSHでログインし、ログを確認し、プロセスを再起動していたはずです。

K8sは、その「誰か」の役割を、自律的に、何度でも、正確に果たしてくれるのです。


4.1.3 冪等性(べきとうせい):何度実行しても同じ結果になる安心感

冪等性(Idempotency) という言葉を聞いたことがあるでしょうか。

数学では「何度演算しても結果が変わらない性質」を指しますが、インフラの文脈では、こう言い換えられます。

「同じ操作を何度実行しても、システムは常に同じ状態になる」

VMの構築スクリプトで、これを実現するのがいかに難しいか、経験者ならご存知でしょう。

# 冪等でないスクリプトの例
echo "ServerName localhost" >> /etc/httpd/conf/httpd.conf

このコマンドを2回実行すると、ServerName localhost が2行追記されてしまいます。3回実行すれば3行。これが本番環境で起きたら?想像するだけで冷や汗が出ます。

一方、K8sの kubectl apply は、設計上、冪等 です。

# 何度実行しても、結果は同じ
kubectl apply -f deployment.yaml
kubectl apply -f deployment.yaml
kubectl apply -f deployment.yaml

1回目:Deploymentが作成される
2回目:「すでに理想状態です」→ 何も変わらない
3回目:同上

これが意味することは重大です。

VMのバックアップ・スナップショットからの解放

VMの世界では、「何かあったら戻せるように」と、こまめにスナップショットを取っていたはずです。なぜなら、手順書ベースの構築では「今の状態を再現できる保証がない」からです。

K8sでは、YAMLファイル自体が「あるべき姿」の完全な記録 です。

  • 環境がおかしくなった? → kubectl apply -f で理想状態に戻る
  • 新しいクラスタを作りたい? → 同じYAMLを適用すれば、同じ環境ができる
  • 半年前の状態に戻したい? → Gitから半年前のYAMLをcheckoutして適用

スナップショットの容量を気にする必要も、リストアに何時間もかかることもありません。テキストファイル数個で、インフラ全体が再現できるのです。


4.2 実践:マニフェストによるインフラのコード管理

理論はここまでにして、実際に手を動かしてみましょう。第3回で作成したWebサーバ環境を、YAMLファイルで「設計図化」します。

4.2.1 Webサーバ(Deployment/Service)の構成をYAMLファイルに書き出す

まず、作業用のディレクトリを作成します。

[Execution User: developer]

mkdir -p ~/k8s-practice/04-declarative
cd ~/k8s-practice/04-declarative

次に、Deploymentのマニフェストを作成します。

[Execution User: developer]

cat << 'EOF' > web-deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
  name: web-server
  labels:
    app: web
    environment: practice
spec:
  replicas: 1
  selector:
    matchLabels:
      app: web
  template:
    metadata:
      labels:
        app: web
    spec:
      containers:
      - name: nginx
        image: nginx:1.27
        ports:
        - containerPort: 80
        resources:
          requests:
            memory: "64Mi"
            cpu: "100m"
          limits:
            memory: "128Mi"
            cpu: "200m"
EOF

続いて、Serviceのマニフェストを作成します。

[Execution User: developer]

cat << 'EOF' > web-service.yaml
apiVersion: v1
kind: Service
metadata:
  name: web-service
  labels:
    app: web
spec:
  type: NodePort
  selector:
    app: web
  ports:
  - port: 80
    targetPort: 80
    nodePort: 30080
    protocol: TCP
EOF

YAMLの構造を理解する

作成したマニフェストの各フィールドが何を意味しているか、確認しておきましょう。

フィールド説明
apiVersion使用するK8s APIのバージョン。リソースの種類によって異なる
kind作成するリソースの種類(Deployment, Service, Pod等)
metadataリソースの識別情報(名前、ラベル等)
specリソースの「あるべき姿」の詳細定義

特に spec セクションが、宣言的定義の核心です。ここに書かれた内容が「理想状態」として etcd に保存され、コントロールプレーンが常に監視し続けます。


4.2.2 kubectl apply -f の実行:設定を変更して「差分だけ」が適用される様子を観察

いよいよ、マニフェストを適用します。

[Execution User: developer]

# Deploymentを適用
kubectl apply -f web-deployment.yaml

# Serviceを適用
kubectl apply -f web-service.yaml

出力を確認しましょう。

deployment.apps/web-server created
service/web-service created

created と表示されています。リソースが新規作成されたことを意味します。

状態を確認します。

[Execution User: developer]

kubectl get deployments,pods,services -l app=web
NAME                         READY   UP-TO-DATE   AVAILABLE   AGE
deployment.apps/web-server   1/1     1            1           30s

NAME                              READY   STATUS    RESTARTS   AGE
pod/web-server-6d4f8b7c9d-x2k8m   1/1     Running   0          30s

NAME                  TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
service/web-service   NodePort   10.96.123.45    <none>        80:30080/TCP   25s

素晴らしい。YAMLに書いた通りの状態が実現されています。

冪等性を体感する

では、同じコマンドをもう一度実行してみましょう。

[Execution User: developer]

kubectl apply -f web-deployment.yaml
kubectl apply -f web-service.yaml
deployment.apps/web-server unchanged
service/web-service unchanged

今度は unchanged と表示されました。「すでに理想状態なので、何も変更しませんでした」という意味です。

これが冪等性です。 何度実行しても、システムは同じ状態を維持します。

差分適用を体感する

次に、設定を変更してみましょう。レプリカ数を1から2に増やします。

[Execution User: developer]

# replicas を 1 から 2 に変更
sed -i 's/replicas: 1/replicas: 2/' web-deployment.yaml

# 変更を確認
grep replicas web-deployment.yaml
  replicas: 2

変更したマニフェストを適用します。

[Execution User: developer]

kubectl apply -f web-deployment.yaml
deployment.apps/web-server configured

今度は configured と表示されました。「設定が変更されました」という意味です。

状態を確認します。

[Execution User: developer]

kubectl get pods -l app=web
NAME                          READY   STATUS    RESTARTS   AGE
web-server-6d4f8b7c9d-x2k8m   1/1     Running   0          5m
web-server-6d4f8b7c9d-p9q3r   1/1     Running   0          10s

Podが1つから2つに増えています。K8sは「理想状態(replicas: 2)」と「現実状態(Pod 1つ)」の差分を検出し、必要な分だけ 新しいPodを作成しました。

VMの世界で同じことをしようとしたら、どうなるでしょうか。

  1. 新しいVMをプロビジョニング
  2. OSをインストール
  3. ミドルウェアをセットアップ
  4. 設定ファイルを既存サーバからコピー
  5. ロードバランサに追加
  6. 動作確認

この手順を、K8sは kubectl apply 一発で、数秒で完了させました。


4.2.3 ドライラン(–dry-run=client)の活用:本番適用前の安全確認フロー

「YAMLを書き換えたけど、適用したらどうなるか不安…」

その気持ち、よくわかります。VMの世界で、本番サーバに設定変更を入れる前、何度もテスト環境で確認してきたはずです。

K8sには、ドライラン(Dry Run) という機能があります。これは「実際には何も変更せず、変更した場合の結果だけを表示する」モードです。

[Execution User: developer]

# ドライランで差分を確認(実際には適用されない)
kubectl apply -f web-deployment.yaml --dry-run=client -o yaml

さらに、現在の状態との差分を視覚的に確認したい場合は、diff オプションが使えます。

[Execution User: developer]

# 現在の状態との差分を表示
kubectl diff -f web-deployment.yaml

このコマンドは、まさにLinuxの diff コマンドのように、変更点をハイライトして表示してくれます。

本番適用前のセーフティネット

インフラエンジニアにとって、「本番環境への変更」は常に緊張の瞬間です。手順書を三重にチェックし、ロールバック手順を用意し、それでも何かが起きないかヒヤヒヤする——。

K8sのドライランと差分確認は、その緊張を大きく和らげてくれます。

# 推奨する本番適用フロー
# 1. まず差分を確認
kubectl diff -f production-deployment.yaml

# 2. 問題なければドライランで最終確認
kubectl apply -f production-deployment.yaml --dry-run=server

# 3. 確信を持って適用
kubectl apply -f production-deployment.yaml

--dry-run=server は、クライアント側ではなくAPIサーバ側でバリデーションを行うモードです。より本番に近い検証ができます。


4.3 理論:コントロールプレーンが「裏側」で行っていること

4.3.1 「1台動かして」と願うだけ。K8sが自律的に帳尻を合わせる仕組み

ここまでの実践で、kubectl apply の便利さを体感していただけたと思います。しかし、なぜこんなことが可能なのでしょうか?その裏側を少し覗いてみましょう。

コントローラという「自律的な番人」

K8sには、コントローラ(Controller) と呼ばれるコンポーネントが複数存在します。それぞれが特定のリソースを監視し、理想状態との差分を埋める責任を持っています。

┌──────────────────────────────┐
│                    Control Plane                           │
├──────────────────────────────┤
│                                                            │
│  ┌────────┐  ┌────────┐                │
│  │ Deployment     │  │ ReplicaSet     │                │
│  │ Controller     │  │ Controller     │                │
│  │                │  │                │                │
│  │ "Deploymentの  │  │ "ReplicaSetの  │                │
│  │  理想を監視"   │  │  理想を監視"   │                │
│  └───┬────┘  └───┬────┘                │
│          │                    │                          │
│          └────┬─────┘                          │
│                    ▼                                      │
│           ┌────────┐                             │
│           │     etcd       │                             │
│           │  (理想状態DB)  │                             │
│           └────────┘                             │
│                                                            │
└──────────────────────────────┘

例えば、Deploymentを適用したとき、裏側では以下のような連鎖が起きています。

  1. Deployment Controller が「Deploymentの理想状態」を監視
  2. 理想状態に対応する ReplicaSet を作成・更新
  3. ReplicaSet Controller が「ReplicaSetの理想状態」を監視
  4. 理想のレプリカ数に達するまで Pod を作成
  5. Scheduler がPodを適切なノードに配置
  6. kubelet がノード上でコンテナを起動

あなたが書いたYAMLは、この連鎖の「起点」に過ぎません。あとは、各コントローラが自律的に動き、システム全体が理想状態に収束していきます。

VMとの決定的な違い

VMの世界では、「サーバAのApacheが落ちた」というイベントが発生したら、それを検知し、対処するのは人間(またはそのための自動化スクリプト)の責任でした。

K8sでは、この「検知 → 対処」のループがシステムに内蔵されています。

  • 人間:「Podが1台動いていてほしい」と宣言
  • K8s:「了解。1台の状態を維持し続けます」

Podが死のうが、ノードが落ちようが、K8sは粛々と「1台の状態」を維持しようとします。人間に相談することなく、自律的に。


4.3.2 手順書がいらなくなる日:インフラの「再現性」がもたらす恩恵

宣言的定義がもたらす最大の恩恵は、インフラの完全な再現性 です。

GitOpsという発想

YAMLファイルをGitで管理すると、インフラに以下の恩恵がもたらされます。

Gitの機能インフラへの恩恵
バージョン管理いつ、誰が、何を変更したか完全に追跡可能
ブランチ本番/ステージング/開発環境を分離管理
プルリクエスト変更を本番適用前にレビュー
ロールバックgit revertkubectl apply で即座に前の状態へ
差分表示インフラの変更履歴が git diff で一目瞭然

VMの世界で「3ヶ月前の本番環境の状態」を再現しようとしたら、どれほどの労力がかかるでしょうか。スナップショットがあればまだマシですが、なければ絶望的です。

K8sとGitの組み合わせなら、こうなります。

# 3ヶ月前のインフラ状態を再現
git checkout HEAD~90 -- manifests/
kubectl apply -f manifests/

# やっぱり最新に戻す
git checkout main -- manifests/
kubectl apply -f manifests/

これが「Infrastructure as Code(IaC)」の真髄であり、K8sが自然にそれを可能にしてくれる理由です。

手順書の終焉

従来の手順書は、「インフラの状態を再現するための記録」でした。しかし、以下の問題を常に抱えていました。

  • 手順書と実際の環境が乖離する(ドキュメントの陳腐化)
  • 手順書通りに実行しても、環境の微妙な違いで失敗する
  • 手順書を書いた人が退職すると、暗黙知が失われる

K8sのマニフェストは、それ自体が「生きた手順書」 です。

  • マニフェストと環境は常に一致している(ズレたらK8sが直す)
  • マニフェストを適用すれば、確実に同じ環境ができる
  • マニフェストを読めば、システムの全容がわかる

手順書を書く時間を、YAMLを書く時間に置き換える。それだけで、インフラ運用の信頼性は劇的に向上します。


4.3.3 【次回予告】コマンド一つでサーバーを10台に増やす「レプリケーション」

今回、replicas: 1replicas: 2 に変更して、Podが自動的に増える様子を見ました。

次回は、この「レプリケーション」の仕組みをさらに深掘りします。

  • トラフィックの急増に応じて、Podを10台、100台に増やす
  • 逆に、深夜の閑散期には1台に減らしてリソースを節約する
  • そして、それを「手動」ではなく「自動」で行う方法

VMの世界で「サーバを10台増やす」と言われたら、何日かかりますか?K8sなら、コマンド一発、数秒です。

次回:「スケーリングの魔法——需要に応じてPodを増減させる」


4.4 トラブルシューティングのTips

YAMLの落とし穴と、それを回避する方法

YAMLは人間にとって読みやすいフォーマットですが、いくつかの「落とし穴」があります。ここでは、よくあるエラーとその対処法を紹介します。

落とし穴1:インデントミス(タブ文字混入)

YAMLでは、インデントにスペースのみが許可されています。タブ文字を使うと、パースエラーになります。

# エラー例
error: error parsing web-deployment.yaml: error converting YAML to JSON: 
yaml: line 8: found character that cannot start any token

対処法:

[Execution User: developer]

# タブ文字をスペースに変換
sed -i 's/\t/  /g' web-deployment.yaml

# タブ文字の存在を確認(何も出力されなければOK)
grep -P '\t' web-deployment.yaml

落とし穴2:apiVersionの記述間違い

K8sのリソースは、種類(kind)によって使用する apiVersion が異なります。間違えると、リソースが見つからないエラーになります。

# エラー例
error: resource mapping not found for name: "web-server" namespace: "" 
from "web-deployment.yaml": no matches for kind "Deployment" in version "v1"

対処法: kubectl explain コマンドで、正しいAPIバージョンを確認できます。

[Execution User: developer]

# Deploymentの正しいAPIバージョンを確認
kubectl explain deployment | head -5
GROUP:      apps
KIND:       Deployment
VERSION:    v1

DESCRIPTION:
...

GROUPVERSION を組み合わせて、apiVersion: apps/v1 が正しいことがわかります。

落とし穴3:ラベルのミスマッチ

Deployment の spec.selector.matchLabelsspec.template.metadata.labels が一致していないと、Podが作成されません。

[Execution User: developer]

# Deploymentの構造を確認
kubectl explain deployment.spec.selector

ベストプラクティス: 以下のように、ラベルを明示的に一致させます。

spec:
  selector:
    matchLabels:
      app: web        # ← ここと
  template:
    metadata:
      labels:
        app: web      # ← ここが一致している必要がある

便利コマンド:kubectl explain

kubectl explain は、K8sリソースの「生きたリファレンス」です。Webで検索するより早く、正確な情報を得られます。

[Execution User: developer]

# Deploymentの全フィールドを確認
kubectl explain deployment --recursive | less

# 特定のフィールドの説明を確認
kubectl explain deployment.spec.replicas
KIND:       Deployment
VERSION:    apps/v1

FIELD: replicas <integer>

DESCRIPTION:
    Number of desired pods. This is a pointer to distinguish between explicit
    zero and not specified. Defaults to 1.

困ったときは、まず kubectl explain を叩く。これをクセにしておくと、トラブルシューティングの速度が格段に上がります。


まとめ:今回学んだこと

  1. 命令的 vs 宣言的: VMの「手順書」はHowを記述し、K8sの「マニフェスト」はWhatを宣言する
  2. Reconciliation Loop: K8sは常に「理想」と「現実」の差分を監視し、自律的に埋め続ける
  3. 冪等性: kubectl apply は何度実行しても同じ結果になり、差分だけが適用される
  4. ドライラン: --dry-rundiff で、本番適用前に安全確認ができる
  5. Infrastructure as Code: YAMLとGitの組み合わせで、インフラの完全な再現性が手に入る

次回予告: 「スケーリングの魔法」——replicas の値を変えるだけで、Podが増減する仕組みを体験します。そして、トラフィックの変化に応じて自動的にスケールする「オートスケーリング」の世界へ。

VMの時代、「サーバ増設」は大仕事でした。K8sでは、それが日常の些事になります。