コンテンツにスキップ

OKE の設計と実践

ひとことで

Oracle Kubernetes Engine(OKE)= 標準の Kubernetes + OCI への接続レイヤー(LB、ブロック・ボリューム、IAM)です。 設計の核心は 外部に出すのは LB だけ、API エンドポイントはプライベート、セキュリティ・リストは Terraform だけが管理する こと。運用の核心は 3 つの層(Terraform / K8s オブジェクト / K8s が間接的に作る OCI リソース)を区別し、削除は 上の層から 行うことです。

このページは Phase 6 と Phase 11(第 8 課と第 2 部の実測)をまとめたものです。K8s そのものの原理は Kubernetes の原理、VCN(仮想クラウド・ネットワーク)/ セキュリティ・リストの基本は ネットワーク設計 を参照してください。

「要確認」について

「要確認」と書いた項目はまだ実測しておらず、ドキュメントまたは推測に基づいて書いています。

1. まず構成要素を知る

OKE AWS ひとことで
OKE クラスタ EKS クラスタ マネージドの K8s。コントロール・プレーンは Oracle 自身のテナンシ(Tenancy)内に作られ、ユーザーからは見えない
BASIC_CLUSTER / ENHANCED_CLUSTER — Basic:コントロール・プレーンは無料、SLA なし。Enhanced:時間課金(SLA、アドオン管理、virtual nodes など)
ノード・プール(Node Pool、managed nodes) Managed node group ワーカー・ノード(VM)のグループ。OKE が VM の作成と登録を担当
virtual nodes Fargate VM の概念がなく、Pod 単位の課金。Enhanced が必要
API エンドポイント 同左 apiserver の入口。VCN にはその IP が 1 つ現れるだけ
oci ce cluster generate-token aws eks get-token kubeconfig の認証:OCI IAM でトークンを取得

誰が何を管理するか

部分 管理者 説明
コントロール・プレーン(apiserver、etcd、scheduler、CCM) Oracle バックアップ、アップグレードは Oracle が行う
VCN / サブネット(Subnet) / セキュリティ・リスト(Security List) 自分 ルールを間違える → ノードが登録できない、LB がつながらない
ワーカー・ノード 自分(軽め) シェイプ(Shape)、台数、イメージ。kubelet などは OKE がプリインストール。OS パッチと K8s のアップグレードは自分で開始する(イメージを変更 → 1 台ずつ入れ替え)
アプリケーション 自分 kubectl。LB / ブロック・ボリュームは K8s が間接的に作る

ワーカー・ノードの考え方は VM とは変わります。nginx はコンテナの中で動き、VM はコンテナを動かす場所にすぎません。管理するのは台数とバージョンで、壊れたら入れ替え、修理はしません。

2. 原理:3 つの層

同じ nginx のサイトが 3 つの層にまたがっています。問題が起きたら、まずどの層かを切り分けます。

flowchart TB
  subgraph L1["① クラウド基盤:Terraform"]
    VCN["VCN / サブネット<br/>セキュリティ・リスト"]
    CL["OKE クラスタ<br/>ノード・プール"]
  end
  subgraph L2["② K8s オブジェクト:kubectl"]
    DEP["Deployment<br/>→ ReplicaSet → Pod"]
    SVC["Service<br/>type=LoadBalancer"]
  end
  subgraph L3["③ 間接的に作られる OCI リソース:state 外"]
    LB["OCI LB<br/>CCM が作成"]
    BV["ブロック・ボリューム<br/>PVC → CSI が作成"]
  end
  SVC -->|"CCM"| LB
  L1 --> L2
層 何を管理するか 誰が担当するか
① クラウド基盤 VCN、サブネット、クラスタ、ノード・プール Terraform(06-oke/)
② K8s オブジェクト Deployment、Pod、Service kubectl で期待する状態を送信。コントローラがオブジェクトを作り、scheduler が node を選び、kubelet がコンテナを起動
③ 間接的に作られるクラウド・リソース Service の OCI LB、PVC のブロック・ボリューム cloud-controller-manager(CCM)/ CSI。Terraform はこれらを知らない

ここから 2 つのルールが導かれます。

  • 削除は上の層から:先に ② の Service / PVC を削除し、③ の LB / ブロック・ボリュームが消えたことを確認してから ① を destroy する(第 8 節)
  • ③ の層が ① の層のものを変更しに行く:CCM はデフォルトでセキュリティ・リストにルールを追加する → Terraform と衝突する(第 4 節)

3. ネットワーク設計

