K8s 的網路看起來很複雜,但核心就是四層:Pod 之間可以直接通訊(flat network)、Service 提供穩定入口、Ingress 做 HTTP 路由、NetworkPolicy 做存取控制。搞懂這四層,K8s 的網路就不再神秘了。

先講結論

K8s 網路 = Pod-to-Pod(flat network,任何 Pod 可以直接打到任何 Pod)+ Service(穩定入口,不用追蹤 Pod IP)+ Ingress(HTTP 路由,一個入口分配到多個 Service)+ NetworkPolicy(防火牆,限制誰能打誰)。四種 Service 類型選擇:ClusterIP(叢集內部)、NodePort(開發測試)、LoadBalancer(雲端生產)、ExternalName(對接外部服務)。


K8s 的 Flat Network 模型

K8s 的網路有一個根本假設:所有 Pod 都能直接跟其他 Pod 通訊,不需要 NAT

這代表:

  • Pod A(Node 1)可以直接打 Pod B(Node 2)的 IP
  • 每個 Pod 有自己的 IP(不是共享 Node 的 IP)
  • 不需要做 port mapping

這個假設由 CNI(Container Network Interface)plugin 實現。常見的 CNI:

  • Flannel:最簡單,overlay network,適合學習
  • Calico:支援 NetworkPolicy,適合生產
  • Cilium:基於 eBPF,效能最好,功能最多

不需要自己裝——大部分 K8s 發行版(k3s, EKS, GKE)都內建好了。


四種 Service 類型

Pod 的 IP 會隨著重建改變。Service 提供一個穩定的 DNS name 和 IP,背後自動 load balance 到對應的 Pod。

1. ClusterIP(預設)

只能在叢集內部存取。最常用。

apiVersion: v1
kind: Service
metadata:
  name: api-service
spec:
  type: ClusterIP    # 可省略,這是預設值
  selector:
    app: api-server
  ports:
    - port: 80          # Service 對外的 port
      targetPort: 8080   # Pod 實際的 port
      protocol: TCP

叢集內的其他 Pod 可以用 http://api-servicehttp://api-service.default.svc.cluster.local 來存取。

2. NodePort(開發測試)

在每個 Node 上開一個固定的 port(30000-32767),從外部可以用 NodeIP:NodePort 存取。

apiVersion: v1
kind: Service
metadata:
  name: api-nodeport
spec:
  type: NodePort
  selector:
    app: api-server
  ports:
    - port: 80
      targetPort: 8080
      nodePort: 30080    # 不指定的話 K8s 會隨機分配

實務建議:NodePort 只在開發和測試環境用。生產環境用 LoadBalancer 或 Ingress。因為你不想讓使用者記住 yoursite.com:30080

3. LoadBalancer(雲端生產)

在雲端環境自動建立一個外部 Load Balancer(AWS ALB/NLB、GCP LB)。

apiVersion: v1
kind: Service
metadata:
  name: api-lb
  annotations:
    # AWS 特有:指定 NLB
    service.beta.kubernetes.io/aws-load-balancer-type: nlb
spec:
  type: LoadBalancer
  selector:
    app: api-server
  ports:
    - port: 80
      targetPort: 8080

每個 LoadBalancer Service 會建立一個獨立的 LB,雲端帳單就是這樣爆的。如果你有多個服務,用 Ingress 共享一個 LB 更划算。

4. ExternalName(對接外部服務)

把外部服務映射成叢集內的 Service name。

apiVersion: v1
kind: Service
metadata:
  name: external-db
spec:
  type: ExternalName
  externalName: mydb.rds.amazonaws.com

叢集內的 Pod 可以用 external-db 這個名字來連 RDS,不用在每個 Deployment 裡寫完整的 hostname。哪天換 DB 只要改 Service,不用改所有的 app 設定。


Ingress Controller + Ingress

Ingress 是 HTTP 路由層。一個 Ingress Controller(通常是 nginx 或 traefik)監聽一個 port,根據 hostname 和 path 把流量分配到不同的 Service。

安裝 nginx Ingress Controller

# 用 Helm 安裝(推薦)
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.replicaCount=2

Hostname Routing

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: multi-app-ingress
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "true"
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
        - web.example.com
      secretName: example-tls
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80
    - host: web.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: web-service
                port:
                  number: 80

