K8s 基礎 #3 kubectl と最初の Pod

読了 13分

#2 ローカル環境 でクラスタ 1 台をノート PC の上に乗せました。この記事ではそのクラスタに初めて Pod 1 つを乗せ、中に入って、ログを読み、きれいに片付けるまでの 1 サイクルを扱います。その過程で kubectl の基本コマンドと Pod の役割も合わせて身につけます。

このシリーズは K8s 基礎 7 編です。

この記事の終わりには kubectl の日常コマンド群が手についていて、Pod という単位を 1 度自分で作ってみた 状態になります。次の記事からは、この Pod を人が直接作るのではなくコントローラに任せる話に移ります。

Pod とは何か #

#1 のリソース表で 1 行で見た定義 — Pod はコンテナ 1 つ、または一緒に動く数個をまとめた K8s の最小実行単位。これを少し詳しく解きほぐします。

Docker 基礎シリーズを通ってきた読者であれば、頭の中に既に コンテナ という単位が入っています。K8s はその上にもう 1 層を置きます — コンテナ 1 つ、または複数をまとめて一緒にスケジューリング・実行する単位が Pod です。同じ Pod 内のコンテナは次を共有します。

  • ネットワーク namespace — 同じ IP、同じポート空間。1 つの Pod の中では localhost で互いを呼ぶ。
  • 一部のボリューム — Pod に定義されたボリュームを複数のコンテナが一緒にマウントできます。
  • 寿命 — 一緒に立ち、一緒に死ぬ (同じノード上で)。

Docker のコンテナと一番混同しやすい点がここです。Docker では「コンテナ 1 つ = 実行単位 1 つ」でしたが、K8s では「Pod 1 つ = 実行単位 1 つ」で、その Pod の中にコンテナが 1 つの場合も 2 つの場合もあります。頭の中の式としては Pod ≈ 「1 つの IP を共有するコンテナグループ」 くらいで無難です。

ただし実務の 99% 以上は 1 Pod 1 コンテナ です。同じ Pod にコンテナを 2 つ一緒に置くのは、サイドカー (ログ収集、プロキシなど) のように、そのコンテナがメインコンテナにくっついて動くべき場合に限られます。最初は「Pod = コンテナ 1 つを K8s が回すために 1 層包んだもの」と理解しても十分です。

もう 1 つ — 運用で直接 kind: Pod を手で立てることはほぼありません。 通常は #4 Deployment が代わりに作ってくれます。この記事で Pod を直接扱うのは K8s を理解する土台を作るためです。人がマニフェストに書く単位はほぼ常に Deployment・StatefulSet・Job のような コントローラ で、Pod はそのコントローラが作り出す産物です。

kubectl コマンドを 1 つの表に #

身に付けておく日常コマンドを 1 つの表に整理します。シリーズ全体で繰り返し出会う形です。

コマンド何をするか
kubectl get <res>リソース一覧を見る。例: kubectl get podskubectl get nodes
kubectl get <res> <name>特定のリソース 1 つの要約を見る
kubectl get <res> -o wide既定の列以外に IP・ノードなどの追加情報まで
kubectl get <res> <name> -o yamlそのリソースの完全な定義を YAML で
kubectl describe <res> <name>そのリソースの詳細 + 最近のイベント (Events)
kubectl logs <pod>Pod コンテナの stdout/stderr
kubectl logs -f <pod>follow — tail -f のように
kubectl exec -it <pod> -- <cmd>立っている Pod コンテナ内でコマンドを実行
kubectl apply -f file.yamlマニフェストをクラスタに反映 (宣言的)
kubectl delete -f file.yaml同じマニフェストで作られたリソースを削除
kubectl delete <res> <name>名前でリソースを削除
kubectl run <name> --image=<img>Pod 1 つを命令的にその場で生成

この表で <res> の部分には podspodpodeploymentsdeployservicessvc のような名前が入ります。よく使うリソースには短縮形があるので、入力量が早く減ります。