VCN 10.6.0.0/16
 ├─ api   10.6.0.0/28   private  API エンドポイント(IP は数個で足りる → /28)
 ├─ nodes 10.6.10.0/24  private  ワーカー・ノード(+ Cloud Shell の private network)
 └─ lb    10.6.20.0/24  public   LB だけ
Pod CIDR 10.244.0.0/16(flannel の overlay)/ Service CIDR 10.96.0.0/16

public は lb だけです:インターネットから直接届くのは LB だけで、node と API エンドポイントにはパブリック IP がありません(ネットワーク設計 と同じ考え方)。nodes のルーティング:0.0.0.0/0 → NAT ゲートウェイ(Docker Hub のイメージを pull)、Oracle サービス → サービス・ゲートウェイ(OKE への登録など)。

サブネットごとにセキュリティ・リストを 1 つ

3 つのサブネットは役割が違い、必要なルールも違います → Phase 4 の network モジュール(すべてのサブネットで 1 つのセキュリティ・リストを共有)は再利用せず、サブネットごとに書きました。

方向 ポート 理由
ローカル PC → lb TCP 80 サイトの入口(ローカル PC の IP のみ許可)
lb → nodes TCP 30000-32767 LB から NodePort へ転送
lb → nodes TCP 10256 LB のヘルスチェックが kube-proxy にアクセス
nodes / Cloud Shell → api TCP 6443、12250 kubelet / kubectl → apiserver(6443)、ワーカー → コントロール・プレーン(12250、proxymux の通り道で、kubectl logs/exec はここを通る)
api → nodes TCP すべて コントロール・プレーン → kubelet(10250 など)
nodes ↔ nodes すべて flannel の VXLAN(UDP 8472)、Pod 間の通信
# 06-oke/security.tf(抜粋):nodes サブネットのイングレス・ルール
ingress_security_rules {
  source      = local.subnet_cidrs.nodes
  protocol    = "all"                  # flannel の overlay(UDP 8472)/ Pod 間
  description = "node to node / pod to pod"
}
ingress_security_rules {
  source      = local.subnet_cidrs.lb
  protocol    = "6"
  description = "LB to NodePort"
  tcp_options {
    min = local.node_port_min          # 30000
    max = local.node_port_max          # 32767
  }
}

nodes ↔ nodes の全許可を削除したら(講義での分析)

VXLAN がブロックされる → node をまたぐ Pod 同士が通信できなくなります。厄介なのは一見すべて正常に見えることです。kubectl get nodes はすべて Ready(kubelet → apiserver は nodes → api を通る)なのに、サイトは開けたり、タイムアウトしたりし(別の node 上の Pod が選ばれたときに失敗)、一部の Pod では名前解決ができません(CoreDNS が片方の node にしかない)。

API エンドポイントはプライベートにし、kubectl は Cloud Shell で使う

# 06-oke/oke.tf(抜粋)
endpoint_config {
  subnet_id            = oci_core_subnet.api.id
  is_public_ip_enabled = false          # パブリック IP を割り当てない → VCN 内からしかアクセスできない
}
  • クラスタ全体を操作できる入口をインターネットに置かない
  • パブリック・エンドポイントにした場合:Cloud Shell の送信元 IP は固定されない → 6443 を 0.0.0.0/0 に開放するしかない(SSH と同じ問題)
  • 代償:kubectl は VCN 内でしか使えない → Cloud Shell の private network で nodes サブネットに接続。ローカル PC には OCI CLI もインストールできない(kubeconfig の認証には oci コマンドが必要)が、Cloud Shell には oci と kubectl が最初から入っている

4. OKE にセキュリティ・リストを自動変更させない

CCM は LB を作るとき、デフォルトでセキュリティ・リストにルールを自分で追加します → Terraform で管理しているセキュリティ・リストで、plan のたびにドリフト(drift)が出ます。

対策:NodePort の範囲とヘルスチェック・ポートを Terraform で事前に許可し(第 3 節)、manifest で自動管理を無効にします。

# 06-oke/k8s/nginx.yaml(抜粋)
metadata:
  annotations:
    # flexible 10Mbps = Always Free の LB(書かないとデフォルトの LB は Always Free を超える)
    service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "10"
    # OKE にセキュリティ・リストを変更させない(ルールは security.tf に書いてある)
    oci.oraclecloud.com/security-rule-management-mode: "None"

結果:LB を作成したあとも terraform plan は No changes のままです。