Path Routing

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: path-routing
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /$2
spec:
  ingressClassName: nginx
  rules:
    - host: app.example.com
      http:
        paths:
          - path: /api(/|$)(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: api-service
                port:
                  number: 80
          - path: /(.*)
            pathType: ImplementationSpecific
            backend:
              service:
                name: web-service
                port:
                  number: 80

cert-manager 自動 TLS

手動管理 TLS 憑證是惡夢。cert-manager 幫你自動從 Let’s Encrypt 申請和續期。

# 安裝 cert-manager
helm repo add jetstack https://charts.jetstack.io
helm install cert-manager jetstack/cert-manager \
  --namespace cert-manager \
  --create-namespace \
  --set crds.enabled=true
# ClusterIssuer — 整個叢集共用
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: admin@example.com
    privateKeySecretRef:
      name: letsencrypt-prod
    solvers:
      - http01:
          ingress:
            class: nginx
---
# Ingress 加上 cert-manager annotation
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: tls-ingress
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.example.com
      secretName: api-example-tls    # cert-manager 自動建立這個 Secret
  rules:
    - host: api.example.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: api-service
                port:
                  number: 80

cert-manager 會自動申請憑證、存到 api-example-tls Secret 裡,到期前自動續期。你什麼都不用做。


CoreDNS:服務發現的核心

K8s 內建 CoreDNS,負責把 Service name 解析成 ClusterIP。DNS 格式:

<service-name>.<namespace>.svc.cluster.local

實際上你通常只需要寫 <service-name>(同 namespace)或 <service-name>.<namespace>(跨 namespace):

# 同 namespace
curl http://api-service
 
# 跨 namespace
curl http://api-service.production
 
# 完整 FQDN
curl http://api-service.production.svc.cluster.local

Headless Service 的 DNS

StatefulSet 搭配 Headless Service(clusterIP: None),每個 Pod 有自己的 DNS record:

postgres-0.postgres-svc.default.svc.cluster.local
postgres-1.postgres-svc.default.svc.cluster.local

這就是為什麼 StatefulSet 需要 Headless Service——你的 app 可以明確連到 primary(postgres-0)或 replica(postgres-1)。


NetworkPolicy:叢集內的防火牆

K8s 預設所有 Pod 可以互相通訊。這很方便,但在生產環境你不希望前端 Pod 直接打資料庫。NetworkPolicy 就是 K8s 的防火牆。

前提:你的 CNI 要支援 NetworkPolicy。Calico 和 Cilium 支援,Flannel 不支援。

只允許特定 namespace 存取 DB

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: db-access-policy
  namespace: database
spec:
  podSelector:
    matchLabels:
      app: postgres
  policyTypes:
    - Ingress
  ingress:
    - from:
        # 只允許 backend namespace 的 Pod 連進來
        - namespaceSelector:
            matchLabels:
              name: backend
          podSelector:
            matchLabels:
              role: api
      ports:
        - protocol: TCP
          port: 5432

預設拒絕所有流量

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: default-deny-all
  namespace: production
spec:
  podSelector: {}    # 套用到 namespace 內所有 Pod
  policyTypes:
    - Ingress
    - Egress

先全部擋住,再逐一開放需要的路徑。這是 zero-trust 的做法。但注意:這也會擋住 DNS(CoreDNS 在 kube-system namespace),所以你還需要:

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-dns
  namespace: production
spec:
  podSelector: {}
  policyTypes:
    - Egress
  egress:
    - to:
        - namespaceSelector:
            matchLabels:
              kubernetes.io/metadata.name: kube-system
      ports:
        - protocol: UDP
          port: 53
        - protocol: TCP
          port: 53

常見踩坑

Service 連不到 Pod:selector 的 label 跟 Pod 的 label 對不上。用 kubectl get endpoints <service-name> 確認 endpoint list 不是空的。

Ingress 設了但外面打不進來:Ingress Controller 沒裝、或 Ingress 的 ingressClassName 打錯。用 kubectl get ingressclass 看有什麼。

跨 namespace 呼叫失敗:沒用 <service>.<namespace> 格式。只寫 api-service 只能解析同 namespace 的 Service。

NetworkPolicy 一加就全斷:default-deny 忘記放行 DNS。Pod 連 Service name 都解析不了,當然什麼都打不通。


K8s 的網路不難,但踩坑成本很高。花一小時把 Service 類型和 Ingress 搞懂,省下十小時 debug。


延伸閱讀


本系列文章