K8s 基礎 #3 kubectl と最初の Pod
#2 ローカル環境 でクラスタ 1 台をノート PC の上に乗せました。この記事ではそのクラスタに初めて Pod 1 つを乗せ、中に入って、ログを読み、きれいに片付けるまでの 1 サイクルを扱います。その過程で kubectl の基本コマンドと Pod の役割も合わせて身につけます。
このシリーズは K8s 基礎 7 編です。
- #1 Kubernetes とは — なぜコンテナオーケストレーターが必要か
- #2 ローカル環境 — minikube / kind / Docker Desktop k8s
- #3 kubectl と最初の Pod ← この記事
- #4 Deployment / ReplicaSet
- #5 Service — ClusterIP / NodePort / LoadBalancer
- #6 ConfigMap / Secret
- #7 Namespace とラベル
この記事の終わりには 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 pods、kubectl 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> の部分には pods、pod、po、deployments、deploy、services、svc のような名前が入ります。よく使うリソースには短縮形があるので、入力量が早く減ります。
命令的 vs 宣言的 #
kubectl でクラスタを扱う道は 2 種類あります。
- 命令的 (imperative) —
kubectl run、kubectl create deploymentのように「今これを作れ」と直接命令。速くて手軽ですが、そのコマンドを再現するには命令そのものがどこかに書かれている必要があります。 - 宣言的 (declarative) — YAML マニフェストに「こういう形であるべき」を書いて
kubectl apply -f file.yaml。マニフェストファイルがすなわちクラスタ状態の記録なので Git に上げやすく、同じファイルを再度applyするだけで同じ状態が出ます。
実務はほぼ全てが宣言的です。クラスタ状態がコードとして記録される という点が決定的です。このシリーズも初回 1 度だけ命令的に Pod を立てて、すぐマニフェストに移ります。
命令的に最初の Pod — kubectl run #
まず一番短い道で Pod 1 つを上げてみましょう。イメージはどこでも入手できる nginx:1.27 を使います。
kubectl run hello --image=nginx:1.27 --port=80pod/hello created
--port=80は情報的なフラグです。外部からこのポートに入ってこさせる作業は #5 Service でやります。今の段階では「Pod の中の nginx が 80 番を聞いている」というメモ程度に見てください。
作られた結果を確認します。
kubectl get podsNAME READY STATUS RESTARTS AGE
hello 1/1 Running 0 12sREADY 1/1 は Pod 内のコンテナ 1 つのうち 1 つが準備されたという意味で、STATUS Running なら期待通りです。列名を 1 行で押さえると — NAME / READY / STATUS / RESTARTS / AGE。シリーズの最後までよく見ます。
もう少し詳しい情報が必要なときは -o wide を付けます。
kubectl get pods -o wideNAME 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 NODE と READINESS GATES は通常は空で、高度なスケジューリングや条件を使うときに埋まります。
describe — そこで何があったのか #
一覧だけでは Pod がどうやってそこまで来たのか分かりません。describe がその空白を埋めてくれます。
kubectl describe pod helloName: 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 の武器です — Scheduled → Pulling → Pulled → Created → Started。#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 processesDocker の 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 に対応するコマンドです。
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 サイクルが終わります。きれいに片付けます。
kubectl delete pod hellopod "hello" deletedkubectl get pods で空であることを確認すれば出発点に戻ります。
宣言的に書き直す — YAML マニフェスト #
同じ Pod を今度はマニフェストに書いてみましょう。ほぼ全ての K8s マニフェストは 4 つの最上位フィールドで始まります。
| フィールド | 意味 |
|---|---|
apiVersion | このリソースを定義する K8s API のバージョン。Pod は安定グループの v1 なので v1 |
kind | リソースの種類。ここでは Pod |
metadata | 名前・ラベル・namespace などの識別情報 |
spec | 「このリソースがどうあるべきか」の本体。リソース種類ごとに形が違う |
この 4 フィールドの形は Pod でも Deployment でも Service でも同じです。覚えておけば全てのマニフェストの背骨になります。
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 を呼びます。
kubectl apply -f hello-pod.yamlpod/hello createdkubectl get pods で結果が同じか確認します — 出てくる形は kubectl run と全く同じです。違いはクラスタの外側にあります。
マニフェストの長所 #
- Git に上がります。 クラスタ状態がコードとして記録されるので、「この環境の形」を PR でレビューし履歴で残せます。
- 再度
applyするだけで同じ状態が出る。 命令的は頭/ターミナル履歴に依存しますが、マニフェストはファイルそのものが真実です。 applyはべき等 (idempotent) です。 既に存在すれば差分だけ反映、無ければ作ります。この性質が CI/CD とよく合います。- コントローラへ自然に拡張できます。 同じマニフェストの
kindをDeploymentに変えるだけで #4 にそのまま移ります。
K8s が埋めてくれるフィールド #
マニフェストには私たちが書いたものしか入っていませんが、実際にクラスタに入ったオブジェクトにはもっと多くのフィールドが付いています。覗いてみましょう。
kubectl get pod hello -o yaml | head -40apiVersion: 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.uid、metadata.resourceVersion、metadata.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 つが異常終了 |
Unknown | apiserver がノードと通信できず状態が分からない |
Web サーバのようにずっと立っているべき Pod は一生を Running で過ごします。バッチ作業のように 1 度回って終わるべき Pod は Succeeded か Failed に入ります。
kubectl describe の Events セクションでこの流れをもう 1 度押さえましょう — Scheduled → Pulling → Pulled → Created → Started。上で見た出力と同じ順序です。この 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 度消してみましょう。
kubectl delete pod hellopod "hello" deletedkubectl get podsNo 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.yamlpod "hello" deleted名前で直接消す道もあります。
kubectl delete pod hello最後に default namespace が空であることだけ確認しておきます。
kubectl get podsNo 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 つが事実上全て。短縮形 (po、deploy、svcなど) もすぐに手につく。- 命令的 (
kubectl run) は速いが再現が難しく、宣言的 (YAML +kubectl apply) はクラスタ状態をコードとして記録します。実務はほぼ全てが宣言的。 - マニフェストの背骨は
apiVersion/kind/metadata/specの 4 フィールド。K8s はそこにmetadata.uid・spec.nodeName・statusのようなフィールドを埋めて 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 つが、そこから本当のワークロードのように振る舞い始めます。