原則:1 つのリソースは 1 つのツールだけに管理させる。 同じセキュリティ・リストを Terraform と CCM が同時に変更すると、両者がずっと上書きし合います。

5. ノード・イメージの自動選択

OKE の node は専用の Oracle Linux イメージ(kubelet などがプリインストール)を使い、名前は Oracle-Linux-9.8-aarch64-2026.09.18-0-OKE-1.36.4-1923 のような形です。

# 06-oke/oke.tf(抜粋):aarch64 + K8s バージョンが一致 + GPU 版を除外 → 名前の降順で先頭(= 日付が最新)
node_images = {
  for s in data.oci_containerengine_node_pool_option.this.sources : s.source_name => s.image_id
  if length(regexall("^Oracle-Linux-[0-9.]+-aarch64-.*-OKE-${replace(local.k8s_ver, ".", "\\.")}-", s.source_name)) > 0
  && !strcontains(s.source_name, "GPU")
}
node_image_name = reverse(sort(keys(local.node_images)))[0]

イメージの OCID を固定しないので、コードは変更不要です。Phase 6 のときは 2026.09.18 版が選ばれ、Phase 11 で再度 apply したときは自動で新しい 2026.10.16 版が選ばれました。

6. 実測:Service LB で実際に何が作られたか

Phase 11 で、Service type=LoadBalancer によって作られた LB を SDK で調べました。

項目 実測
シェイプ flexible、min = max = 10Mbps(annotation が有効 = Always Free)
リスナー 名前は TCP-80、プロトコルは TCP(L4)。LB 製品は L7 に対応しているが、ここでは L4 として使われている
バックエンド node 2 台の NodePort、ポリシーは ROUND_ROBIN
ヘルスチェック HTTP :10256/healthz(kube-proxy)。アプリの 80 番ポートではない
名前 / タグ 名前は UUID、タグなし、Terraform の state にも入っていない → コンソール上では誰が作ったのかわかりにくい
node のフォルト・ドメイン FD を指定しなかったところ、2 台はそれぞれ FD-1 / FD-3 に。観察は 1 回だけなので、毎回分散が保証されるとは言えない

トラフィック:ローカル PC → LB:80 → node:NodePort → kube-proxy(iptables)→ Pod:80。別の node 上の Pod に転送されることもあります(原理は Kubernetes の原理 を参照)。

Events で見えたコンポーネントの連鎖:Deployment controller → ReplicaSet controller → scheduler → kubelet(Pulling / Started)。CCM:EnsuringLoadBalancer → already in progress で 1 回失敗して再試行 → EnsuredLoadBalancer。

Pod ネットワーク:flannel が node ごとに範囲を割り当てる

  • 通常の Pod の IP は 10.244.x.x。flannel、kube-proxy、CSI などのシステム Pod は hostNetwork で、node の IP をそのまま使う
  • 各 node は Pod CIDR の一部を割り当てられる:Phase 6 は /24(1 台が 10.244.1.x、もう 1 台が 10.244.0.x)、Phase 11 は /25(10.244.0.0/25 と 10.244.0.128/25)
  • なぜ変わったのかはまだ確認していない(node ごとの maxPods 設定に関係すると推測)

7. A1 の容量不足とノード・プール

Phase 11 の apply:クラスタは作成できましたが、ノード・プールが Out of host capacity になりました(無料の A1 リソースが一時的に足りない)。

  • OKE は失敗したノード・プールを自動で削除し、VM は残らない。再度 plan するとノード・プールの 1 to add だけになる
  • エラーの詳細は ContainerEngineClient.list_work_request_errors で確認
  • 対応:plan がノード・プールの 1 to add だけであることを確認してから apply。1 回目とすぐの再試行はどちらも失敗し、その後 10 分ごとに再試行して 3 回目(約 25 分後)に成功。待っている間、クラスタとネットワークは課金されない
  • Terraform がノード・プールの「Creation complete」を表示しても、node が ready とは限らない → SDK の get_node_pool().nodes で全台が ACTIVE であることを確認

8. 削除の順序

上の層から削除する

  1. kubectl delete -f nginx.yaml(PVC なども)→ K8s が自分で作った LB / ブロック・ボリュームを K8s 自身に削除させる
  2. SDK / コンソールで LB が消えたことを確認
  3. Cloud Shell の Network を Public に戻す(private network の VNIC が nodes サブネットに付いている)
  4. terraform destroy

