跳转至

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 CIDR 10.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 接进 nodes subnet,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

kubectl apply -f nginx.yaml
kubectl get svc nginx -w
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 是 flexible 10/10 Mbps(annotation 生效)
  • 这个 LB 的名字是 UUID,没有标签,也不在 Terraform 的 state 里 → Console 上很难看出是谁建的

Kubernetes 实验

自愈

kubectl delete pod <Pod 名>
kubectl get pods -o wide
  • 马上出现一个新 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。

扩容

kubectl scale deployment nginx --replicas=4

scheduler 自动分成 2 + 2,分散到两台节点。

drain(模拟节点维护)

kubectl drain <节点名> --ignore-daemonsets --delete-emptydir-data
  • 节点变成 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 之后

kubectl uncordon <节点名>
kubectl scale deployment nginx --replicas=2
  • Pod 不会自动迁回:K8s 只在建新 Pod 时选节点,不会主动重新分配
  • scale 回 2 时删掉的是新建的那两个 → 结果 2 个 Pod 都在同一台节点上 = 没有冗余
  • 正式环境的对策:
    • topologySpreadConstraints / podAntiAffinity:强制把 Pod 分散到不同节点
    • PodDisruptionBudget:drain 时保证至少有 N 个 Pod 在运行
    • 已经偏了的话:kubectl rollout restart deployment nginx 重新分配

清理

顺序很重要

  1. kubectl delete -f nginx.yaml → 让 OKE 自己删掉它建的 LB
  2. 确认 LB 已经没了
  3. Cloud Shell 的 Network 切回 Public
  4. 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