命令的 vs 宣言的 #

kubectl でクラスタを扱う道は 2 種類あります。

  • 命令的 (imperative)kubectl runkubectl create deployment のように「今これを作れ」と直接命令。速くて手軽ですが、そのコマンドを再現するには命令そのものがどこかに書かれている必要があります。
  • 宣言的 (declarative) — YAML マニフェストに「こういう形であるべき」を書いて kubectl apply -f file.yaml。マニフェストファイルがすなわちクラスタ状態の記録なので Git に上げやすく、同じファイルを再度 apply するだけで同じ状態が出ます。

実務はほぼ全てが宣言的です。クラスタ状態がコードとして記録される という点が決定的です。このシリーズも初回 1 度だけ命令的に Pod を立てて、すぐマニフェストに移ります。

命令的に最初の Pod — kubectl run #

まず一番短い道で Pod 1 つを上げてみましょう。イメージはどこでも入手できる nginx:1.27 を使います。

命令的に Pod 1 つ
kubectl run hello --image=nginx:1.27 --port=80
出力例
pod/hello created

--port=80 は情報的なフラグです。外部からこのポートに入ってこさせる作業は #5 Service でやります。今の段階では「Pod の中の nginx が 80 番を聞いている」というメモ程度に見てください。

作られた結果を確認します。

Pod 一覧
kubectl get pods
出力例
NAME    READY   STATUS    RESTARTS   AGE
hello   1/1     Running   0          12s

READY 1/1 は Pod 内のコンテナ 1 つのうち 1 つが準備されたという意味で、STATUS Running なら期待通りです。列名を 1 行で押さえると — NAME / READY / STATUS / RESTARTS / AGE。シリーズの最後までよく見ます。

もう少し詳しい情報が必要なときは -o wide を付けます。

追加列まで
kubectl get pods -o wide
出力例
NAME    READY   STATUS    RESTARTS   AGE   IP           NODE                 NOMINATED NODE   READINESS GATES
hello   1/1     Running   0          25s   10.244.0.5   kind-control-plane   <none>           <none>

IP は Pod に割り当てられたクラスタ内部 IP、NODE はどの worker node に立っているかです。単一ノード kind クラスタなので kind-control-plane 1 か所にしか集まりませんが、マルチノードクラスタならここでどの Pod がどこに散ったかが見えます。NOMINATED NODEREADINESS GATES は通常は空で、高度なスケジューリングや条件を使うときに埋まります。

describe — そこで何があったのか #

一覧だけでは Pod がどうやってそこまで来たのか分かりません。describe がその空白を埋めてくれます。

Pod 詳細
kubectl describe pod hello
出力例 — 抜粋
Name:         hello
Namespace:    default
Priority:     0
Node:         kind-control-plane/172.18.0.2
Start Time:   ...
Labels:       run=hello
...
Containers:
  hello:
    Image:          nginx:1.27
    Port:           80/TCP
    State:          Running
      Started:      ...
    Ready:          True
    Restart Count:  0
...
Events:
  Type    Reason     Age   From               Message
  ----    ------     ----  ----               -------
  Normal  Scheduled  30s   default-scheduler  Successfully assigned default/hello to kind-control-plane
  Normal  Pulling    29s   kubelet            Pulling image "nginx:1.27"
  Normal  Pulled     25s   kubelet            Successfully pulled image "nginx:1.27"
  Normal  Created    25s   kubelet            Created container hello
  Normal  Started    25s   kubelet            Started container hello

上半分には Pod の定義 (ノード、ラベル、コンテナ、状態) が、下半分には Events が時系列で書かれています。この Events セクションが K8s デバッグの第 1 の武器です — ScheduledPullingPulledCreatedStarted#1 で図でしか見ていなかった流れがそのまま書かれています。scheduler がノードを決め、そのノードの kubelet がイメージを取得してコンテナを作り、起動する順序です。

