Kubernetes の原理¶
ひとことで
Kubernetes(K8s)の核心は 3 つだけです。 ① 期待する状態を書けば、コントローラがずっとそれに近づけ続ける(reconcile)、② オブジェクト同士は Label で結び付く、③ K8s は抽象化レイヤーで、裏で実際に働くのはクラウドのリソース(LB、ブロック・ボリューム、IAM)。 混乱したときは、この 3 つに立ち返ってください。
このページは Phase 11 の講義(第 1〜8 課)と Phase 6 の実験をまとめたものです。OCI 上での構築方法と実測結果は OKE の設計と実践 を参照してください。
1. まずはオブジェクトを知る¶
| K8s オブジェクト | ひとことで | たとえ(OCI / これまでの Phase) |
|---|---|---|
| Pod | 最小単位:1 つ以上のコンテナで、IP を 1 つ共有する | — |
| Deployment / ReplicaSet | 「同じ Pod を N 個」維持し、バージョンの切り替えを担当 | Phase 4 の「VM ×2 + LB のヘルスチェック」。ただし自動で補充される |
| Service | 変わり続ける Pod に固定の IP と名前を与える | LB のバックエンド・セット(Backend Set) |
| Ingress | HTTP の振り分けルール(ドメイン、パス → どの Service) | LB の L7 ルーティング・ルール |
| ConfigMap / Secret | イメージの外に置く設定 / 機密情報 | ファンクションの config / ボールト(Vault)のシークレット |
| PersistentVolumeClaim(PVC) | 「50GB のディスクがほしい」という申請 | CSI がブロック・ボリューム(Block Volume)を作成 |
| Namespace | クラスタ内の区画 | コンパートメント(Compartment) |
| Role / RoleBinding(RBAC) | 誰がどの Namespace で何をできるか | IAM ポリシー(Policy) |
| ServiceAccount | Pod 自身の ID | リソース・プリンシパル / インスタンス・プリンシパル |
| Job / CronJob | 実行し終えたら終わるタスク / 定期実行 | ファンクション / cron |
2. 原理¶
2.1 コンテナ:「覆い」をかけられたプロセス¶
コンテナは小さな VM ではなく、ホスト上の 1 つのプロセスにすぎません。覆いをかけられていて、外が見えないだけです。
| VM | コンテナ | |
|---|---|---|
| 何を分離するか | マシン全体(それぞれがカーネルを持つ) | プロセス(すべてのコンテナがホストのカーネルを共有) |
| 起動時間 | 数十秒〜数分 | 数十ミリ秒〜数秒 |
| サイズ | 数 GB | 数 MB〜数百 MB |
| 分離の強さ | 強い | VM より弱い(カーネルに脆弱性があるとすべてのコンテナに影響) |
覆いは Linux の 3 つの機能で実現しています。namespace は「何が見えるか」(自分専用の PID、IP、ファイルシステム)、cgroups は「最大どれだけ使えるか」(メモリ上限を超える → OOMKilled)を決め、レイヤー型ファイルシステムによってイメージの読み取り専用の層は共有され、コンテナは一番上に書き込み可能な層を 1 つ追加するだけです。
イメージに関する 3 つのポイント:
- tag は変わるが、digest は変わらない:
nginx:1.27の tag は別のイメージに付け替えられることがあります。絶対に固定したいならnginx@sha256:...を使います - CPU アーキテクチャ:OKE のノードは A1(arm64)です。公式イメージの多くは「マルチアーキテクチャ」で、pull 時に自動で選ばれます。amd64 版しかないイメージは A1 では動きません
- データと設定は外に置く:コンテナを削除すると書き込み可能な層も消えます。同じイメージを開発 / 本番で異なる設定で使います
この背景にある原則が cattle, not pets(サーバーはペットではなく家畜として扱う)です。壊れたら入れ替え、修理はしません。実行中のコンテナの中を変更せず、変更したいときは新しいイメージを build して入れ替えます。
2.2 宣言型 + 継続的な修正(reconcile)¶
K8s に「どういう状態にしたいか」を伝えると、K8s はずっと見張り続け、実際の状態が違うと気付いたら自動で修正します。
| Terraform | K8s | |
|---|---|---|
| 書き方 | 宣言型(.tf) |
宣言型(YAML manifest) |
| drift をいつ修正するか | apply を実行したとき(1 回だけ) |
常に修正し続ける。人が起動する必要はない |
| 期待する状態の保存先 | state ファイル | etcd |
すべてのオブジェクトは同じ構造です。spec はユーザーが書いた期待、status は K8s が書く実際の状態で、コントローラが status を spec に追い付かせます。
2.3 アーキテクチャ¶
flowchart TB
U["kubectl"] -->|"HTTPS :6443"| API
subgraph CP["Control Plane(OKE では Oracle が管理)"]
API["kube-apiserver<br/>唯一の入口"]
ETCD[("etcd<br/>全状態")]
CTL["scheduler<br/>controller-manager<br/>cloud-controller-manager"]
CTL <-->|"変化を監視、結果を書き戻す"| API
API --- ETCD
end
subgraph N1["Worker Node(各台に存在)"]
K1["kubelet"] --> R1["コンテナ・ランタイム<br/>CRI-O"]
R1 --> P1["Pod"] & P2["Pod"]
KP1["kube-proxy<br/>flannel"]
end
API <-->|"自分に割り当てられた Pod は?"| K1
| コンポーネント | 場所 | 役割 |
|---|---|---|
| kube-apiserver | Control Plane | すべての操作の唯一の入口:認証 → 権限チェック → etcd に保存。コンポーネント同士は直接やり取りせず、すべてこれを経由する |
| etcd | Control Plane | クラスタの全状態を保存するキー・バリュー・データベース |
| scheduler | Control Plane | 新しい Pod をどの node に置くか。決めるだけで、起動は担当しない |
| controller-manager | Control Plane | Deployment、ReplicaSet、Node、Job など数十のコントローラが、それぞれ reconcile loop を回す |
| cloud-controller-manager(CCM) | Control Plane | クラウドとのやり取り:Service type=LoadBalancer → OCI の LB を作成 |
| kubelet | 各 node | apiserver に「自分に割り当てられた Pod」を問い合わせ、ランタイムにコンテナを起動させ、状態を報告する |
| kube-proxy | 各 node | 「Service へのアクセス → 実際の Pod」への転送ルールを node に書き込む |
| CNI プラグイン | 各 node | Pod に IP を割り当て、node 間の Pod ネットワークをつなぐ(このリポジトリ:flannel) |
2.4 コンポーネントの連鎖:kubectl apply のあとに何が起きるか¶
「レプリカ 2 つの nginx Deployment」を例にします。
| ステップ | 誰が | 何をしたか |
|---|---|---|
| ① | kubectl → apiserver | 期待する状態を etcd に書き込む(認証は OCI IAM のトークン → RBAC チェック)。kubectl はここで戻る |
| ② | Deployment コントローラ | ReplicaSet を 1 つ作成 |
| ③ | ReplicaSet コントローラ | 0 個で 2 個必要 → Pod オブジェクトを 2 つ作成(この時点ではまだ node がない) |
| ④ | scheduler | node のない Pod に node を選び、node 名を書き込む |
| ⑤ | その node の kubelet | 自分に割り当てられた Pod に気付く → CRI-O にイメージを pull させ、コンテナを起動。CNI に IP を割り当てさせる |
| ⑥ | kubelet | 「Running、IP は 10.244.x.x」と報告 |
| ⑦ | 他のコントローラ / kube-proxy | Pod が準備完了 → Service の転送先を更新 |
最も間違えやすい点:コンテナを起動するのは誰か
コントローラが作るのは Pod オブジェクト(etcd 内の 1 レコード)だけです。実際にコンテナ(プロセス)を起動するのはその node 上の kubelet です。 コントローラがオブジェクトを作る → scheduler が node を選ぶ → kubelet が実行する。3 者は互いを呼び出さず、どれも「apiserver を見張り、差に気付き、データを書き換える」だけです。
Phase 11 では、この連鎖を実際に目にしました。node がまだ参加していないとき、coredns の Pod オブジェクトはすでに存在していましたが(コントローラの仕事は完了)、状態は Pending、NODE は <none>、Events は no nodes available to schedule pods(scheduler で止まっている)で、起動する kubelet もいませんでした。node が参加すると自動で Running になりました。
コントロール・プレーンが止まる ≠ サービスが止まる:
| scheduler が 5 分間止まった | 結果 |
|---|---|
| すでに動いている Pod | 影響なし(scheduler が必要なのは「どこに置くか」の瞬間だけ) |
| 実行中のコンテナがクラッシュ | kubelet がその場で再起動 |
| 新しい Pod / 自己修復で補充する Pod | Pending のまま止まり、scheduler が復旧すると自動でスケジュールされる |
ユーザーのリクエストは LB → node → Pod(データ・プレーン)を通り、コントロール・プレーンは経由しません。
3. 主要オブジェクト:Pod、Label、Deployment、Service¶
Pod¶
- 1 つの Pod 内のコンテナは IP と Volume を共有します。ほとんどは 1 Pod 1 コンテナで、ログ転送などの補助コンテナを入れる場合は sidecar と呼びます
- Pod も使い捨てです:落ちたら新しいものに入れ替わり、名前も IP も変わります(Phase 6:
10.244.1.4 → .5)→ Pod の IP を直接使ったり、単独の Pod を直接作ったりしません
| 状態 | 意味 | よくある原因 |
|---|---|---|
Pending |
まだ node に割り当てられていない | リソース不足、node がない |
ImagePullBackOff |
イメージの pull に失敗 | 名前の誤り、認証が必要、対応するアーキテクチャがない |
CrashLoopBackOff |
起動してすぐクラッシュ | プログラムのエラー、設定ミス、exec format error(アーキテクチャの不一致) |
OOMKilled |
メモリ上限を超えて kill された | limits.memory が小さすぎる |
Label と Selector:K8s の「接着剤」¶
オブジェクト同士は名前ではなく、ラベルで結び付きます。Deployment は app=nginx の Pod を管理し、Service は app=nginx の Pod にトラフィックを転送します。
OCI の freeform_tags とは違います。OCI のタグは単なるメモですが、K8s のラベルは「誰が誰を管理するか、トラフィックを誰に送るか」を直接決めます。別の Pod に手動で app=nginx を付けると、ReplicaSet は 1 つ多いと判断して 1 つ削除し、Service もその Pod にトラフィックを送ります。
Deployment → ReplicaSet → Pod¶
# 06-oke/k8s/nginx.yaml(抜粋)
spec:
replicas: 2 # 期待:2 つを維持
selector:
matchLabels:
app: nginx # ① 「app=nginx の付いた Pod」を管理する
template:
metadata:
labels:
app: nginx # ② 新しい Pod にこのラベルを付ける(① と一致必須。違うと apply 時に拒否)
spec:
containers:
- name: nginx
image: nginx:1.27 # ここを変える → ローリング・アップデート
なぜ 2 層に分かれているのか。ReplicaSet は「数」を、Deployment は「バージョン」を管理します。image を変えると、Deployment は新しい ReplicaSet を作り、新しい Pod を少しずつ増やし、古い Pod を少しずつ減らします(ローリング・アップデート)。古い ReplicaSet はレプリカ数 0 で残るので、kubectl rollout undo ですぐに戻せます。
Pod 名 nginx-598968dd58-xxxxx = Deployment 名 - テンプレートの hash - ランダム文字列です。
Service:固定の入口¶
| type | どこからアクセスできるか | 実際にやっていること |
|---|---|---|
ClusterIP(デフォルト) |
クラスタ内部のみ | 仮想 IP を割り当てる(このリポジトリでは 10.96.0.0/16 から) |
NodePort |
外部から node IP:30000〜32767 |
ClusterIP + 各 node で同じポートを開く |
LoadBalancer |
外部からクラウドの LB 経由 | NodePort + CCM が OCI の LB を作成 |
ほとんどの Service は ClusterIP で(Web → API は http://api のような名前でアクセスし、CoreDNS が解決)、外部向けの入口は通常 1 つだけです。
Service にアクセスできるのに応答がないときは、まず kubectl get endpointslices -l kubernetes.io/service-name=nginx を見ます。転送先が空 = selector が Pod にマッチしていません。
その他のワークロード¶
| 種類 | 用途 | Deployment との違い |
|---|---|---|
| StatefulSet | ステートフル(DB、メッセージ・キュー) | Pod 名が固定(db-0)、Pod ごとに専用ストレージ、順番に起動・停止 |
| DaemonSet | 各 node で 1 つ動かす | レプリカ数を指定しない(kube-proxy、flannel、csi-oci-node) |
| Job / CronJob | 実行し終えたら終わる / 定期実行 | 成功後は再起動しない |
実際のプロジェクトでは、DB は一般に K8s に入れず、マネージド DB(ADB など)を使い、K8s ではステートレスなアプリだけを動かします。
4. ネットワーク¶
クラスタ内の 4 種類のアドレス(06-oke の構成)¶
| アドレス | 範囲 | 誰が割り当てるか | VCN 内に実在するか |
|---|---|---|---|
| node の IP | 10.6.10.0/24 |
OCI(VCN) | ✅ |
| Pod の IP | 10.244.0.0/16 |
flannel(CNI) | ❌(overlay で、VCN は知らない) |
| Service の IP(ClusterIP) | 10.96.0.0/16 |
K8s | ❌ どこにも存在しない。node 上の転送ルールにすぎない |
| LB のパブリック IP | — | OCI(CCM が申請) | ✅ |
Pod ネットワーク:K8s が定めているのは「各 Pod が自分の IP を持ち、Pod 同士は NAT を介さず直接通信できる」ことだけで、実装は CNI プラグインに任されています。
| flannel(このリポジトリ) | VCN-Native(OCI 独自) | |
|---|---|---|
| Pod の IP | overlay(10.244.x.x) |
VCN サブネットの IP |
| node をまたぐ通信 | 外側に node → node のパケットをもう 1 枚かぶせる(VXLAN、UDP 8472) | カプセル化しない |
| VCN から Pod が見えるか | 見えない | 見える(Pod ごとに NSG を設定、LB から Pod へ直接転送) |
| VCN の IP の消費 | 消費しない | Pod の数だけ消費する |
Service の正体:ClusterIP を持っているネットワーク・インタフェースはどこにもありません。kube-proxy が各 node に iptables のルールを書きます:「ClusterIP:80 宛てのパケット → Pod を 1 つランダムに選び、宛先アドレスを書き換える(DNAT)」。そのため ClusterIP に ping が通らないのは正常です(ルールは TCP / UDP にしかマッチしない)。Service の確認には curl を使います。
ブラウザから Pod まで:独立した 2 回の選択¶
flowchart TB
PC["ブラウザ"] -->|":80"| LB["OCI LB"]
LB -->|"① LB が node を選ぶ<br/>node の IP しか知らない"| NA["node A<br/>:NodePort"]
LB -.-> NB["node B<br/>:NodePort"]
NA -->|"② node が Pod を選ぶ<br/>候補はクラスタ全体の Pod"| PA["node A 上の Pod"]
NA -->|"別 node の Pod を選択<br/>flannel でカプセル化"| PB["node B 上の Pod"]
- LB が node を選ぶ:LB は Pod の IP(overlay)を知らないので、node の NodePort をバックエンドとして登録するしかありません
- node が Pod を選ぶ:kube-proxy のルールにはクラスタ全体の Pod が含まれるので、ある node に Pod が 1 つもなくてもリクエストは成功します(デフォルトの
externalTrafficPolicy: Cluster)。代償としてホップが 1 回増え、Pod から見える送信元 IP は node のものになります - LB のヘルスチェックの宛先は kube-proxy の
:10256です。「この node がこの Service を処理できるか」を知っているのは kube-proxy だけです(externalTrafficPolicy: Localのときは、これで Pod のない node を除外します)
L4 と L7:LB と Ingress¶
| 何が見えるか | 何で振り分けられるか | |
|---|---|---|
| L4(トランスポート層) | IP、ポート | 「80 番ポートのトラフィックをこのサーバー群に」 |
| L7(アプリケーション層) | HTTP のドメイン、パス、ヘッダー | 「a.example.com は A へ、/api は API へ」 |
Service 自体は L4 の概念です(YAML に書けるのはポートだけ)→ K8s は L7 を表現するために Ingress を別に設計しました。OKE の Service type=LoadBalancer で作られる OCI の LB は製品としては L7 に対応していますが、リスナーは TCP で、L4 として使われています(実測で確認)。
LB は「正門」、Ingress は「門を入ったあとの振り分けルール」で、両者は組み合わせて使います。
ブラウザ → OCI LB(正門:パブリック IP、1 つだけ)
→ Ingress Controller の Pod(ルールに従って振り分け、実体はリバース・プロキシ)
→ Service(ClusterIP)→ Pod
| LB(OCI LB) | Ingress | |
|---|---|---|
| 何か | クラウドの製品で、クラスタの外にある | K8s のオブジェクト = ルーティング・ルール 1 つ |
| 誰が動かすか | OCI | Ingress Controller(別途インストールする Pod) |
| どこで設定するか | OCI(Terraform、コンソール) | K8s の YAML(アプリと一緒に置く) |
- 実際のプロジェクトでは HTTP(S) のアプリは 1 つの入口を共有し、LB の数は「入口の種類」(パブリック / 内部、HTTP 以外)で決まります。アプリごとに 1 つではありません
- 後継の標準は Gateway API です。OCI のどの Ingress 実装を使うかは要確認です
なぜ LB を Terraform ではなく K8s に作らせるのか:バックエンド(全 node の NodePort)は変わり続けるため、CCM が変化を見張って自動で更新します。アプリは Service を 1 つ書くだけで公開でき、同じ Service を別のクラウドでも使えます。代償は、LB が Terraform の state に入らないことで、削除時は先に kubectl delete が必要です(OKE のページを参照)。一般的な分担は、LB は K8s に作らせ、予約 IP、DNS、証明書のような「固定のもの」は Terraform で管理する、というものです。
flannel は NetworkPolicy を実装していない
K8s のデフォルトでは、すべての Pod が互いにアクセスできます(Namespace をまたいでも同じ)。NetworkPolicy で制限できますが、flannel は実装していないため、書いても受け付けられるだけで効果はなく、エラーも出ません。使うなら Calico などを入れるか、VCN-Native + NSG を使います。
5. 設定とストレージ¶
ConfigMap と Secret¶
設定をイメージの外に置く → 同じイメージで、開発と本番で別の ConfigMap を使います。環境変数またはファイルとして注入できます。
ConfigMap を変えても、Pod が追従するとは限らない
環境変数:更新されません(プロセス起動時に 1 回読むだけ)。ファイル:1 分ほどで更新されますが、アプリが自分で読み直す必要があります。
→ 変更後は一般に kubectl rollout restart deployment <名前> を実行します。
Secret の使い方は ConfigMap とほぼ同じです。違いは、RBAC で読める人を個別に制限できること、etcd 内で暗号化できること、ファイルとしてマウントするとメモリ上に置かれることです。
Secret はデフォルトでは base64 なだけで、暗号化ではない
読み取り権限があれば base64 -d で平文が見えます。値の入った Secret の YAML は Git にコミットしません(「機密情報を Terraform の state に入れない」と同じ原則)。
| K8s Secret | OCI ボールトのシークレット | |
|---|---|---|
| 保存先 | etcd | ボールト |
| 暗号化 | デフォルトではなし(OKE では自分のボールトの鍵で etcd を暗号化できる) | 必ず暗号化 |
| バージョン管理 / ローテーション | なし | あり |
| 権限 | RBAC | IAM ポリシー |
実際のプロジェクトでよくあるやり方は、機密情報はボールトで管理し、実行時に K8s へ同期する(External Secrets Operator など)ことです。
ストレージ:PV / PVC / StorageClass¶
Pod は node を移る → データは「Pod についていく」ストレージに置く必要があります。
| オブジェクト | 意味 | Terraform との対比 |
|---|---|---|
| PVC | アプリの申請:「50GB がほしい、1 つの node から読み書き」 | .tf 内の resource |
| StorageClass | どう作るか。OKE のデフォルトは oci-bv(CSI → OCI のブロック・ボリューム) |
provider |
| PV | 実際に作られたストレージ(自動作成) | apply 後の実際のリソース |
OKE での注意点:
- 最小 50GB:
1Giを申請しても 50GB が作られます。Always Free のブロック・ボリューム合計 200GB のうち、node 2 台のブート・ボリューム(Boot Volume)ですでに 100GB を使っている → PVC は最大 2 つ - Terraform の state に入らない:PVC を削除せずに
terraform destroyすると、ブロック・ボリュームが残って枠を占有します reclaimPolicy:Delete= PVC を削除するとディスクも削除、Retain= 残す(消し忘れるとずっと枠を占有)。oci-bvのデフォルトがどちらかは要確認です(講義ではドキュメントに基づきDeleteと書いた)
6. 信頼性¶
probe / resources はどこに書くか
manifest(Deployment の containers[])に書き、イメージには書きません。イメージは「何を動かすか」、manifest は「どう動かすか」を管理します。Dockerfile の HEALTHCHECK は K8s では無視されます。
3 種類の probe¶
| probe | 何を問うか | 失敗したら |
|---|---|---|
| startupProbe | 起動は完了したか? | 完了するまで他の 2 つは実行しない(起動の遅いアプリが誤判定されるのを防ぐ) |
| livenessProbe | まだ生きているか?(再起動すべきか) | kubelet が同じ Pod 内でコンテナを再起動(RESTARTS が増える) |
| readinessProbe | 今リクエストを受けられるか?(トラフィックを送るべきか) | Service の転送先から外す(再起動はしない)。回復すれば戻す |
startupProbe:
httpGet: {path: /, port: 80}
periodSeconds: 10
failureThreshold: 12 # 最大 10 秒 × 12 = 120 秒待つ
readinessProbe:
httpGet: {path: /, port: 80}
periodSeconds: 5
livenessProbe:
httpGet: {path: /, port: 80}
periodSeconds: 10
failureThreshold: 3 # 3 回連続で失敗したら再起動
よくある 2 つの間違い
- liveness で外部の依存先をチェックする(例:DB に接続できるか):DB が少し止まる → すべての Pod が一斉に再起動を繰り返し、再起動しても接続できない。liveness は自分自身だけをチェックし、依存先は readiness に置きます
- 起動に 60 秒かかるアプリに liveness だけを書く:30 秒で「死んでいる」と判定 → 再起動 → いつまでも起動できない。チェック開始を遅らせるだけでは不安定 → startupProbe を使います
resources:requests と limits¶
- requests = 予約で、scheduler が見るものです:「node の割り当て可能量 − 既存 Pod の requests 合計」で足りるか。見るのは実際の使用量ではありません。すべての node が予約で埋まる →
Pending - limits = 上限で、cgroups が強制します。CPU を超える → スロットリング(kill されない)、メモリを超える →
OOMKilled - よくあるやり方は、メモリには上限を設け、CPU には設けないことです(このリポジトリの nginx.yaml もそう:
requestsは cpu 50m / memory 64Mi、limitsは memory 128Mi)
ローリング・アップデート:readiness で無停止を保証する¶
maxSurge(最大何個多くしてよいか)と maxUnavailable(最大何個利用不可でよいか)がペースを決めます。デフォルトはどちらも 25% です。
ローリング・アップデートは、新しい Pod の readiness が成功するのを待ってから先に進みます。新バージョンにバグがあって起動できない → readiness が失敗し続ける → 更新はそこで止まり、古い Pod がサービスを続けます → kubectl rollout undo。readiness を書かないと、壊れたバージョンも正常と見なされ、古い Pod が 1 つずつ削除されていきます。
スケジューリング:必要なのは「分散」で、「node の指定」ではない¶
scheduler が決めるのは配置の瞬間だけで、あとから自動で調整することはありません。Phase 6 の実験:1 台を drain → Pod がすべてもう 1 台に移る → uncordon 後に 2 つに縮小 → 2 つとも同じ node(冗長性なし)。
# app=web の Pod を node ごとに分散(各 node の数の差は 1 以下、できなければスケジュールしない)
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname # 分散の単位:node(FD なども可)
whenUnsatisfiable: DoNotSchedule # できないとき:スケジュールしない(ScheduleAnyway = できるだけ)
labelSelector:
matchLabels: {app: web}
| やりたいこと | 使うもの |
|---|---|
| Pod を別々の node / FD に分散する | topologySpreadConstraints |
| 特定の種類の node にだけ置く(例:arm64) | nodeSelector / nodeAffinity |
| node が一般の Pod を拒否する(例:GPU node) | node に taint、Pod に toleration |
| メンテナンス(drain)時の最低保証 | PodDisruptionBudget(PDB) |
nodeSelector で特定の 1 台を指定してはいけません。その 1 台にすべてが集まるか、その node が入れ替えられたとき(アップグレード時は node 名も変わる)に Pod の行き場がなくなります。
PDB(Pod Disruption Budget、DB とは無関係)=「メンテナンス時に最低いくつ稼働させておくか」です。kubectl drain のとき、1 つ退避させ、新しい Pod が準備完了になるのを待ってから次を退避させます。ただし対象は drain やアップグレードのような自発的な中断だけで、node の突然のダウンには分散配置で対応します。
| 信頼性の部品 | 何を防ぐか |
|---|---|
| probe | アプリのハング、準備前のリクエスト受け付け |
| resources | リソースの奪い合い、node の詰め込みすぎ |
| ローリング・アップデート + readiness | バージョン切り替え時の中断、壊れたバージョンの拡散 |
| 分散 + PDB | 1 台落ちると全滅、メンテナンス時の全停止 |
7. 権限と運用¶
Namespace¶
クラスタ内の区画で、OCI のコンパートメントによく似ています:グループ化、それを単位とした権限設定、クォータ。
- ネットワークの分離ではありません:異なる Namespace の Pod もデフォルトで互いにアクセスできます
- Namespace を削除 = 中身をすべて削除(PVC の裏のブロック・ボリュームも含む)
RBAC:誰が何をできるか¶
OCI のポリシーは 1 文ですが、K8s では 2 つのオブジェクトに分かれます:Role(何ができるか)+ RoleBinding(誰に与えるか)。分けているのは、同じ Role を多くの人にバインドするためです。
| 範囲 | |
|---|---|
| Role + RoleBinding | 1 つの Namespace 内 |
| ClusterRole + ClusterRoleBinding | クラスタ全体 |
組み込みの ClusterRole:view(読み取り専用、Secret は含まない)、edit、admin、cluster-admin。
組み込みの view を再利用する:RoleBinding で ClusterRole もバインドできる
developers グループを dev でだけ読み取り専用にしたい場合、Role を自分で書く必要はありません。
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding # RoleBinding → この Namespace でのみ有効
metadata:
name: dev-view
namespace: dev
subjects:
- kind: Group
name: developers
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole # バインドするのはクラスタ・レベルの ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
権限の範囲は Binding で決まります。ClusterRole は「何ができるか」の定義にすぎず、RoleBinding でバインドすればその Namespace でのみ有効です。
OKE の権限は 2 層です:① OCI IAM:kubeconfig / トークンを取得できるか、② K8s RBAC:クラスタ内で何ができるか。テナンシ(Tenancy)の管理者が自動で cluster-admin になるかは要確認です。
ServiceAccount:Pod 自身の ID¶
Pod 内のプログラムが K8s API を呼ぶときに使う ID です。各 Namespace に default(ほぼ権限なし)が自動で作られます。
- 一般的な Web アプリは K8s API を呼ばない →
automountServiceAccountToken: false(侵害されても攻撃者はトークンを取れない) - API を呼ぶプログラム:専用の ServiceAccount を作り、最小限の権限だけを与えます
- Pod から OCI API を呼ぶ場合:node のインスタンス・プリンシパルを使うと、その node 上のすべての Pod が同じ権限になります(範囲が広すぎる)。ServiceAccount 単位で IAM ポリシーを書ける OKE Workload Identity は Enhanced クラスタ(有料)でしか使えません
Job、CronJob と Helm¶
- Job:プロセスが正常終了 = 完了で、再起動しません。リトライや node 障害のとき2 回実行されることがある → 冪等に設計します。CronJob は
timeZoneを書かないと UTC です - Helm = K8s 版の
dnf:Chart(テンプレート + デフォルト・パラメータ)≒ Terraform の module、values ≒terraform.tfvars、helm template≒ plan - chart 内の LoadBalancer / PVC も OCI のリソースを作る → 先に
helm uninstall、そのあとterraform destroy
8. 設計判断の一覧¶
| 判断 | 選んだもの | 選ばなかったもの | 理由 |
|---|---|---|---|
| Pod の動かし方 | Deployment | Pod を直接作成 | 削除されても誰も補充しない。バージョン切り替えは ReplicaSet に頼る |
| イメージ | tag を固定(nginx:1.27)、マルチアーキテクチャ |
latest |
tag が固定されていないと、どのバージョンが動いているかわからない。A1 には arm64 が必要。digest は今後の練習 |
| resources | メモリに limits、CPU には設けない | どちらも書かない / CPU にも上限 | node のメモリを食い尽くすのを防ぐ。CPU に上限を設けると不要にスロットリングされる |
| ヘルスチェック | liveness は自分だけ、依存先は readiness | liveness で DB をチェック | 依存先が止まるとすべての Pod が一斉に再起動する |
| 冗長性 | topologySpreadConstraints + PDB |
nodeSelector で node を指定 | node は家畜で、入れ替えられる |
| 権限 | RoleBinding + 組み込みの view / edit |
全員に cluster-admin |
最小権限。Role を作り直さずに済む |
| 機密情報 | ボールトで管理し、実行時に同期 | Secret の YAML を Git に入れる | Secret は base64 にすぎない |
| DB | マネージド DB(K8s の外) | StatefulSet で自分で動かす | バックアップ、高可用性はクラウドに任せる |
| 外部への入口 | LB 1 つ(+ Ingress) | アプリごとに LB 1 つ | 費用、管理。Always Free の LB は 1 つだけ |
このリポジトリの 06-oke/k8s/nginx.yaml は最小の例です。probe、PDB、明示的な分散、専用 Namespace、イメージ digest はまだ書いておらず、今後の練習です。
9. 検証(実験で観察したこと)¶
| 確認したいこと | やったこと | 見えたこと |
|---|---|---|
| 自己修復(reconcile) | kubectl delete pod |
すぐに新しい Pod が補充され、名前も IP も変わった |
| コンポーネントの連鎖 | node 参加前に coredns を見る | Pod オブジェクトは存在、Pending、NODE は <none>。node 参加後に自動で Running |
| コンポーネントの連鎖 | kubectl get events(Pod 単位は kubectl events --for pod/<name>) |
Deployment controller(ScalingReplicaSet)→ ReplicaSet controller(SuccessfulCreate)→ kubelet(Pulling / Started)。scheduler の段階は個別に記録していない |
| スケジューリングは配置時にしか起きない | drain → uncordon → 2 つに縮小 | Pod は戻らず、2 つとも同じ node に |
10. ハマりどころ¶
| 現象 | 原因 | 対策 |
|---|---|---|
exec format error / no matching manifest for linux/arm64 |
イメージに arm64 版がない(A1 は arm64) | マルチアーキテクチャ・イメージを使う、または docker buildx --platform |
| 実行中のコンテナで直したバグが戻ってくる | Pod の再作成で書き込み可能な層が消えた | Dockerfile を変更し、新しいイメージを build |
Deployment の selector を変えると field is immutable |
selector は作成後に変更できない | delete して再作成 |
| ConfigMap を変えてもアプリが変わらない | 環境変数は起動時にしか読まれない | kubectl rollout restart |
起動の遅いアプリがずっと CrashLoopBackOff |
liveness だけを書いていて、起動中に再起動される | startupProbe を追加 |
| NetworkPolicy を書いても効かない | flannel が実装していない | Calico など / VCN-Native + NSG |
| uncordon 後も Pod が 1 台に偏ったまま | scheduler はあとから調整しない | kubectl rollout restart。普段から topologySpreadConstraints を書く |
| Service にアクセスしても応答がない | selector が Pod にマッチしていない | endpointslices を見る |
kubectl delete pod 10.6.10.x → NotFound |
それは node 名(OKE の node 名 = プライベート IP) | Pod 名は nginx-xxxx-xxxxx の形式 |
11. 確認問題¶
1. Deployment のコントローラが Pod を作った。では Pod の中のコンテナを起動するのは誰?
その node 上の kubelet です(CRI-O にイメージを pull させ、起動させる)。コントローラが作るのは Pod オブジェクト(etcd 内のレコード)だけで、scheduler はどの node に置くかを決めるだけです。
2. Deployment を kubectl apply したあと、各コンポーネントが何をするかを順番に説明してください
apiserver が etcd に保存 → Deployment コントローラが ReplicaSet を作成 → ReplicaSet コントローラが Pod オブジェクトを作成 → scheduler が Pod に node を選ぶ → kubelet がコンテナを起動して状態を報告 → Service の転送先が更新される。各コンポーネントは互いを呼び出さず、すべて apiserver を経由します。
3. クラスタにまだ node が 1 台もないとき、Pod はどんな状態?どのコンポーネントで止まっている?
Pending、NODE は <none> です。Pod オブジェクトはコントローラがすでに作っていて、scheduler で止まっています(選べる node がない)。そのため起動する kubelet もいません。
4. 起動に 60 秒かかるアプリに liveness だけを書いた(10 秒ごと、3 回失敗で再起動)。どうなる?どう直す?
30 秒で「死んでいる」と判定されて再起動され、いつまでも起動できません(CrashLoopBackOff)。startupProbe を追加し、periodSeconds × failureThreshold を起動時間より長くします。これが成功するまで liveness は実行されません。
5. ローリング・アップデートで、新しい Pod の準備完了を判断するのはどの probe?liveness が失敗すると何が起きる?
readinessProbe です。liveness の失敗は、kubelet が同じ Pod 内でコンテナを再起動することで、新しい Pod を作るわけではなく、ローリング・アップデートの判断にも影響しません。
6. liveness で「DB に接続できるか」をチェックすると何が問題?
DB が止まると全 Pod の liveness が失敗 → 全部が再起動を繰り返し、再起動しても接続できず、問題が大きくなります。liveness は自分自身だけをチェックし、依存先のチェックは readiness に置きます(接続できなければ、ひとまずリクエストを受けない)。
7. レプリカ 2 つの Pod が両方とも同じ node で動いている。別々の node に分ける設定のポイントは?
topologySpreadConstraints:topologyKey: kubernetes.io/hostname(node 単位)、maxSkew: 1、whenUnsatisfiable: DoNotSchedule、labelSelector でこの Pod 群を選択。すでに偏っている場合は kubectl rollout restart で再配置します。
8. なぜ nodeSelector で 2 つの Pod をそれぞれ別の node に指定しないのか?
node は入れ替えられるもの(アップグレード時は新しい node になり、名前も変わる)で、特定の node を指定すると、その node がなくなったとき Pod の行き場がなくなります。書くべきなのは「どこに置くか」ではなく「分散」のルールです。
9. developers グループを dev Namespace で読み取り専用にしたい。いちばん手間のかからない書き方は?
dev に RoleBinding を 1 つ作り、roleRef で組み込みの ClusterRole view を指します。Role を自分で書く必要はなく、RoleBinding なので権限は dev の中でだけ有効です。
10. 組み込みの view で Secret は見られる?Secret は暗号化して保存されている?
view には Secret は含まれません。Secret はデフォルトでは base64 なだけで、読み取り権限があれば平文が見えます。etcd の暗号化は別途設定が必要です(OKE では自分のボールトの鍵を使える)。
11. Pod が CPU の limits を超えた場合と、メモリの limits を超えた場合で結果はどう違う?
CPU:スロットリングされて遅くなりますが、kill はされません。メモリ:OOMKilled になり、コンテナが再起動されます。
12. PDB で node の突然のダウンによるサービス中断を防げる?
防げません。PDB の対象は drain やノード・プールのアップグレードのような自発的な中断だけです。突然のダウンには、Pod を別々の node / FD に分散して対応します。
13. ClusterIP に ping が通らない。Service が壊れているということ?
そうとは限りません。ClusterIP は kube-proxy が node に書いた iptables のルールにすぎず、TCP / UDP のポートにしかマッチしないので、ICMP には応答しません。curl で確認します。