Kubernetes特集 | 第5章(Day 6 / 6・最終章)
可用性・RBAC・イメージ・監視・オートスケール・アップグレード・障害対応。
「動いた」の先にある、本番に出す前の準備をまとめます。
最終日です。昨日の第4章では、別のNamespaceからServiceを名前で呼び、NetworkPolicyで通信を絞り、PVCでPodを消しても残るデータを作りました。ここまでで、アプリを動かし、届け、データを残すところまでは手が届いています。
今日はその先です。Nodeが1台止まったら。誰かが権限を持ちすぎていたら。夜中にPodが Pending のまま増えていったら。本番で困るのは、Kubernetesの機能を知らないことより、壊れたときに何を見てどう判断するかが決まっていないことです。この章では、可用性、権限、イメージ、観測、オートスケール、アップグレード、障害対応を順に見て、最後に本番導入のチェックリストにまとめます。
今日だけは、Nodeが複数あるクラスターを使います。Node障害やドレイン(Nodeを空にする作業)は、Nodeが1台では試せないからです。作り方は下の「今日つくる構成」に書きました。
💡 本番のKubernetesで難しいのは、クラスターを作ることより、壊れたときに安全に判断できる運用を作ることです。可用性はreplicasの数だけでは決まりません。Podを複数の障害ドメインへ散らし、PodDisruptionBudget(PDB)で同時に止めてよい数を決め、アプリが止まり方を知っていて、初めて「1台止まっても大丈夫」と言えます。セキュリティはRBACだけでなく、イメージ、Secret、NetworkPolicy、Admission、監査、Nodeの更新の組み合わせです。監視・ログ・トレースで「遅い」「失敗した」「どこで詰まった」を分け、アップグレードは緊急作業にせず、段階適用とロールバックを定期的に練習します。
言い換えると、この章のテーマは「仕組みにする」です。経験のある人が夜中に勘でコマンドを打つ運用は、その人がいない日に崩れます。設定、手順、確認項目を決めておけば、当番が誰でも同じ判断に近づけます。
kubectl auth can-i で確かめられるkindで、Control Plane 1台とworker 3台のクラスター ops を作ります。workerにはゾーンのラベルを付けて、クラウドの「3つのアベイラビリティゾーン」に見立てます。そこへ第3章と同じ api を3つ置き、PDB、ServiceAccount、HPAを足していきます。最終的に図5-1の形になります。
図5-1: 今日つくる構成。worker 3台をゾーンに見立て、api を1つずつ散らす。
# kind-ops.yaml ― Control Plane 1台、worker 3台
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
- role: worker
- role: worker
- role: worker
kind create cluster --name ops --config kind-ops.yaml
kubectl config current-context # kind-ops になっていることを確認(本番に向いていないこと)
# worker にゾーンのラベルを付けて、3つのゾーンに見立てる
kubectl label node ops-worker topology.kubernetes.io/zone=zone-a
kubectl label node ops-worker2 topology.kubernetes.io/zone=zone-b
kubectl label node ops-worker3 topology.kubernetes.io/zone=zone-c
kubectl get nodes -L topology.kubernetes.io/zone
kubectl create namespace lab
# 出力の例(バージョンは環境によって違う)
NAME STATUS ROLES AGE VERSION ZONE
ops-control-plane Ready control-plane 60s v1.33.1
ops-worker Ready <none> 40s v1.33.1 zone-a
ops-worker2 Ready <none> 40s v1.33.1 zone-b
ops-worker3 Ready <none> 40s v1.33.1 zone-c
topology.kubernetes.io/zone は、クラウドのマネージドKubernetesならNodeに自動で付くラベルです。kindには付かないので、今日は手で付けました。
⚠️ Nodeが1台の環境では、この章の演習の一部ができません。 minikubeなら minikube start -p ops --nodes 4 で複数Nodeにできます(Node名は ops、ops-m02 のようになるので、コマンド中の名前を読み替えてください)。Docker DesktopのKubernetesは既定で1台です。その場合は、ドレインの演習は読むだけにして、RBACとHPAの演習を試してください。kindのクラスターを作ると、PCのメモリを数GB使います。今日の最後に kind delete cluster --name ops で消します。
まず、よくある思い込みから崩します。「replicasを3にしたから、1台壊れても大丈夫」。これは、3つのPodが別々のNodeに載っているときだけ正しい話です。
図5-2: replicasが同じでも、載る場所しだいで結果が変わる。
スケジューラーは、何も指定しなければ空いている資源を見てPodを置きます。結果として同じNodeに寄ることがあります。散らしたいなら、それを宣言します。使うのは topologySpreadConstraints です。
# api.yaml ― 第3章の api に、本番向けの設定を足したもの
apiVersion: apps/v1
kind: Deployment
metadata:
name: api
spec:
replicas: 3
selector:
matchLabels: {app: api}
template:
metadata:
labels: {app: api}
spec:
automountServiceAccountToken: false # APIサーバーを呼ばないアプリにはトークンを渡さない
terminationGracePeriodSeconds: 30
topologySpreadConstraints:
- maxSkew: 1 # ゾーン間のPod数の差は1まで
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule # 守れないなら置かない(Pendingになる)
labelSelector:
matchLabels: {app: api}
containers:
- name: nginx
image: nginx:1.27
ports:
- name: http
containerPort: 80
resources:
requests: {cpu: 100m, memory: 64Mi}
limits: {memory: 128Mi}
readinessProbe:
httpGet: {path: /, port: http}
periodSeconds: 5
lifecycle:
preStop:
exec:
command: ["sleep", "5"] # 転送先から外れるのを待ってから止まる
---
apiVersion: v1
kind: Service
metadata:
name: api
spec:
selector: {app: api}
ports:
- port: 80
targetPort: http
📝 適用すると裏で起きること
app: api の数を数え、差が1を超えない場所を選ぶpreStop の sleep 5 を実行し、その間にEndpointSliceから外れる。そのあとコンテナに停止シグナルが送られるkubectl -n lab apply -f api.yaml
kubectl -n lab rollout status deployment/api
kubectl -n lab get pod -l app=api -o wide # NODE 列が3台に分かれているか
# 出力の例(Pod名とIPは環境によって違う)
NAME READY STATUS RESTARTS AGE IP NODE
api-6c8d7b9f5d-2kq8x 1/1 Running 0 20s 10.244.1.2 ops-worker
api-6c8d7b9f5d-h7wzp 1/1 Running 0 20s 10.244.3.2 ops-worker3
api-6c8d7b9f5d-tn4c9 1/1 Running 0 20s 10.244.2.2 ops-worker2
Nodeの更新やドレインのように、人や仕組みが計画して起こす中断を「自発的な中断」と呼びます。PodDisruptionBudget(PDB)は、そのとき同時に失ってよいPodの数を制限します。
# pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: api
spec:
maxUnavailable: 1 # 同時に追い出してよいのは1つまで
selector:
matchLabels: {app: api}
kubectl -n lab apply -f pdb.yaml
kubectl -n lab get pdb api
NAME MIN AVAILABLE MAX UNAVAILABLE ALLOWED DISRUPTIONS AGE
api N/A 1 1 5s
見るのは ALLOWED DISRUPTIONS です。いま何個まで追い出してよいかを表します。ここが 0 のままだと、ドレインはそのPodを追い出せずに止まります(演習1で起こします)。PDBが守るのは自発的な中断だけで、Nodeの電源断のような突然の障害は防げません。それは散らす設定と余力で受けます。
PDBは、厳しすぎても緩すぎても困ります。3つしかないのに minAvailable: 3 にするとメンテナンスが進まず、何も書かなければ更新で全部同時に止まることがあります。可用性の目標と、メンテナンスにかけられる時間を一緒に決めてください。
Podが消されるとき、コンテナには停止シグナル(SIGTERM。イメージによってはSIGQUIT)が送られ、terminationGracePeriodSeconds を過ぎると強制終了されます。アプリは、シグナルを受けたら新しい接続の受付を止め、処理中のリクエストを終えてから終了するのが理想です。これをGraceful Shutdownと呼びます。
もう1つの落とし穴は、Podが止まり始めてから、ServiceのEndpointSliceから外れるまでの短い時間差です。その間に届いたリクエストは、止まりかけのPodへ行きます。上のYAMLの preStop の sleep 5 は、この時間差を待つための定番の手です。猶予時間を長くするだけでは、切り替えの順序の問題は解消しません。負荷をかけながらPodを消し、エラーが出ないかを実際に確かめてください。
RBACは、誰が(Subject)・どのリソースに(Resource)・何をできるか(Verb)を決める仕組みです。第2章で、kubectlのリクエストはAPIサーバーで認証と認可を通ると説明しました。その「認可」の中身がRBACです。
| リソース | 役割 | 範囲 |
|---|---|---|
| Role | できることの一覧(例: Deploymentの get と patch) | 1つのNamespace |
| ClusterRole | 同じく一覧。Nodeのようなクラスター全体のリソースも書ける | クラスター全体 |
| RoleBinding | RoleやClusterRoleを、人やServiceAccountに結び付ける | 1つのNamespace |
| ClusterRoleBinding | ClusterRoleを、クラスター全体で結び付ける | クラスター全体 |
基本は、Namespace単位のRoleとRoleBindingです。cluster-admin を開発者やCIに常に持たせるのは避けます。CIから更新するためのServiceAccountを作り、Deploymentの更新だけを許可してみます。
# rbac-ci.yaml ― CI用の身元と、許可する操作
apiVersion: v1
kind: ServiceAccount
metadata:
name: ci
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: ci-deployer
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: ci-deployer
subjects:
- kind: ServiceAccount
name: ci
namespace: lab
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: ci-deployer
kubectl -n lab apply -f rbac-ci.yaml
# --as で「ci として」聞いてみる(いまの自分が管理者なのでできる)
SA=system:serviceaccount:lab:ci
kubectl -n lab auth can-i patch deployments --as=$SA # yes
kubectl -n lab auth can-i delete pods --as=$SA # no
kubectl -n lab auth can-i get secrets --as=$SA # no
kubectl -n lab auth can-i --list --as=$SA # 許可されている操作の一覧
権限を足したら、足りているかだけでなく、余計なものが入っていないかを can-i で確かめる習慣を付けます。特に注意したいのは、Podを作れる権限です。Podを作れる人は、そのNamespaceのSecretをマウントしたPodや、設定によってはNodeのファイルを覗くPodを作れます。「読むだけの権限」より、ずっと重く扱ってください。
ServiceAccountは、ワークロードの身元です。上の api では automountServiceAccountToken: false にして、APIサーバーを呼ばないアプリにトークンを渡していません。CIの認証情報を、期限のない固定のSecretで持ち続ける方法も避けます。クラウドのIAMと連携する仕組み(EKSのPod Identity、GKEのWorkload Identityなど)を使えば、短い期限の認証情報で済みます。Roleの変更は監査ログに残し、誰がどの権限を持っているかを定期的に棚卸しします。
本番で nginx:latest のようなタグを使うと、同じManifestでも、取得した日によって中身が変わります。障害が起きて「昨日のバージョンに戻したい」と思ったとき、昨日の中身を再現できません。タグに加えて、中身のハッシュであるイメージダイジェストで固定します。
# いま動いている api のイメージが、実際にはどのダイジェストか
kubectl -n lab get pod -l app=api \
-o jsonpath='{.items[0].status.containerStatuses[0].imageID}{"\n"}'
# 出力の例(ダイジェストは取得した時期によって違う)
docker.io/library/nginx@sha256:(64桁の16進数)
# Manifest では、タグとダイジェストを並べて書ける(読む人のためにタグを残す)
image: nginx:1.27@sha256:<上で確かめたダイジェスト>
ダイジェストの更新は、CIが明示的に行います。そのほか、信頼できるレジストリ、脆弱性スキャン、署名、SBOM(イメージに何が入っているかの一覧)を組み合わせて、「どこから来た、何が入ったイメージか」を説明できるようにします。
Kubernetesには、Namespaceにラベルを付けるだけで、危ないPodの設定を警告・拒否する仕組み(Pod Security Admission)があります。いきなり拒否にすると既存のアプリが止まるので、まず warn(警告だけ)で、いまのPodが何を満たしていないかを見ます。
kubectl label namespace lab \
pod-security.kubernetes.io/warn=restricted \
pod-security.kubernetes.io/enforce=baseline
kubectl -n lab rollout restart deployment/api # Podを作り直して警告を出す
# 出力の例(改行を足している)
Warning: would violate PodSecurity "restricted:latest":
allowPrivilegeEscalation != false (container "nginx" must set securityContext.allowPrivilegeEscalation=false),
unrestricted capabilities (container "nginx" must set securityContext.capabilities.drop=["ALL"]),
runAsNonRoot != true (pod or container "nginx" must set securityContext.runAsNonRoot=true),
seccompProfile (pod or container "nginx" must set securityContext.seccompProfile.type to "RuntimeDefault" or "Localhost")
enforce=baseline は、特権コンテナやNodeのディレクトリのマウントのような、明らかに危ない設定だけを拒否する水準です。restricted はもっと厳しく、警告の4項目がそれにあたります。公式の nginx イメージはrootで起動する前提なので、runAsNonRoot を満たすには、非rootで動くよう作られたイメージ(例: nginxinc/nginx-unprivileged。待ち受けは8080番)に替える必要があります。設定だけで直らない項目があるので、新しいアプリは最初から非rootで作るほうが楽です。
非root化のほかに、読み取り専用のルートファイルシステム、不要なLinux capabilityの削除、seccompも検討します。どうしても例外が必要なPodは、期限なしで許可せず、所有者と期限を記録してください。
「遅い」「エラーが出ている」と言われたとき、手元に何があれば原因に近づけるか。答えは、3種類の信号です。役割が違うので、どれか1つでは足りません。
図5-3: メトリクスで気づき、トレースで場所を絞り、ログで理由を読む。
メトリクスは、クラスターのCPU使用率だけでは足りません。リクエスト数、レイテンシ、エラー率、キューの長さ、DBの接続数のように、利用者への影響に近い指標を集めます。ログは、Podの標準出力をまとめて集め、時刻、リクエストID、サービス名、重大度を決まった形(構造化ログ)で出します。Secret、Authorizationヘッダー、個人情報はログに出さない設計にします。
トレースは、入口のHTTPリクエストが、API、キュー、DB、外部APIのどこで遅くなったかを追います。全部のリクエストを記録すると、費用と保管量が増えるので、どの割合で記録するか(サンプリング率)を決めます。3つの信号を同じサービス名・環境名で結び付けておき、アラートからダッシュボード、そこからログへ、迷わずたどれるようにします。
学習環境でも、Kubernetes自身が出す情報は見られます。
kubectl -n lab logs deploy/api --tail=20 # 3つのうち1つのPodのログ
kubectl -n lab logs -l app=api --prefix --tail=5 # ラベルで全Podのログを、Pod名付きで
kubectl get events -A --sort-by=.lastTimestamp | tail -20
ただし、kubectl logs で見えるのは、いまあるPodのログだけです。Podが消えたり、Nodeごと落ちたりすると読めなくなります。本番では、ログをクラスターの外へ送って残す仕組みが必須です。
アラートを増やしすぎると、当番は通知を見なくなります。「CPUが高い」だけで夜中に起こすのはやめて、利用者への影響と、放っておくと確実に壊れるものに絞ります。
| アラート | なぜ見るか | 緊急度の目安 |
|---|---|---|
| SLO違反・エラー予算の急な消費 | 利用者が実際に困っている | すぐ対応 |
| Ready Podの不足 | あと1つ落ちたら止まる | すぐ対応 |
| Pendingの増加 | スケールしたいのに置き場所がない | すぐ〜営業時間 |
| PVCの容量逼迫 | 満杯になるとアプリが書けなくなる | 残りの日数で決める |
| 証明書の期限 | 切れた瞬間に全部の通信が止まる | 30日前・7日前に通知 |
| バックアップの失敗 | 壊れたときに戻せない | 翌営業日 |
どのアラートにも、最初に見るダッシュボードと、対応手順(後で説明するRunbook)へのリンクを付けておきます。「通知は来たが、何をすればいいか分からない」をなくすためです。
HorizontalPodAutoscaler(HPA)は、CPUやメモリ、独自の指標を見てPodの数を増減させます。HPAがCPUの値を知るには、metrics-serverが要ります。kindには入っていないので、入れます。
# バージョンを固定して入れる(latest を使わない理由は、イメージの節と同じ)
kubectl apply -f https://github.com/kubernetes-sigs/metrics-server/releases/download/v0.7.2/components.yaml
# kind の kubelet は自己署名の証明書なので、学習環境に限って検証を外す
kubectl -n kube-system patch deployment metrics-server --type=json \
-p='[{"op":"add","path":"/spec/template/spec/containers/0/args/-","value":"--kubelet-insecure-tls"}]'
kubectl -n kube-system rollout status deployment/metrics-server
kubectl top node # 数値が出れば準備OK(出るまで1分ほどかかる)
# hpa.yaml ― CPU使用率の平均が requests の60%を超えたら増やす
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: api
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: api
minReplicas: 3
maxReplicas: 6
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 60
📝 適用すると裏で起きること
api のPodのCPU使用量をmetrics-serverから読むreplicas を書き換える。増えたPodは、さっきの散らす設定に従って置かれるkubectl -n lab apply -f hpa.yaml
kubectl -n lab get hpa api
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
api Deployment/api cpu: 0%/60% 3 6 3 30s
負荷をかけて、増えるところを見ます。別のターミナルで kubectl -n lab get hpa api -w を開いておくと、変化が分かりやすいです。
# 負荷をかけるPod(止めるまでアクセスし続ける)
kubectl -n lab run load --image=busybox:1.36 --restart=Never -- \
/bin/sh -c "while true; do wget -q -O /dev/null http://api; done"
# 数分見たら止める。Podは、5分ほどの様子見のあとで減る
kubectl -n lab delete pod load
nginxは軽いので、PCの性能によっては60%に届かず、増えないことがあります。そのときは averageUtilization を小さくするか、負荷のPodを2つにしてください。増えたPodが Pending になった場合は、Nodeの空きが足りていません。それが次の話です。
HPAが増やすのはPodだけです。Podを置くNodeの空きがなくなれば、増えたPodは Pending で止まります。Node自体を増減させるのは、Cluster AutoscalerやKarpenter、クラウドのNodeの自動スケールの役割です。kindにはこの仕組みがないので、今日は試せません。
Nodeの自動スケールが判断に使うのは、実際の使用量ではなく、Podのrequestsです。requestsが実態より大きいとNodeが無駄に増え、小さいと詰め込みすぎて性能が落ちます。HPAもrequestsを基準に使用率を計算するので、requestsのずれはスケールの判断を両方とも歪めます(演習3で、requestsがないとHPAが動かないところを見ます)。起動の遅いアプリでは、増やしてから効くまで時間がかかるので、キューの長さやレイテンシで早めに増やすことも考えます。逆に、夜間に縮めたいのに、DaemonSetやPDB、Nodeのローカルに置いたデータが邪魔して、Nodeが減らないこともあります。
見落としやすいのは、ロードバランサー、ディスク、スナップショット、ログの保管、外部IP、データ転送、監視サービスの費用です。第4章の片付けで「消し忘れたディスクに料金がかかり続ける」と書いたのも、この一部です。Namespaceやチームごとに、費用を割り振るためのラベルを決めておき、requestsが大きすぎるワークロードを定期的に見直します。反対に、安いNodeに詰め込みすぎて、障害のときに受け止める余力がなくなるのも避けてください。
Kubernetesは、年に3回ほど新しいマイナーバージョンが出て、古いものはサポートが終わります。つまり、アップグレードは「いつかやる」ではなく、毎年何度も回ってくる定期作業です。毎回を緊急作業にしないために、手順を決めて練習します。
図5-4: アップグレードは段階に分け、段階ごとに確かめる。黄色が本番で触る段階。
Kubernetes本体、Node、CNI、CSI、Ingress Controller、監視、Admission Webhookは、それぞれ別々に対応バージョンを持っています。本体だけ上げたら、ネットワークプラグインが新しい版に対応していなかった、ということが起こります。上げる前に、使っているアドオンの対応表と、クラウドの提供版のサポート表を確認します。Nodeのkubeletは、Control Planeより新しくできません。だからControl Planeが先です。
もう1つの確認は、非推奨・削除予定のAPIを使っていないかです。APIサーバーは、非推奨のAPIが呼ばれた回数を自分のメトリクスに出しています。
kubectl version
# 非推奨のAPIを呼んだ記録があるか(何も出なければ、起動してからは呼ばれていない)
kubectl get --raw /metrics | grep apiserver_requested_deprecated_apis
このメトリクスは、APIサーバーが起動してから実際に呼ばれたものしか出ません。月に1回しか動かないJobやCIのManifestは、別途ファイルを検索して確かめます。Manifestの apiVersion を書き換えれば済むとは限らず、項目の名前や既定値が変わっていることもあります。
本番を上げたあとは、次のように層ごとに確認します。
kubectl get nodes -o wide # 全Nodeが Ready で、VERSION がそろったか
kubectl get pods -A | grep -v -E 'Running|Completed' # 動いていないPodが残っていないか
kubectl get events -A --sort-by=.lastTimestamp | tail -30
kubectl -n lab rollout status deployment/api
アプリの変更も同じ考え方です。更新する前に kubectl diff -f api.yaml で差分を残し、何分間エラー率とレイテンシを見るか、どうなったら戻すかを先に書きます。ロールバックで戻るのはDeploymentだけで、DBのスキーマ変更や送ってしまったメールは戻りません。アプリのリリースとデータの変更は、同じ計画の中で考えます。
⚠️ マネージドサービスでは、手順も表示も提供元の形式になります。 EKS・GKE・AKSでは、Control Planeの更新はコンソールやCLIで依頼し、Nodeの更新はNodeグループ単位で行うのが一般的です。kubectl version のサーバー側の表示に、提供元独自の文字列が付くこともあります。出力だけでサポート期間を判断せず、提供元の更新ポリシーとサポート表を確認してください。延長サポートに追加料金がかかるサービスもあります。
障害の最中は、誰でも焦ります。焦った状態で「とりあえずPodを消す」と、原因の手がかりも一緒に消えます。順番を決めておくのは、焦っても同じ動きができるようにするためです。
図5-5: 障害対応の流れ。④の前に、Podの状態・ログ・Eventsを保存しておく。
最初に記録するのは、影響範囲、開始時刻、直前の変更、いまの症状です。障害の原因でいちばん多いのは直前の変更なので、デプロイや設定変更の履歴をすぐ見られるようにしておきます。変更が原因と分かれば、証拠を残したうえで、ロールバックや機能の縮退を判断します。
層ごとに見るときは、この特集のこれまでの章が、そのまま手引きになります。
| 症状 | まず疑う場所 | 詳しくは |
|---|---|---|
Pending | Nodeの空き(requests)、PVC、散らす設定・taint | 第2章・第4章・この章の演習 |
ImagePullBackOff | イメージ名・タグ、レジストリの認証、Nodeからの通信 | 第1章 |
CrashLoopBackOff | アプリの起動、設定値・Secret、Probeの設定 | 第3章 |
| 503・繋がらない | EndpointSlice、readiness、ポート、NetworkPolicy、Ingress | 第4章 |
Forbidden | RBAC、ServiceAccount、Admissionの拒否 | この章の演習2 |
Nodeが NotReady | クラウドのインスタンス状態、ディスク、CNI、kubelet | 下の注意 |
Nodeの障害では、いきなりPodを消して証拠を失わないことです。先にクラウドのインスタンスの状態、ディスクの空き、CNI、kubeletのログを確かめます。データを伴う障害では、書き込みを止める、二重に処理しない、どの時点まで戻すか、戻したあとに整合性を確かめる、を優先します。直ったと思ったら、主要な利用の流れ、裏で動く処理、メトリクス、ログ、アラートを確認し、時系列と再発防止策を残します。
障害のあとの振り返り(ポストモーテム)では、個人を責めません。検知、判断、復旧、連絡のどこに仕組みの改善余地があったかを確認します。「Aさんがコマンドを間違えた」で終わると、次はBさんが同じ間違いをします。
EKS、GKE、AKSのようなマネージドサービスは、Control Planeの可用性や更新など、運用の一部を提供元が引き受けます。ただし、全部ではありません。
| 項目 | 提供元が担うことが多い | 自分たちに残る |
|---|---|---|
| Control Plane | APIサーバーとetcdの可用性、パッチ | 更新のタイミングの判断、アドオンとの互換性の確認 |
| Node | イメージの提供、自動更新の仕組み(サービスによる) | Nodeの種類と台数、更新の実施、余力の確保 |
| ネットワーク | CNIの既定の実装、ロードバランサーとの連携 | NetworkPolicy、Ingress・Gateway、IPアドレスの設計 |
| ストレージ | CSIドライバー、ディスクの提供 | StorageClassの選択、バックアップと復元テスト |
| 権限 | クラウドのIAMとの連携の仕組み | RBAC、ServiceAccountとIAMの対応付け、棚卸し |
| 観測・費用 | 基本のログ・メトリクスの出力先 | 何を集めるか、アラート、費用の配賦と見直し |
選ぶときは、既存のクラウドとの統合、チームの経験、必要な拡張性、Node管理の手間、サポート期限、マルチクラウドの要件、障害のときの連絡経路を比べます。Kubernetes APIと互換だから同じ、とは考えず、使うアドオンと運用体制を含めた総コストで判断してください。
そして、特集トップで書いたことに戻ります。単一のWebサービスや少数の定期ジョブなら、AWS ECSやCloud Runのような、もっと抽象度の高いマネージドコンテナやPaaSのほうが合理的なことがよくあります。この章の表を見て「自分たちに残る」列を担える人がいないなら、それは「まだKubernetesでなくていい」という判断材料です。
今日作った api・PDB・ci・HPAを使って、3つ壊します。どれも、本番で「設定は入れたのに、肝心なときに効かなかった」となる典型です。HPAで api が増えたままなら、3つに戻るまで数分待ってから始めてください(kubectl -n lab get deploy api)。
「絶対に1つも減らしたくない」と考えて、3つのPodに minAvailable: 3 を設定した状態を作ります。そのうえで、Nodeのメンテナンスのためにドレインします。
kubectl -n lab patch pdb api --type=merge \
-p '{"spec":{"maxUnavailable":null,"minAvailable":3}}'
kubectl -n lab get pdb api # ALLOWED DISRUPTIONS が 0
kubectl drain ops-worker --ignore-daemonsets --delete-emptydir-data --timeout=60s
# 出力の例(Pod名は環境によって違う。60秒間、同じ行がくり返される)
node/ops-worker cordoned
evicting pod lab/api-6c8d7b9f5d-2kq8x
error when evicting pods/"api-6c8d7b9f5d-2kq8x" -n "lab" (will retry after 5s): Cannot evict pod as it would violate the pod's disruption budget.
...
error: unable to drain node "ops-worker" due to error: [error when evicting pods/"api-6c8d7b9f5d-2kq8x" -n "lab": global timeout reached: 1m0s], continuing command...
ドレインは、PDBを守るために追い出しを待ち続け、時間切れで止まりました。本番でタイムアウトを付けずに実行すると、メンテナンスの時間枠が終わるまで待ち続けます。もう1つ大事なのは、失敗してもNodeは「新しいPodを置かない」状態のまま残ることです。
kubectl get nodes # ops-worker が Ready,SchedulingDisabled のまま
直すときは、PDBを外すのではなく、可用性の目標に合った値に戻します。3つのうち1つずつなら止めてよい、が今日の目標でした。
kubectl -n lab apply -f pdb.yaml # maxUnavailable: 1 に戻す
kubectl uncordon ops-worker # 置かない状態を解除する
kubectl get nodes
PDBの値は、「単一のレプリカ」「Nodeのローカルに置いたデータ」「起動に何分もかかるアプリ」と組み合わさると、もっと詰まりやすくなります。ドレインが止まったときに、PDBだけを緩めて押し通すのではなく、なぜ追い出せないのかを先に確かめてください。
CI用の ci には、Deploymentの patch を許可しました。では、ci としてPodの数を4にしてみます。
SA=system:serviceaccount:lab:ci
kubectl -n lab scale deployment api --replicas=4 --as=$SA
# 出力の例
Error from server (Forbidden): deployments.apps "api" is forbidden: User "system:serviceaccount:lab:ci" cannot patch resource "deployments/scale" in API group "apps" in the namespace "lab"
エラーの文に答えが書いてあります。kubectl scale が触るのは deployments ではなく、deployments/scale というサブリソースです。RBACでは、サブリソースは別のリソースとして扱われます。Podのログ(pods/log)や、Podの中でコマンドを実行する pods/exec も同じです。
kubectl -n lab auth can-i patch deployments --as=$SA # yes
kubectl -n lab auth can-i patch deployments --subresource=scale --as=$SA # no
直すには、Roleにサブリソースの行を足します。rbac-ci.yaml の rules を次のようにして、もう一度 apply します。
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
verbs: ["get", "list", "watch", "patch"]
- apiGroups: ["apps"]
resources: ["deployments/scale"]
verbs: ["get", "patch", "update"]
kubectl -n lab apply -f rbac-ci.yaml
kubectl -n lab auth can-i patch deployments --subresource=scale --as=$SA # yes
kubectl -n lab scale deployment api --replicas=4 --as=$SA
kubectl -n lab get hpa api -w # 数分後、HPAが3に戻す
スケールは通りましたが、しばらくするとHPAが3に戻します。HPAが管理しているDeploymentでは、手で変えた数は長続きしません。本番でPodを増やしたいときは、HPAの minReplicas を変えます。権限の設計では、「その操作を、そもそもCIにさせるべきか」も一緒に考えてください。
「requestsはよく分からないので書かない」状態を作ります。第3章で、requestsはスケジューラーが使う予約の量だと説明しました。HPAにとっても、計算の基準です。
kubectl -n lab patch deployment api --type=json \
-p='[{"op":"remove","path":"/spec/template/spec/containers/0/resources/requests"}]'
kubectl -n lab rollout status deployment/api
kubectl -n lab get hpa api
# 出力の例
NAME REFERENCE TARGETS MINPODS MAXPODS REPLICAS AGE
api Deployment/api cpu: <unknown>/60% 3 6 3 25m
Podは問題なく動いていて、アクセスも通ります。気づきにくいのはそこです。HPAは静かに、判断をやめています。理由はEventsにあります。
kubectl -n lab describe hpa api | tail -5
# 出力の例(文言はバージョンによって違う)
Warning FailedGetResourceMetric ... failed to get cpu utilization: missing request for cpu in container "nginx" of Pod "api-..."
使用率は「使用量 ÷ requests」なので、requestsがなければ計算できません。この状態で負荷が来ても、Podは増えません。HPAを入れたら、TARGETS に数字が出ていることまでを確認項目にしてください。
kubectl -n lab apply -f api.yaml # requests を戻す
kubectl -n lab get hpa api # 1分ほどで cpu: 0%/60% のように数字に戻る
ここで1つ注意があります。api.yaml には replicas: 3 が書いてあるので、applyするたびに、HPAが決めた数を3に上書きします。本番でHPAを使うDeploymentは、Manifestから replicas を消し、数はHPAに任せます。
第1章から使っている6項目です。今日は「再発防止」に、仕組みで防ぐ方法(設定、確認項目、権限)を書いてください。
# 演習メモの例
日時/Context : 2026-10-24 21:30 / kind-ops(Namespace: lab)
症状 : ops-worker のドレインが60秒で時間切れ。Node は SchedulingDisabled のまま
最初に見たもの: drain の出力(violate the pod's disruption budget)→ get pdb で ALLOWED DISRUPTIONS 0
原因 : replicas 3 に対して PDB が minAvailable: 3。1つも追い出せない
修正 : PDB を maxUnavailable: 1 に戻し、uncordon した
再発防止 : PDB の値をレビュー項目にする。ALLOWED DISRUPTIONS が 0 のPDBをアラートの対象にする
チェックリストを読むだけでは、障害のときに使える運用かどうかは分かりません。わざと壊して確かめる練習を、ゲームデイと呼びます。演習1を、正しいPDBでもう一度やってみます。今度はドレインが通るはずです。そのあとに何が起きるかを見てください。
kubectl -n lab get pdb api # ALLOWED DISRUPTIONS が 1
kubectl -n lab get pods -o wide
kubectl drain ops-worker --ignore-daemonsets --delete-emptydir-data --timeout=5m
kubectl -n lab get pods -o wide # 追い出された api の代わりはどこへ?
ドレインは成功します。ところが、追い出された api の代わりのPodが、Pending のまま残ることがあります。describe で理由を見ます。
kubectl -n lab get pods -l app=api --field-selector=status.phase=Pending
kubectl -n lab describe pod <Pendingのapi のPod名>lt;Pendingになったapi Pod名<Pendingのapi のPod名>gt; | tail -5
# 出力の例(改行を足している)
0/4 nodes are available: 1 node(s) had untolerated taint {node-role.kubernetes.io/control-plane: },
1 node(s) were unschedulable, 2 node(s) didn't match pod topology spread constraints.
zone-aにはNodeが1台しかなく、そのNodeをドレインしました。残りのzone-bとzone-cに置くと、ゾーン間の差が2になり、maxSkew: 1 と DoNotSchedule を守れません。スケジューラーは指示どおり、置かずに待っています。設定は正しく効いていて、そのせいでメンテナンス中は2つで動くことになりました。
これがゲームデイで見つけたい「残った課題」です。対策は、1つのゾーンに複数のNodeを置く、メンテナンス中は2つで受けきれるだけの余力を持たせる、散らす設定を ScheduleAnyway(できるだけ守る)にする、などがあり、どれを選ぶかは可用性の目標で決まります。
kubectl uncordon ops-worker
kubectl -n lab get pods -o wide # Pending だった api が ops-worker に置かれる
Node名やドレインのオプションは、環境に合わせます。--delete-emptydir-data は、emptyDirの一時データを消すことに同意する指定です(今日はmetrics-serverが使っています)。本番では、意味を確かめずに付けないでください。ゲームデイには、止める条件と連絡先を先に決め、結果と残った手作業を記録します。成功の条件は「コマンドが終わった」ではなく、SLOを守れたか、復旧までの時間を測れたかです。
ここまでの内容を、本番に出す前の確認項目にまとめました。全部に印が付くまで出してはいけない、という意味ではありません。印が付かない項目は、そのリスクを誰が受け入れたのかを記録してください。
Pending、CrashLoopBackOff、503、Node障害の訓練をした当番が経験でコマンドを選ぶだけでは、夜間の障害の復旧の質が安定しません。サービスごとに、次のような手順書(Runbook)を用意します。
# Runbook の例(サービス api)
依存しているもの : DB(本番RDS)、決済API(外部)、Namespace: app
SLO : 成功率 99.9%(30日)、p95 レイテンシ 300ms 以下
見るダッシュボード: api-overview(エラー率・p95・Ready Pod数)
ログの検索 : service="api" env="prod" level="error"
戻す条件 : デプロイ後10分以内に エラー率 1% 超 または p95 500ms 超
戻し方 : kubectl -n app rollout undo deployment/api(DBの変更を伴う版は不可。下の連絡先へ)
データの連絡先 : DB担当の当番(連絡方法を書く)
最終確認日 : 2026-10-24(検証クラスターで手順を実行)
Runbookは、平常時に読み合わせ、検証クラスターで実際にコマンドを実行して、権限不足や古いリソース名を直します。書いたきり一度も実行していない手順は、障害のときにだいたい動きません。
運用の指標は、稼働率だけでなく、変更の失敗率、平均復旧時間、バックアップからの復元にかかる時間、残っている脆弱性の数、当番に届いたアラートの数でも見直します。Nodeを増やす前に、requests、リトライの設定、ログの量、常に動かしておく必要のないワークロードを確かめます。新しい機能を足すときは、解決する問題と、やめるときの外し方も書いておきます。
| 頻度 | 見直す項目 |
|---|---|
| 毎月 | 使っていないNamespaceとロードバランサー、大きすぎるrequests、失敗したJob、期限の近い証明書、バックアップの容量、脆弱性が残ったイメージ |
| 四半期ごと | 復元の演習、権限の棚卸し、クラスターとアドオンのサポート期限の更新 |
| 規模が変わったとき | Namespaceの設計、GitOps、Policy as Code、費用の配賦、当番の体制。逆に、運用担当が1人しかいないなら、もっと高レベルなマネージド基盤へ移す判断も含める |
技術の採用そのものを目的にせず、サービスの信頼性と変更の速さを測って判断してください。
今日はクラスターごと作ったので、クラスターごと消します。
kind delete cluster --name ops
kind get clusters # ops が消えたことを確認
kubectl config get-contexts # 次に使うクラスターを確かめる
kubectl config use-context kind-kind # 第1章から使ってきたクラスターに戻す場合
kindのクラスターを消すと、中のPV、metrics-server、RBACの設定もすべて消えます。クラウドで同じ練習をした場合は、Nodeグループ、ロードバランサー、ディスク、ログの保管先が残っていないかを、コンソールでも確かめてください。
auth can-i で確かめる5つの章を通して、Kubernetesが「Podを起動する仕組み」から、状態を宣言し、通信・ストレージ・更新・観測・復旧を組み立てる運用の基盤へと、広がって見えてきたと思います。
| 章 | 手に入れたもの |
|---|---|
| 🧱 第1章 基礎 | 「あるべき状態を宣言すると、近づけてくれる」という考え方。Context確認と演習メモの習慣 |
| ⚙️ 第2章 アーキテクチャ | APIサーバー・etcd・スケジューラー・kubeletが、YAMLをどうPodに変えるか |
| 📦 第3章 ワークロード | Deployment・Probe・requests/limits・ConfigMap/Secret。安全に更新して戻す |
| 🌐 第4章 ネットワークとストレージ | 届く経路を層に分けて調べる目と、Podが消えても残るデータ |
| 🏭 第5章 本番運用(このページ) | 壊れても判断できる仕組み。可用性・権限・観測・更新・障害対応 |
大切なのは、リソースの種類を増やすことではありません。変更を再現でき、失敗に気づけて、データを戻せることです。Kubernetesを採用するなら、最初から本番に近い課題を、今日のように小さく練習してください。採用しないと決めた場合も、コンテナ、サービスディスカバリ、ヘルスチェック、資源の制御、監視という設計の考え方は、ECSでもCloud Runでもそのまま使えます。
🎉 Kubernetes特集は、この第5章で完結です。 6日間、毎日読んでくださった方、本当にありがとうございました。途中で手が止まった章があっても大丈夫です。全ページは特集トップからいつでも読めます。演習メモが手元に残っていれば、それがあなた専用のトラブルシューティング集になります。
全6ページがそろいました。障害対応で「あの切り分けの表はどこだっけ」となったとき、特集トップを1つ保存しておけば、どの章にもすぐ戻れます。チームの新しいメンバーに渡すときも、特集トップのURLを1つ送るだけで済みます。
新しい特集や記事の公開は、X(@BusinessHub413)でお知らせします。
※本章の記述は2026年10月時点の公式ドキュメントに基づきます。エラーやイベントの文言、kubectl の出力形式、スケジューラーの判断の細部は、Kubernetesのバージョンや学習環境(kind・minikube・Docker Desktop)によって異なることがあります。マネージドサービスの責任分界とサポート期限は、提供元の最新の資料で確認してください。本章のコマンドは手順として整理したもので、すべての環境での動作を確認したものではありません。
「本番に出す前に、何が足りないかを見てほしい」「ECSやCloud Runで足りるのか」から、
クラスター設計・運用体制づくりまで、30分の無料相談で承ります。