順序を間違えるとどうなるか:

  • kubectl delete を忘れた場合:Terraform が lb サブネットを削除しようとしたとき、LB がまだそれを使っている → 削除できない。LB は Terraform の管理外の孤立したリソースになり、枠を占有し続ける(第 8 課での分析)
  • Public に戻し忘れた場合:Phase 6 で実際に起き、nodes サブネットの削除に 8 分 41 秒かかった。Cloud Shell を閉じてから約 95 秒後に VNIC が解放され、Terraform が再試行し続けてようやく削除できた。誰が使っているかは list_private_ips(subnet_id=...) で確認できる

9. 設計判断と Always Free のトレードオフ

判断 選んだもの 選ばなかったもの 理由
クラスタ・タイプ BASIC_CLUSTER ENHANCED_CLUSTER コントロール・プレーンが無料。学習用に SLA は不要
node A1 1 OCPU / 6GB × 2、ブート 50GB × 2 より大きく / より多く Always Free(A1 4 OCPU / 24GB、ブロック・ボリューム 200GB)の中で余裕を残す → PVC をあと 2 つ作れる
AD / FD 大阪の AD は 1 つだけ。FD は指定しない FD を指定 実測では今回は別々の FD に配置された(観察 1 回)
Pod ネットワーク(CNI) flannel(overlay) VCN-Native シンプルで、VCN の IP を消費しない。小さな node でも VNIC 数の制限を受けない。代償:NetworkPolicy が効かない、LB から Pod へ直接転送できない
セキュリティ・ルール サブネットごとにセキュリティ・リスト 1 つ 共有 1 つ / NSG OKE のルールはサブネットの役割ごとに自然に分かれる
API エンドポイント プライベート + Cloud Shell の private network パブリック・エンドポイント クラスタの操作入口を 0.0.0.0/0 にさらさない
セキュリティ・リストの管理 Terraform が専有、annotation None CCM に自動でルールを追加させる ドリフトと上書き合戦を避ける
Service LB flexible 10Mbps(annotation) デフォルトのシェイプ Always Free はこれだけ
ノード・イメージ data source で名前から最新を絞り込み OCID を固定 バージョン更新時にコードを変えなくてよい
イメージ・レジストリ Docker Hub(NAT ゲートウェイ経由) OCIR(サービス・ゲートウェイ経由) 公式の nginx マルチアーキテクチャ・イメージを使っている

10. 検証

確認したいこと 方法 結果
node が本当に ready か SDK get_node_pool().nodes 2 台 ACTIVE。kubectl get nodes で 2 台 Ready(ワーカーだけで、コントロール・プレーンは見えない)
サイトにアクセスできるか ローカル PC のブラウザ → LB:80 Welcome to nginx!
LB が無料のシェイプか SDK で LB を確認 flexible 10 / 10Mbps
CCM がセキュリティ・リストを変更していないか LB 作成後に terraform plan No changes
きれいに削除できたか destroy 後に SDK でコンパートメントを確認 VCN / LB / インスタンス / ブート・ボリューム / ブロック・ボリュームはすべてなくなった
oci-bv の reclaimPolicy kubectl get storageclass 要確認(Delete のはず)
テナンシの管理者が自動で cluster-admin になるか — 要確認
どの Ingress 実装を使うか — 要確認(OCI Native Ingress Controller / Gateway API)

11. ハマりどころ

現象 原因 対策
ノード・プールが失敗:Out of host capacity 無料の A1 リソースが一時的に足りない plan がノード・プールだけであることを確認して再試行
ノード・プールは「complete」なのに node がまだ ready でない Terraform は API の応答しか待たない SDK で node の状態を確認
kubectl dial tcp 10.6.0.x:6443: i/o timeout Cloud Shell が private network に接続していない nodes サブネットにつながる private network に切り替える
private network に接続できない 以前の定義が削除済みのサブネットを指している private network の定義を作り直す
LB 作成後に Terraform でドリフト CCM がセキュリティ・リストにルールを追加 事前に許可 + annotation None
terraform destroy でサブネットが削除できない LB が state に入っていない / Cloud Shell の VNIC が残っている 先に kubectl delete。Network を Public に戻す
kubectl describe pod に Events の節がない 原因は未確認(kubectl 1.36.4) kubectl events --for pod/<name>
Events がしばらくすると消える Event はデフォルトで 1 時間しか保持されない 見るなら早めに

12. 確認問題

1. OKE のコントロール・プレーンはどこにある?なぜ kubectl get nodes に出てこない?

Oracle 自身のテナンシ内にあり、Oracle が管理しています。get nodes に表示されるのは kubelet が自分から登録した Node オブジェクトで、コントロール・プレーン側には登録しに来る kubelet がいません。

