跳转至

OKE 的设计和实践

一句话

Oracle Kubernetes Engine(OKE)= 标准的 Kubernetes + 一层 OCI 接入(LB、Block Volume、IAM)。 设计的核心:只有 LB 对外、API endpoint 私有、Security List 只由 Terraform 管;运维的核心:分清三层(Terraform / K8s 对象 / K8s 间接建的 OCI 资源),删除时从上往下删。

本页整理自 Phase 6 和 Phase 11(第 8 课和第 2 部分的实测)。K8s 本身的原理见 Kubernetes 原理,VCN / Security List 的基础见 网络设计。

关于「待确认」

标"待确认"的项目还没有实测,是按文档或推测写的。

1. 先认识这些组件

OKE AWS 一句话
OKE cluster EKS cluster 托管的 K8s。control plane 建在 Oracle 自己的 tenancy 里,用户看不到
BASIC_CLUSTER / ENHANCED_CLUSTER — Basic:control plane 免费、无 SLA;Enhanced:按小时收费(SLA、add-on 管理、virtual nodes 等)
node pool(managed nodes) Managed node group 一组 worker node(VM)。OKE 负责建 VM 和注册
virtual nodes Fargate 没有 VM 的概念,按 Pod 收费,要 Enhanced
API endpoint 同左 apiserver 的入口,VCN 里只出现它的一个 IP
oci ce cluster generate-token aws eks get-token kubeconfig 的认证:用 OCI IAM 换 token

谁管什么

部分 谁管 说明
control plane(apiserver、etcd、scheduler、CCM) Oracle 备份、升级由 Oracle 做
VCN / subnet / Security List 自己 规则写错了 → node 注册不进去、LB 连不通
worker node 自己(轻量) shape、台数、镜像。kubelet 等由 OKE 预装;OS 补丁和 K8s 升级要自己发起(换镜像 → 逐台替换)
应用 自己 kubectl。LB / Block Volume 由 K8s 间接建

worker node 的思路和 VM 不一样了:nginx 跑在容器里,VM 只是跑容器的地方,管的是数量和版本,坏了就换,不去修。

2. 原理:三层结构

同一个 nginx 网站跨越三层。出了问题,先分清是哪一层:

flowchart TB
  subgraph L1["① 云基础设施:Terraform"]
    VCN["VCN / subnet<br/>Security List"]
    CL["OKE cluster<br/>node pool"]
  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["Block Volume<br/>PVC → CSI 建"]
  end
  SVC -->|"CCM"| LB
  L1 --> L2
层 管理什么 谁负责
① 云基础设施 VCN、subnet、cluster、node pool Terraform(06-oke/)
② K8s 对象 Deployment、Pod、Service kubectl 提交期望;controller 建对象、scheduler 选 node、kubelet 启动容器
③ 间接建的云资源 Service 的 OCI LB、PVC 的 Block Volume cloud-controller-manager(CCM)/ CSI;Terraform 不知道它们

由此得出两条规则:

  • 删除从上往下:先删 ② 的 Service / PVC,确认 ③ 的 LB / Block Volume 没了,再 destroy ①(第 8 节)
  • 第 ③ 层会去改第 ① 层的东西:CCM 默认会往 Security List 里加规则 → 和 Terraform 打架(第 4 节)

3. 网络设计

VCN 10.6.0.0/16
 ├─ api   10.6.0.0/28   private  API endpoint(只要几个 IP → /28)
 ├─ nodes 10.6.10.0/24  private  worker node(+ 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

只有 lb 是 public:从 Internet 能直接到的只有 LB,node 和 API endpoint 都没有公网 IP(和 网络设计 同样的思路)。nodes 的路由:0.0.0.0/0 → NAT Gateway(拉 Docker Hub 的镜像),Oracle 服务 → Service Gateway(向 OKE 注册等)。

每个 subnet 一个 Security List

三个 subnet 的角色不同,要的规则也不同 → 没有复用 Phase 4 的 network module(它所有 subnet 共用一个 Security List),每个 subnet 单独写。

方向 端口 为什么
本机 → lb TCP 80 网站入口(只允许本机 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)、worker → control plane(12250,proxymux 的通道,kubectl logs/exec 走这里)
api → nodes TCP 全部 control plane → kubelet(10250 等)
nodes ↔ nodes 全部 flannel 的 VXLAN(UDP 8472)、Pod 之间的通信
# 06-oke/security.tf(摘录):nodes subnet 的入站规则
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 只在某一台上)。

