Phase 6:OKE(Kubernetes)¶
目标¶
- 用 Terraform 建 OKE cluster,在上面跑 nginx 并通过 LB 公开
- 理解托管 Kubernetes 里"谁管什么"
- 体验 Kubernetes 的自愈、扩容、drain
OKE 是什么¶
标准的 upstream Kubernetes(CNCF 认证),kubectl / Helm / manifest 的用法和别的环境一样。≒ AWS EKS、Azure AKS、Google GKE。
和 OCI 绑定的只是"接入层":
| K8s 的对象 / 功能 | OCI 上发生的事 |
|---|---|
Service type: LoadBalancer |
cloud-controller-manager 建一个 OCI LB |
PersistentVolumeClaim |
CSI driver 建一个 OCI Block Volume |
| kubeconfig 的认证 | oci ce cluster generate-token(OCI IAM,≒ aws eks get-token) |
| Pod 网络 | flannel(overlay)或 VCN-Native(Pod 直接拿 VCN 的 IP) |
谁管什么¶
| 部分 | 谁管 | 说明 |
|---|---|---|
| control plane(apiserver / etcd / scheduler 等) | Oracle | 建在 Oracle 自己的 tenancy 里,用户看不到这些 VM,VCN 里只出现 API endpoint 的一个 IP |
| VCN / subnet / Security List | 自己 | 规则写错了 → node 注册不进去、LB 连不通 |
| worker node | 自己(轻量) | shape、台数、镜像。OS 补丁和 K8s 升级要自己发起(换镜像 → 节点逐台替换)。kubelet 等由 OKE 预装 |
| 应用 | 自己 | kubectl。LB / Block Volume 是 K8s 间接建的,不在 Terraform 的 state 里 |
| cluster 类型 | control plane | 备注 |
|---|---|---|
BASIC_CLUSTER |
免费,无 SLA | 这次用这个 |
ENHANCED_CLUSTER |
按小时收费 | SLA、add-on 管理、virtual nodes 等 |
| node 类型 | 说明 |
|---|---|
| Managed nodes | 用户管 node pool,OKE 负责建 VM 和注册(这次用这个) |
| Virtual nodes | 全托管,没有 VM 的概念(≒ AWS Fargate),按 Pod 收费,需要 Enhanced |
| Self-managed nodes | 自己建 VM 再加入 cluster |
worker node 的思路变了
Phase 2〜4 里 nginx 是用 cloud-init / Ansible 装在 VM 上的。现在 nginx 跑在容器里,VM 只是跑容器的地方: 管的是它的数量和版本,不管里面装了什么;坏了就换一台,不去修。
架构¶
flowchart LR
PC[本机 my_ip] -->|:80| LB
CS[Cloud Shell<br/>private network] -->|kubectl :6443| API
subgraph OCI_ORACLE["Oracle 的 tenancy(看不到)"]
CP[control plane]
end
subgraph VCN["VCN 10.6.0.0/16"]
subgraph SAPI["api 10.6.0.0/28 private"]
API[K8s API endpoint]
end
subgraph SNODES["nodes 10.6.10.0/24 private"]
N1[worker A1 #1]
N2[worker A1 #2]
end
subgraph SLB["lb 10.6.20.0/24 public"]
LB[Service LB<br/>flexible 10Mbps]
end
end
CP --- API
API --> N1 & N2
LB -->|NodePort| N1 & N2
- Pod CIDR
10.244.0.0/16(flannel 的 overlay,不占 VCN 的 IP)/ Service CIDR10.96.0.0/16 - worker:A1.Flex 1 OCPU / 6GB ×2,boot volume 50GB ×2 → 在 Always Free 范围内
设计上的选择¶
API endpoint 用私有的,kubectl 在 Cloud Shell 里执行¶
- 本机装不了 OCI CLI(Phase 5b 的路径长度问题),而 kubeconfig 的认证要用
oci命令 - Cloud Shell 自带
oci和kubectl - 如果用公网 endpoint:Cloud Shell 的出口 IP 不固定 → 只能对
0.0.0.0/0开放 6443(和 Phase 2 的 SSH 是同一个问题) - → API endpoint 放在 private subnet,Cloud Shell 用 private network 接进
nodessubnet,6443 只对 nodes 的 CIDR 开放
每个 subnet 一个 Security List¶
Phase 4 的 network module 是所有 subnet 共用一个 Security List,OKE 每个 subnet 要的规则都不一样 → 这次不用 module,直接写资源。
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 等)
不让 OKE 自动改 Security List¶
cloud-controller-manager 默认会自己往 Security List 里加 LB 用的规则 → Terraform 管理的 Security List 每次 plan 都会出 drift。
→ NodePort 范围(30000-32767)和健康检查端口(10256)在 Terraform 里提前放行,manifest 里关掉自动管理:
metadata:
annotations:
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"
oci.oraclecloud.com/security-rule-management-mode: "None"
结果:建了 LB 之后 terraform plan 还是 No changes ✅
node 镜像自动选择¶
oci_containerengine_node_pool_option 的 sources 里按名字筛选: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]
操作步骤¶
1. terraform apply¶
14 added。cluster 约 7 分钟,node pool 约 3 分钟。
Note
Terraform 显示 node pool "Creation complete" 时,节点不一定 ready。用 SDK 的 get_node_pool().nodes 确认每个节点都是 ACTIVE。
2. Cloud Shell 接入 VCN,生成 kubeconfig¶
Cloud Shell 的 Network → Ephemeral private network setup → VCN tflab6-vcn / Subnet tflab6-subnet-nodes
oci ce cluster create-kubeconfig --cluster-id <cluster OCID> \
--file $HOME/.kube/config --region ap-osaka-1 \
--token-version 2.0.0 --kube-endpoint PRIVATE_ENDPOINT
kubectl get nodes -o wide
NAME STATUS ROLES VERSION OS-IMAGE CONTAINER-RUNTIME
10.6.10.228 Ready node v1.36.4 Oracle Linux Server 9.8 cri-o://1.36.3
10.6.10.92 Ready node v1.36.4 Oracle Linux Server 9.8 cri-o://1.36.3
- 只有 worker,没有 control-plane 节点 → master 是托管的,看不到
- 节点名就是私有 IP;arm64
- 容器运行时是 CRI-O(不是 Docker / containerd)。K8s 只通过 CRI 接口调用,对 manifest 没有影响
3. 部署 nginx + Service LB¶
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S)
nginx LoadBalancer 10.96.189.195 203.0.113.130 80:32248/TCP
- 本机访问
http://<EXTERNAL-IP>→Welcome to nginx! 32248是 NodePort。流量:本机 → LB:80 → node:32248 → kube-proxy → Pod:80(可能转到另一台 node 上的 Pod)- 用 SDK 确认 LB 是
flexible10/10 Mbps(annotation 生效) - 这个 LB 的名字是 UUID,没有标签,也不在 Terraform 的 state 里 → Console 上很难看出是谁建的
Kubernetes 实验¶
自愈¶
- 马上出现一个新 Pod:Deployment 的期望是
replicas: 2,ReplicaSet 发现少了就补(reconcile loop) - 新 Pod 的名字和 IP 都变了 → 不能直接用 Pod IP,要通过 Service 访问
- Terraform 只在你执行 apply 时修正 drift,K8s 是一直在后台自动修正
flannel 的 IP 分配¶
一台节点上的 Pod 都是 10.244.1.x,另一台都是 10.244.0.x → flannel 给每个节点分一个 /24。
扩容¶
scheduler 自动分成 2 + 2,分散到两台节点。
drain(模拟节点维护)¶
- 节点变成
Ready,SchedulingDisabled,上面的 nginx(和coredns)被驱逐,在另一台上重建 - 这就是 node pool 升级时 OKE 自动做的事:cordon → drain → 换节点
drain 时列出的 DaemonSet = OKE 在每个节点上放的组件:
| DaemonSet | 作用 |
|---|---|
kube-proxy |
Service / NodePort 的转发 |
kube-flannel-ds |
Pod 网络(overlay) |
csi-oci-node |
挂载 OCI Block Volume |
proxymux-client |
control plane → 节点的通道,kubectl logs/exec 走这里(Security List 的 12250) |
oke-node-problem-detector |
检测节点异常 |
oke-dataplane-observability-agent |
监控 agent |
uncordon 之后¶
- Pod 不会自动迁回:K8s 只在建新 Pod 时选节点,不会主动重新分配
- scale 回 2 时删掉的是新建的那两个 → 结果 2 个 Pod 都在同一台节点上 = 没有冗余
- 正式环境的对策:
topologySpreadConstraints/podAntiAffinity:强制把 Pod 分散到不同节点PodDisruptionBudget:drain 时保证至少有 N 个 Pod 在运行- 已经偏了的话:
kubectl rollout restart deployment nginx重新分配
清理¶
顺序很重要
kubectl delete -f nginx.yaml→ 让 OKE 自己删掉它建的 LB- 确认 LB 已经没了
- Cloud Shell 的 Network 切回 Public
terraform destroy
顺序反了的话,LB 会留下来继续占用(甚至收费),VCN 也删不掉。
坑¶
nodes subnet 删了 8 分 41 秒
subnet 里还剩一个 IP,名字是 UUID → 是 Cloud Shell private network 的 VNIC。 关掉 Cloud Shell 窗口后约 95 秒才释放,Terraform 遇到冲突会一直重试,所以最后自己删掉了。
- 预防:destroy 之前先把 Cloud Shell 的 Network 切回
Public - 排查:
list_private_ips(subnet_id=...)看 subnet 里还剩谁
kubectl delete pod 10.6.10.228 → NotFound
那是节点名。Pod 名是 nginx-598968dd58-xxxxx 这种格式。
完整代码¶
06-oke/oke.tf
# ------------------------------------------------------------
# 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/security.tf
# ------------------------------------------------------------
# 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/network.tf
# ------------------------------------------------------------
# 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/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