2. kubectl delete を忘れてそのまま terraform destroy すると何が起きる?

Terraform は CCM が作った LB を知りません。lb サブネットを削除しようとしたとき LB がまだそれを使っているため、サブネットが削除できず、LB は孤立したリソースとして枠を占有し続けます。先に kubectl delete し、LB が消えたことを確認してから destroy します。

3. なぜ manifest で security-rule-management-mode を「None」にするのか?

CCM はデフォルトで LB 用のルールをセキュリティ・リストに追加しますが、このセキュリティ・リストは Terraform が管理しています → plan のたびにドリフトが出て、両者が上書きし合います。そこでポートは Terraform で事前に許可し、CCM には触らせません。

4. Service LB のヘルスチェックはなぜ node の 80 ではなく 10256 にアクセスする?

LB のバックエンドは Pod ではなく node の NodePort です。10256 は kube-proxy の /healthz で、この node がこの Service のトラフィックを処理できるかを知っているのは kube-proxy だけです。

5. nodes ↔ nodes の全許可を削除すると、kubectl get nodes はエラーになる?どんな現象が起きる?

エラーにはならず、node はすべて Ready です。しかし flannel の VXLAN がブロックされ、node をまたぐ Pod 同士が通信できません。サイトがつながったりつながらなかったりし、一部の Pod で名前解決ができなくなります。

6. API エンドポイントをパブリックにすればずっと簡単なのに、なぜそうしないのか?

クラスタ全体を操作できる入口がさらされます。しかも Cloud Shell の送信元 IP は固定されないので、6443 を 0.0.0.0/0 に開放するしかありません。プライベート・エンドポイント + Cloud Shell の private network のほうが安全です。

7. VCN-Native ではなく flannel を選んだ代償は?

VCN から Pod が見えません:Pod ごとに NSG を設定できず、LB から Pod へ直接転送できません(node を 1 回余計に経由)。さらに flannel は NetworkPolicy を実装していません。その代わりにシンプルで、VCN の IP を消費しません。

8. 2 台の node が今回は FD-1 と FD-3 に配置された。OKE は必ず分散してくれると言える?

言えません。観察は 1 回だけです。保証が必要なら明示的に設定し、Pod のレベルでも topologySpreadConstraints を書く必要があります。

コード全文

06-oke/oke.tf(クラスタ + ノード・プール)
# ------------------------------------------------------------
# OKE(Container Engine for Kubernetes):クラスタ = コントロール・プレーン(Control Plane)、ノード・プール(Node Pool)= ワーカー・ノード(Worker Node)
#   費用:BASIC_CLUSTER のコントロール・プレーンは無料。ENHANCED_CLUSTER は時間課金 → 使用しない
#         ワーカー = A1.Flex(Always Free 枠内)。Service LB は annotation で flexible 10Mbps を指定
# ------------------------------------------------------------

data "oci_identity_availability_domains" "this" {
  compartment_id = var.compartment_ocid
}

# ノード用のイメージ(Image):OKE 専用の Oracle Linux(kubelet 等がプリインストール済み)
#   名前の例:Oracle-Linux-9.8-aarch64-2026.09.18-0-OKE-1.36.4-1923
#   条件:aarch64(A1 は ARM)+ K8s バージョンと一致 + GPU 版は除外 → 名前の降順で先頭を取得(= 最新日付)
data "oci_containerengine_node_pool_option" "this" {
  node_pool_option_id = "all"
  compartment_id      = var.compartment_ocid
}

locals {
  k8s_ver = trimprefix(var.kubernetes_version, "v")

  node_images = {
    for s in data.oci_containerengine_node_pool_option.this.sources : s.source_name => s.image_id
    if length(regexall("^Oracle-Linux-[0-9.]+-aarch64-.*-OKE-${replace(local.k8s_ver, ".", "\\.")}-", s.source_name)) > 0
    && !strcontains(s.source_name, "GPU")
  }
  node_image_name = reverse(sort(keys(local.node_images)))[0]
}