API endpoint 私有,kubectl 在 Cloud Shell 里用

# 06-oke/oke.tf(摘录)
endpoint_config {
  subnet_id            = oci_core_subnet.api.id
  is_public_ip_enabled = false          # 不分配公网 IP → 只能从 VCN 里访问
}
  • 能操作整个集群的入口不放到公网
  • 如果用公网 endpoint:Cloud Shell 的出口 IP 不固定 → 只能对 0.0.0.0/0 开 6443(和 SSH 是同一个问题)
  • 代价:kubectl 只能在 VCN 里用 → Cloud Shell 的 private network 接进 nodes subnet。本机也装不了 OCI CLI(kubeconfig 的认证要用 oci 命令),而 Cloud Shell 自带 oci 和 kubectl

4. 不让 OKE 自动改 Security List

CCM 建 LB 时,默认会自己往 Security List 里加规则 → Terraform 管理的 Security List 每次 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 List(规则已经在 security.tf 里写好了)
    oci.oraclecloud.com/security-rule-management-mode: "None"

结果:建了 LB 之后 terraform plan 还是 No changes。

原则:一份资源只让一个工具管。 同一个 Security List 被 Terraform 和 CCM 同时改,两边会一直互相覆盖。

5. node 镜像自动选择

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 用 SDK 查了 Service type=LoadBalancer 建出来的 LB:

项目 实测
shape flexible,min = max = 10Mbps(annotation 生效 = Always Free)
listener 名字 TCP-80,协议 TCP(L4)。LB 产品支持 L7,但这里当 L4 用
backend 2 台 node 的 NodePort,策略 ROUND_ROBIN
健康检查 HTTP :10256/healthz(kube-proxy),不是业务的 80 端口
名字 / 标签 名字是 UUID,没有标签,不在 Terraform 的 state 里 → Console 上很难看出是谁建的
node 的 fault domain 没指定 FD,两台分别在 FD-1 / FD-3。只观察了 1 次,不能当作每次都保证分散

流量:本机 → 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 失败重试 → EnsuredLoadBalancer。

Pod 网络:flannel 给每个 node 分一段

  • 普通 Pod 的 IP 是 10.244.x.x;flannel、kube-proxy、CSI 等系统 Pod 是 hostNetwork,直接用 node 的 IP
  • 每个 node 分到 Pod CIDR 的一段:Phase 6 是 /24(一台 10.244.1.x、另一台 10.244.0.x),Phase 11 是 /25(10.244.0.0/25 和 10.244.0.128/25)
  • 为什么变了还没确认(推测和每个 node 的 maxPods 设定有关)

7. A1 容量不足和 node pool

Phase 11 的 apply:cluster 建好了,node pool 报 Out of host capacity(免费的 A1 资源暂时不够)。

  • OKE 会自动删掉失败的 node pool,不留 VM;再 plan 就只剩 node pool 1 to add
  • 错误详情用 ContainerEngineClient.list_work_request_errors 查
  • 做法:确认 plan 正好是 node pool 1 to add 才 apply,第一次和立刻重试都失败,之后每 10 分钟重试一次,第 3 次(约 25 分钟后)成功;cluster 和网络在等待期间不收费
  • Terraform 显示 node pool "Creation complete" 时,node 不一定 ready → 用 SDK 的 get_node_pool().nodes 确认每台都是 ACTIVE

8. 删除顺序

从上往下删

  1. kubectl delete -f nginx.yaml(和 PVC 等)→ 让 K8s 自己删掉它建的 LB / Block Volume
  2. 用 SDK / Console 确认 LB 已经没了
  3. Cloud Shell 的 Network 切回 Public(private network 的网卡挂在 nodes subnet 上)
  4. terraform destroy

顺序反了会怎样:

  • 忘了 kubectl delete:Terraform 删到 lb subnet 时,LB 还占着它 → 删不掉;LB 成了 Terraform 不管的孤立资源,继续占额度(第 8 课的分析)
  • 忘了切回 Public:Phase 6 实际碰到了,nodes subnet 删了 8 分 41 秒。关掉 Cloud Shell 约 95 秒后网卡才释放,Terraform 一直重试才删掉。用 list_private_ips(subnet_id=...) 能查到是谁占着

9. 设计决策和 Always Free 的取舍

