コンテンツにスキップ

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 で確認します。