resource "oci_containerengine_cluster" "this" {
  compartment_id     = var.compartment_ocid
  name               = "${local.name_prefix}-oke"
  kubernetes_version = var.kubernetes_version
  vcn_id             = oci_core_vcn.this.id
  type               = "BASIC_CLUSTER" # ENHANCED_CLUSTER は有料

  cluster_pod_network_options {
    cni_type = "FLANNEL_OVERLAY"
  }

  # API エンドポイントはプライベート・サブネット(Subnet)に配置し、パブリック IP は割り当てない → VCN(仮想クラウド・ネットワーク)内からのみアクセス可能(Cloud Shell のプライベート・ネットワーク)
  endpoint_config {
    subnet_id            = oci_core_subnet.api.id
    is_public_ip_enabled = false
  }

  options {
    # Service type=LoadBalancer のロード・バランサ(Load Balancer)はこのサブネットに作成
    service_lb_subnet_ids = [oci_core_subnet.lb.id]

    kubernetes_network_config {
      pods_cidr     = "10.244.0.0/16" # flannel のオーバーレイ用。VCN と重複しなければよい
      services_cidr = "10.96.0.0/16"  # ClusterIP 用
    }
  }

  freeform_tags = local.common_tags
}

resource "oci_containerengine_node_pool" "this" {
  compartment_id     = var.compartment_ocid
  cluster_id         = oci_containerengine_cluster.this.id
  name               = "${local.name_prefix}-pool"
  kubernetes_version = var.kubernetes_version

  node_shape = "VM.Standard.A1.Flex"
  node_shape_config {
    ocpus         = 1
    memory_in_gbs = 6
  }

  node_source_details {
    source_type             = "IMAGE"
    image_id                = local.node_images[local.node_image_name]
    boot_volume_size_in_gbs = 50 # 最小値。ブロック・ボリューム(Block Volume)は合計 200GB まで無料
  }

  node_config_details {
    size = var.node_count

    # 大阪は可用性ドメイン(Availability Domain)が 1 つのみ
    placement_configs {
      availability_domain = data.oci_identity_availability_domains.this.availability_domains[0].name
      subnet_id           = oci_core_subnet.nodes.id
    }

    node_pool_pod_network_option_details {
      cni_type = "FLANNEL_OVERLAY"
    }

    freeform_tags = local.common_tags
  }

  freeform_tags = local.common_tags
}
06-oke/network.tf(VCN / サブネット / ルーティング)
# ------------------------------------------------------------
# Phase 6:OKE(Container Engine for Kubernetes)用のネットワーク(flannel オーバーレイ版)
#   VCN(仮想クラウド・ネットワーク) 10.6.0.0/16 ─ IGW / NAT GW / Service GW
#    ├─ api   10.6.0.0/28  private:K8s API エンドポイント(パブリック IP なし)
#    ├─ nodes 10.6.10.0/24 private:ワーカー・ノード(Worker Node)。Cloud Shell のプライベート・ネットワークもここに接続
#    └─ lb    10.6.20.0/24 public :Service type=LoadBalancer が作成する OCI ロード・バランサ(Load Balancer)
#   Pod の IP(10.244.0.0/16)は flannel がノード間でオーバーレイ化するため、VCN の IP は消費しない
#
# 04 のネットワーク・モジュール(module)は「全サブネット(Subnet)で 1 つのセキュリティ・リスト(Security List)を共用」する構成だが、
# OKE はサブネットごとに必要なルールが異なる → ここではモジュールを使わず、リソースを直接記述(セキュリティ・リストは security.tf)
# ------------------------------------------------------------

locals {
  name_prefix = "tflab6"
  vcn_cidr    = "10.6.0.0/16"

  subnet_cidrs = {
    api   = "10.6.0.0/28"
    nodes = "10.6.10.0/24"
    lb    = "10.6.20.0/24"
  }

  common_tags = {
    project    = "tf-lab"
    managed_by = "terraform"
    phase      = "6"
  }
}

resource "oci_core_vcn" "this" {
  compartment_id = var.compartment_ocid
  cidr_blocks    = [local.vcn_cidr]
  display_name   = "${local.name_prefix}-vcn"
  dns_label      = local.name_prefix

  freeform_tags = local.common_tags
}

# ---- Gateway ----

resource "oci_core_internet_gateway" "this" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-igw"
  enabled        = true

  freeform_tags = local.common_tags
}

# ワーカー・ノードが Docker Hub 等の外部イメージを pull するために使用
resource "oci_core_nat_gateway" "this" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-natgw"

  freeform_tags = local.common_tags
}

data "oci_core_services" "all" {
  filter {
    name   = "name"
    values = ["All .* Services In Oracle Services Network"]
    regex  = true
  }
}

# ワーカー・ノードの OKE への登録、OCIR イメージの pull、Cloud Shell のトークン取得(containerengine API)はすべてここを経由
resource "oci_core_service_gateway" "this" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-sgw"

  services {
    service_id = data.oci_core_services.all.services[0].id
  }

  freeform_tags = local.common_tags
}