何か問題が起きたときも、答えはほぼ常にこの Events の中にあります。イメージ名を間違えていれば Failed to pull image が、コンテナが起動直後に死ねば Back-off restarting failed container がここに上がってきます。

logs — Pod が何を言っているか #

Pod コンテナの stdout/stderr は kubectl logs で見ます。

ログを見る
kubectl logs hello
出力例 — 抜粋
/docker-entrypoint.sh: /docker-entrypoint.d/ is not empty, will attempt to perform configuration
/docker-entrypoint.sh: Launching /docker-entrypoint.d/10-listen-on-ipv6-by-default.sh
...
/docker-entrypoint.sh: Configuration complete; ready for start up
... [notice] start worker processes

Docker の docker logs と 1:1 対応するコマンドです。よく使うバリエーションも同じです。

便利な形
kubectl logs -f hello             # follow
kubectl logs --tail=100 hello     # 最後 100 行
kubectl logs --since=10m hello    # 直近 10 分
kubectl logs --previous hello     # 以前に死んだコンテナのログ

最後の --previous は K8s でしか出会わないオプションなので慣れる必要があります。コンテナが 1 度死んでまた起動した場合、死ぬ直前のログは現在のコンテナの stdout ではなく、以前のコンテナインスタンス の出力です。クラッシュ原因を追うときによく使います。

exec — Pod の中に入る #

Docker の docker exec に対応するコマンドです。

Pod コンテナの中へ
kubectl exec -it hello -- bash

-it はインタラクティブ + TTY、その後ろの --ここから先は Pod 内で実行するコマンド という区切りです。-- を抜かすと kubectl が後ろのオプションを自分のものとして解釈してしまうのでよくミスします。

中に入って nginx の設定を 1 度確認して出ます。

コンテナの中で
root@hello:/# curl -s localhost:80 | head -5
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
root@hello:/# exit

単発コマンドだけ実行したいなら -it 無しでもいいです。

単発コマンド
kubectl exec hello -- nginx -v
# nginx version: nginx/1.27.x

最初の片付け #

ここまでで命令的な 1 サイクルが終わります。きれいに片付けます。

Pod の削除
kubectl delete pod hello
出力例
pod "hello" deleted

kubectl get pods で空であることを確認すれば出発点に戻ります。

宣言的に書き直す — YAML マニフェスト #

同じ Pod を今度はマニフェストに書いてみましょう。ほぼ全ての K8s マニフェストは 4 つの最上位フィールドで始まります。

フィールド意味
apiVersionこのリソースを定義する K8s API のバージョン。Pod は安定グループの v1 なので v1
kindリソースの種類。ここでは Pod
metadata名前・ラベル・namespace などの識別情報
spec「このリソースがどうあるべきか」の本体。リソース種類ごとに形が違う

この 4 フィールドの形は Pod でも Deployment でも Service でも同じです。覚えておけば全てのマニフェストの背骨になります。

hello-pod.yaml ファイルを次のように書きます。

hello-pod.yaml
apiVersion: v1
kind: Pod
metadata:
  name: hello
  labels:
    app: hello
spec:
  containers:
    - name: web
      image: nginx:1.27
      ports:
        - containerPort: 80

同じディレクトリで apply を呼びます。

マニフェストで Pod を作る
kubectl apply -f hello-pod.yaml
出力例
pod/hello created

kubectl get pods で結果が同じか確認します — 出てくる形は kubectl run と全く同じです。違いはクラスタの外側にあります。

マニフェストの長所 #

  • Git に上がります。 クラスタ状態がコードとして記録されるので、「この環境の形」を PR でレビューし履歴で残せます。
  • 再度 apply するだけで同じ状態が出る。 命令的は頭/ターミナル履歴に依存しますが、マニフェストはファイルそのものが真実です。
  • apply はべき等 (idempotent) です。 既に存在すれば差分だけ反映、無ければ作ります。この性質が CI/CD とよく合います。
  • コントローラへ自然に拡張できます。 同じマニフェストの kindDeployment に変えるだけで #4 にそのまま移ります。