决策 选了 没选 理由
cluster 类型 BASIC_CLUSTER ENHANCED_CLUSTER control plane 免费;学习用不需要 SLA
node A1 1 OCPU / 6GB × 2,boot 50GB × 2 更大 / 更多 Always Free(A1 4 OCPU / 24GB、Block Volume 200GB)里留出余量 → PVC 还能建 2 个
AD / FD 大阪只有 1 个 AD;FD 不指定 指定 FD 实测这次分到了不同 FD(1 次观察)
Pod 网络(CNI) flannel(overlay) VCN-Native 简单、不占 VCN 的 IP;小 node 也不受 VNIC 数限制。代价:NetworkPolicy 不生效、LB 不能直接转给 Pod
安全规则 每个 subnet 一个 Security List 共用一个 / NSG OKE 的规则按 subnet 的角色天然分开
API endpoint 私有 + Cloud Shell private network 公网 endpoint 不把集群的操作入口暴露在 0.0.0.0/0
Security List 的管理 Terraform 独占,annotation None 让 CCM 自动加规则 避免 drift 和互相覆盖
Service LB flexible 10Mbps(annotation) 默认 shape Always Free 只有这个
node 镜像 data source 按名字筛选最新 写死 OCID 版本更新时代码不用改
镜像仓库 Docker Hub(经 NAT Gateway) OCIR(经 Service Gateway) 用的是官方 nginx 多架构镜像

10. 验证

要确认的事 怎么做 结果
node 真的 ready SDK get_node_pool().nodes 2 台 ACTIVE;kubectl get nodes 2 台 Ready(只有 worker,看不到 control plane)
网站能访问 本机浏览器 → LB:80 Welcome to nginx!
LB 是免费的规格 SDK 查 LB flexible 10 / 10Mbps
CCM 没改 Security List 建 LB 后 terraform plan No changes
删干净了 destroy 后用 SDK 查 compartment VCN / LB / 实例 / boot volume / block volume 都没有了
oci-bv 的 reclaimPolicy kubectl get storageclass 待确认(应该是 Delete)
tenancy 管理员是否自动 cluster-admin — 待确认
Ingress 用哪个实现 — 待确认(OCI Native Ingress Controller / Gateway API)

11. 坑

现象 原因 对策
node pool 失败:Out of host capacity 免费的 A1 资源暂时不够 确认 plan 只剩 node pool 再重试
node pool "complete" 了,node 还没 ready Terraform 只等 API 返回 用 SDK 确认 node 状态
kubectl dial tcp 10.6.0.x:6443: i/o timeout Cloud Shell 没接 private network 切到接 nodes subnet 的 private network
private network 连不上 以前的定义指向已删除的 subnet 新建 private network 的定义
建了 LB 后 Terraform 有 drift CCM 往 Security List 里加规则 提前放行 + annotation None
terraform destroy 删不掉 subnet LB 不在 state 里 / Cloud Shell 的网卡还在 先 kubectl delete;Network 切回 Public
kubectl describe pod 没有 Events 节 原因未确认(kubectl 1.36.4) kubectl events --for pod/<name>
Events 过一会儿就没了 Event 默认只保留 1 小时 要看就趁早

12. 自测问题

1. OKE 的 control plane 在哪?为什么 kubectl get nodes 看不到它?

在 Oracle 自己的 tenancy 里,由 Oracle 管理。get nodes 列的是 kubelet 主动报到的 Node 对象,control plane 那边没有 kubelet 来报到。

2. 忘了 kubectl delete 就直接 terraform destroy,会发生什么?

Terraform 不知道 CCM 建的 LB。删到 lb subnet 时 LB 还占着它,subnet 删不掉;LB 成为孤立资源继续占额度。要先 kubectl delete,确认 LB 没了再 destroy。

3. 为什么 manifest 里要把 security-rule-management-mode 设成「None」?

CCM 默认会往 Security List 里加 LB 用的规则,而这个 Security List 是 Terraform 管的 → 每次 plan 都有 drift,两边互相覆盖。所以端口在 Terraform 里提前放行,让 CCM 不要动。

4. Service LB 的健康检查为什么打 node 的 10256,而不是 80?

LB 的后端是 node 的 NodePort,不是 Pod。10256 是 kube-proxy 的 /healthz,只有 kube-proxy 知道这个 node 能不能处理这个 Service 的流量。