# ---- Route Table ----

resource "oci_core_route_table" "public" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-rt-public"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_internet_gateway.this.id
  }

  freeform_tags = local.common_tags
}

resource "oci_core_route_table" "private" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-rt-private"

  route_rules {
    destination       = "0.0.0.0/0"
    destination_type  = "CIDR_BLOCK"
    network_entity_id = oci_core_nat_gateway.this.id
  }

  route_rules {
    destination       = data.oci_core_services.all.services[0].cidr_block
    destination_type  = "SERVICE_CIDR_BLOCK"
    network_entity_id = oci_core_service_gateway.this.id
  }

  freeform_tags = local.common_tags
}

# ---- Subnet ----

resource "oci_core_subnet" "api" {
  compartment_id             = var.compartment_ocid
  vcn_id                     = oci_core_vcn.this.id
  cidr_block                 = local.subnet_cidrs.api
  display_name               = "${local.name_prefix}-subnet-api"
  dns_label                  = "api"
  route_table_id             = oci_core_route_table.private.id
  security_list_ids          = [oci_core_security_list.api.id]
  prohibit_public_ip_on_vnic = true

  freeform_tags = local.common_tags
}

resource "oci_core_subnet" "nodes" {
  compartment_id             = var.compartment_ocid
  vcn_id                     = oci_core_vcn.this.id
  cidr_block                 = local.subnet_cidrs.nodes
  display_name               = "${local.name_prefix}-subnet-nodes"
  dns_label                  = "nodes"
  route_table_id             = oci_core_route_table.private.id
  security_list_ids          = [oci_core_security_list.nodes.id]
  prohibit_public_ip_on_vnic = true

  freeform_tags = local.common_tags
}

resource "oci_core_subnet" "lb" {
  compartment_id             = var.compartment_ocid
  vcn_id                     = oci_core_vcn.this.id
  cidr_block                 = local.subnet_cidrs.lb
  display_name               = "${local.name_prefix}-subnet-lb"
  dns_label                  = "lb"
  route_table_id             = oci_core_route_table.public.id
  security_list_ids          = [oci_core_security_list.lb.id]
  prohibit_public_ip_on_vnic = false

  freeform_tags = local.common_tags
}
06-oke/security.tf(サブネットごとのセキュリティ・リスト)
# ------------------------------------------------------------
# セキュリティ・リスト(Security List):サブネット(Subnet)ごとに 1 つ(ルールは OKE ドキュメントの flannel の例を参考に、Always Free の検証用に最小化)
#
#   my_ip ──:80──→ [lb] LB ──:30000-32767 / :10256──→ [nodes] ワーカー ×2
#   [nodes] ワーカー / Cloud Shell ──:6443 / :12250──→ [api] K8s API エンドポイント
#   [api] コントロール・プレーン ──all──→ [nodes](kubelet 10250 等)
#
# Service LB のルールは OKE に自動変更させない:
#   cloud-controller-manager はデフォルトでセキュリティ・リストに自らルールを追加する → Terraform 管理のセキュリティ・リストで毎回 plan にドリフト(drift)が出る
#   → ここで NodePort 範囲を事前に許可し、manifest 側で annotation により自動管理を無効化(k8s/nginx.yaml)
# ------------------------------------------------------------

locals {
  node_port_min    = 30000
  node_port_max    = 32767
  kube_proxy_hc    = 10256 # LB のヘルスチェックは kube-proxy の /healthz 宛て
  services_cidr_sg = data.oci_core_services.all.services[0].cidr_block
}

# ---- api(K8s API エンドポイント)----

resource "oci_core_security_list" "api" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-sl-api"

  # ワーカーと Cloud Shell(いずれも nodes サブネット)→ kube-apiserver
  ingress_security_rules {
    source      = local.subnet_cidrs.nodes
    protocol    = "6"
    description = "kube-apiserver from nodes / Cloud Shell"
    tcp_options {
      min = 6443
      max = 6443
    }
  }

  # ワーカー → コントロール・プレーン(Control Plane)の通信(OKE 専用ポート)
  ingress_security_rules {
    source      = local.subnet_cidrs.nodes
    protocol    = "6"
    description = "worker to control plane"
    tcp_options {
      min = 12250
      max = 12250
    }
  }

  ingress_security_rules {
    source   = local.subnet_cidrs.nodes
    protocol = "1"
    icmp_options {
      type = 3
      code = 4
    }
  }

  # コントロール・プレーン → ワーカー(kubelet 10250、exec / logs 等)
  egress_security_rules {
    destination = local.subnet_cidrs.nodes
    protocol    = "all"
    description = "control plane to workers"
  }

  # コントロール・プレーン → OKE / OCI の API(サービス・ゲートウェイ(Service Gateway)経由)
  egress_security_rules {
    destination      = local.services_cidr_sg
    destination_type = "SERVICE_CIDR_BLOCK"
    protocol         = "6"
    description      = "to OCI services"
    tcp_options {
      min = 443
      max = 443
    }
  }

  freeform_tags = local.common_tags
}