K8s が埋めてくれるフィールド #

マニフェストには私たちが書いたものしか入っていませんが、実際にクラスタに入ったオブジェクトにはもっと多くのフィールドが付いています。覗いてみましょう。

実際のオブジェクトを YAML で見直す
kubectl get pod hello -o yaml | head -40
出力例 — 抜粋
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "..."
  labels:
    app: hello
  name: hello
  namespace: default
  resourceVersion: "..."
  uid: ...-...-...-...-...
spec:
  containers:
    - image: nginx:1.27
      imagePullPolicy: IfNotPresent
      name: web
      ports:
        - containerPort: 80
          protocol: TCP
      ...
  nodeName: kind-control-plane
  ...
status:
  conditions:
    - ...
  phase: Running
  podIP: 10.244.0.5
  ...

私たちが書いた部分 (名前・ラベル・イメージ・ポート) はそのまま残っていて、そこに K8s が次のようなフィールドを埋めています。

  • metadata.uidmetadata.resourceVersionmetadata.creationTimestamp — このオブジェクトの素性
  • spec.nodeName — scheduler が決めたノード
  • spec.containers[].imagePullPolicy: IfNotPresent のような既定値
  • status ブロック全体 — Phase、Pod IP、conditions などの 現在の状態

#1 で見た desired state vs actual state が 1 つのオブジェクトの中に一緒に入っているのが、まさにこの形です。spec が desired、status が actual。コントローラは絶え間なく両者を比較します。

Pod ライフサイクルを 1 度 #

Pod は Phase という概念を持ちます。kubectl get pod 出力の STATUS 列が映しているのは概ねこの Phase です。正確な値は 5 つです。

Phase意味
Pendingオブジェクトは作られたがまだコンテナが起動していない状態。イメージダウンロード中またはノード割当待ちなど
Runningノードに割り当てられ、コンテナのうち少なくとも 1 つが実行中
Succeeded全コンテナが正常に終了 (exit 0)。再起動しない
Failed全コンテナが終了し、そのうち少なくとも 1 つが異常終了
Unknownapiserver がノードと通信できず状態が分からない

Web サーバのようにずっと立っているべき Pod は一生を Running で過ごします。バッチ作業のように 1 度回って終わるべき Pod は SucceededFailed に入ります。

kubectl describe の Events セクションでこの流れをもう 1 度押さえましょう — ScheduledPullingPulledCreatedStarted。上で見た出力と同じ順序です。この 5 つの単語のどこで止まるかがデバッグの最初の分岐です。

よくある失敗 2 つ #

最初に最もよく出会う失敗は 2 つです。

ImagePullBackOff / ErrImagePull — イメージを取得できませんでした。名前のタイポ、プライベートレジストリの認証漏れ、そのタグが本当に無い、など。kubectl describe pod <name> の Events に Failed to pull image "..." メッセージとその下に理由が書かれています。そこから始めればいいです。

CrashLoopBackOff — コンテナが起動直後に死んで、K8s が再起動して、また死んで… が繰り返される状態。第 1 の手がかりは kubectl logs <pod>、直前に死んだコンテナのログが必要なら kubectl logs --previous <pod>RESTARTS 列が早く上がっていく形でも見えます。

この 2 つのパターンはシリーズを通して繰り返し出会います。最初の手がかりが describe の Events / 2 番目が logs — この順序だけ手元に置けば 9 割は解けます。

Pod 1 つではなぜ足りないか #

ここまで付いてきたら自然に 1 つ気にかかり始めます — この Pod は死んだらどうなるのか? 試しに強制的に 1 度消してみましょう。

Pod を強制削除すると
kubectl delete pod hello
出力例
pod "hello" deleted
再確認
kubectl get pods
出力例
No resources found in default namespace.