5. 把 nodes ↔ nodes 的全部放行删掉,kubectl get nodes 会报错吗?会出现什么现象?

不会报错,node 都是 Ready。但 flannel 的 VXLAN 被挡,跨 node 的 Pod 之间不通:网站时好时坏,有的 Pod 名字解析不了。

6. API endpoint 用公网的会简单很多,为什么不这么做?

能操作整个集群的入口会暴露出来;而且 Cloud Shell 的出口 IP 不固定,只能对 0.0.0.0/0 开 6443。私有 endpoint + Cloud Shell private network 更安全。

7. 选 flannel 而不是 VCN-Native,代价是什么?

VCN 看不到 Pod:不能按 Pod 设 NSG、LB 不能直接转给 Pod(多跳一次 node);而且 flannel 不实现 NetworkPolicy。换来的是简单、不占 VCN 的 IP。

8. 两台 node 这次分到了 FD-1 和 FD-3,能说明 OKE 一定会分散吗?

不能,只观察了 1 次。需要保证的话要明确配置,Pod 层面也要写 topologySpreadConstraints。

完整代码

06-oke/oke.tf(cluster + node pool)
# ------------------------------------------------------------
# OKE:cluster(control plane)+ node pool(worker node)
#   费用:BASIC_CLUSTER 的 control plane 免费;ENHANCED_CLUSTER 按小时收费 → 不用
#         worker = A1.Flex(Always Free 额度内);Service LB 用 annotation 指定 flexible 10Mbps
# ------------------------------------------------------------

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

# node 用的镜像: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 endpoint 放在 private subnet,不分配公网 IP → 只能从 VCN 里访问(Cloud Shell private network)
  endpoint_config {
    subnet_id            = oci_core_subnet.api.id
    is_public_ip_enabled = false
  }

  options {
    # Service type=LoadBalancer 的 LB 建在这个 subnet
    service_lb_subnet_ids = [oci_core_subnet.lb.id]

    kubernetes_network_config {
      pods_cidr     = "10.244.0.0/16" # flannel 的 overlay,和 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

    # 大阪只有 1 个 AD
    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 / subnet / 路由)
# ------------------------------------------------------------
# Phase 6:OKE 用的网络(flannel overlay 版)
#   VCN 10.6.0.0/16 ─ IGW / NAT GW / Service GW
#    ├─ api   10.6.0.0/28  private:K8s API endpoint(没有公网 IP)
#    ├─ nodes 10.6.10.0/24 private:worker node(+ Cloud Shell 的 private network 也接这里)
#    └─ lb    10.6.20.0/24 public :Service type=LoadBalancer 建的 OCI LB
#   Pod 的 IP(10.244.0.0/16)由 flannel 在节点之间做 overlay,不占 VCN 的 IP
#
# 04 的 network module 是"所有 Subnet 共用一个 Security List",
# OKE 每个 Subnet 要的规则都不一样 → 这里不用 module,直接写资源(Security List 在 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
}

# worker node 拉 Docker Hub 等外部镜像要用
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
  }
}

# worker node 向 OKE 注册、拉 OCIR 镜像,以及 Cloud Shell 拿 token(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(每个 subnet 的 Security List)
# ------------------------------------------------------------
# Security List:每个 Subnet 一个(规则参考 OKE 文档的 flannel 示例,按 Always Free 实验需要精简)
#
#   my_ip ──:80──→ [lb] LB ──:30000-32767 / :10256──→ [nodes] worker ×2
#   [nodes] worker / Cloud Shell ──:6443 / :12250──→ [api] K8s API endpoint
#   [api] control plane ──all──→ [nodes](kubelet 10250 等)
#
# Service LB 的规则不让 OKE 自动改:
#   cloud-controller-manager 默认会自己往 Security List 里加规则 → Terraform 管的 SL 每次 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 endpoint)----

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"

  # worker 和 Cloud Shell(都在 nodes subnet)→ 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
    }
  }

  # worker → 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
    }
  }

  # control plane → worker(kubelet 10250、exec / logs 等)
  egress_security_rules {
    destination = local.subnet_cidrs.nodes
    protocol    = "all"
    description = "control plane to workers"
  }

  # control plane → OKE / OCI 的 API(走 Service GW)
  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 的 overlay = 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
    }
  }

  # 出:Internet(NAT)、OCI 服务(SGW)、API endpoint 全部放行
  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)→ 再 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 镜像,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 的 LB(不写的话默认是 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