# ---- nodes(ワーカー・ノード(Worker Node)+ Cloud Shell)----

resource "oci_core_security_list" "nodes" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-sl-nodes"

  # ノード間はすべて許可(flannel のオーバーレイ = UDP 8472 / Pod 間の通信)
  ingress_security_rules {
    source      = local.subnet_cidrs.nodes
    protocol    = "all"
    description = "node to node / pod to pod"
  }

  ingress_security_rules {
    source      = local.subnet_cidrs.api
    protocol    = "6"
    description = "control plane to workers"
  }

  ingress_security_rules {
    source      = local.subnet_cidrs.lb
    protocol    = "6"
    description = "LB to NodePort"
    tcp_options {
      min = local.node_port_min
      max = local.node_port_max
    }
  }

  ingress_security_rules {
    source      = local.subnet_cidrs.lb
    protocol    = "6"
    description = "LB health check to kube-proxy"
    tcp_options {
      min = local.kube_proxy_hc
      max = local.kube_proxy_hc
    }
  }

  ingress_security_rules {
    source   = "0.0.0.0/0"
    protocol = "1"
    icmp_options {
      type = 3
      code = 4
    }
  }

  # エグレス:インターネット(NAT)、OCI サービス(SGW)、API エンドポイントをすべて許可
  egress_security_rules {
    destination = "0.0.0.0/0"
    protocol    = "all"
  }

  freeform_tags = local.common_tags
}

# ---- lb(Service LB)----

resource "oci_core_security_list" "lb" {
  compartment_id = var.compartment_ocid
  vcn_id         = oci_core_vcn.this.id
  display_name   = "${local.name_prefix}-sl-lb"

  ingress_security_rules {
    source      = "${var.my_ip}/32"
    protocol    = "6"
    description = "HTTP from my IP"
    tcp_options {
      min = 80
      max = 80
    }
  }

  egress_security_rules {
    destination = local.subnet_cidrs.nodes
    protocol    = "6"
    description = "LB to NodePort"
    tcp_options {
      min = local.node_port_min
      max = local.node_port_max
    }
  }

  egress_security_rules {
    destination = local.subnet_cidrs.nodes
    protocol    = "6"
    description = "LB health check to kube-proxy"
    tcp_options {
      min = local.kube_proxy_hc
      max = local.kube_proxy_hc
    }
  }

  freeform_tags = local.common_tags
}
06-oke/k8s/nginx.yaml
# nginx ×2 + Service type=LoadBalancer
#   Cloud Shell で実行:kubectl apply -f nginx.yaml
#   削除時:先に kubectl delete -f nginx.yaml で、OKE が作成した LB を OKE 自身に削除させる → その後 terraform destroy
apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx
spec:
  replicas: 2
  selector:
    matchLabels:
      app: nginx
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
        - name: nginx
          image: nginx:1.27 # multi-arch イメージ(Image)のため A1(arm64)でも動作
          ports:
            - containerPort: 80
          resources:
            requests:
              cpu: 50m
              memory: 64Mi
            limits:
              memory: 128Mi
---
apiVersion: v1
kind: Service
metadata:
  name: nginx
  annotations:
    # flexible 10Mbps = Always Free のロード・バランサ(Load Balancer)(未指定の場合はデフォルトで 100Mbps の LB となり、Always Free を超過)
    service.beta.kubernetes.io/oci-load-balancer-shape: "flexible"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-min: "10"
    service.beta.kubernetes.io/oci-load-balancer-shape-flex-max: "10"
    # OKE にセキュリティ・リスト(Security List)を自動変更させない(ルールは Terraform の security.tf に記述済み)
    oci.oraclecloud.com/security-rule-management-mode: "None"
spec:
  type: LoadBalancer
  selector:
    app: nginx
  ports:
    - port: 80
      targetPort: 80