ただ消えます。 自動でまた立ちません。K8s の立場から見ると、私たちは「この Pod があるべき」とどこにも書いておかなかったからです。apply で 1 度作ったオブジェクトを人が消したのだから、それが人の意図だと見なされます。

#1 で見た reconcile loop が働くには「これが N 個立っているべき」という 上位の宣言 が必要です。それを持っているのが次の記事の主役 Deployment です。Deployment が ReplicaSet を作り、ReplicaSet が「この Pod テンプレートで N 個維持」という責任を負います。なので同じ実験を Deployment で行うと、Pod を消した直後に新しい Pod が自動でまた立ちます。

K8s 公式ドキュメントが Pod に対してよく使う表現が mortal — いつか死ぬ存在という意味です。ノードが死ぬ、コンテナが OOM で死ぬ、誰かが間違えて消す、すると Pod はただ消えて、新しく立ててくれる人もいません。自動再起動・再配置は K8s がただではくれません — コントローラを通してくれます。

なので最初に話した 1 行がまた来ます — 運用で kind: Pod を直接使うことはほぼありません。 デバッグ用の一時 Pod、Job が作る使い捨て Pod、DaemonSet がノードごとに立てる Pod、のような特殊ケースを除けば、私たちが書くマニフェストはほぼ常にコントローラです。この記事の Pod は、そうしたコントローラが内部で何を作り出すのかを見るための土台です。

片付け #

今日の実験の痕跡をきれいに消します。マニフェストで作ったリソースは同じマニフェストで消すのが最も確実です。

マニフェストで片付け
kubectl delete -f hello-pod.yaml
出力例
pod "hello" deleted

名前で直接消す道もあります。

名前で片付け
kubectl delete pod hello

最後に default namespace が空であることだけ確認しておきます。

空か
kubectl get pods
出力例
No resources found in default namespace.

#2 で見た kube-system のシステム pod はそのまま立っています。そちらはクラスタが自分の運用のために持っているもので、私たちが作ったワークロードとは性質が異なります。両者が混ざらないよう namespace で分ける話は #7 に回します。

まとめ #

この記事で押さえた流れ:

  • Pod は K8s の最小実行単位 — コンテナ 1 つ、または一緒に動く数個をまとめ、同じ IP・ポート空間と一部のボリュームを共有させる。実務の 99% は 1 Pod 1 コンテナ。
  • kubectl の日常コマンド群は get / describe / logs / exec / apply / delete の 6 つが事実上全て。短縮形 (podeploysvc など) もすぐに手につく。
  • 命令的 (kubectl run) は速いが再現が難しく、宣言的 (YAML + kubectl apply) はクラスタ状態をコードとして記録します。実務はほぼ全てが宣言的。
  • マニフェストの背骨は apiVersion / kind / metadata / spec の 4 フィールド。K8s はそこに metadata.uidspec.nodeNamestatus のようなフィールドを埋めて desired と actual を 1 オブジェクトに収める。
  • Pod の Phase は Pending / Running / Succeeded / Failed / Unknown の 5 つで、デバッグは kubectl describe の Events → kubectl logs(--previous) の順が第 1・第 2 の手がかり。
  • Pod は mortal — 死ねばただ消える。なので自動再起動・再配置を担当するコントローラが別にあり、その出発点が次の記事の Deployment。

次 — Deployment / ReplicaSet #

この記事の最後で見た 1 つ — Pod を消すとただ消える — が次の記事の出発点です。人が一々見ていなくても「この Pod が N 個立っているべき」を K8s が絶え間なく維持してくれる抽象が必要です。

#4 Deployment / ReplicaSet では (1) ReplicaSet がどうやって Pod の数を保証するか(2) Deployment がその上で何を加えてくれるか (ローリングアップデート、ロールバック)(3) 同じ nginx Pod を Deployment マニフェストで書き直して replicas: 3 で立て、1 つ消したときどう自動復旧するか を扱います。この記事の Pod 1 つが、そこから本当のワークロードのように振る舞い始めます。

X