【K8s硬核实战】打破网络边界:构建安全、自维护的节点级边缘代理网络

摘要:在混合云或边缘计算场景中,Kubernetes 节点往往分布在不同的物理局域网内。如何让集群内的 Pod 安全地通过特定节点访问该节点所在局域网的资源(如打印机、数据库),同时又不暴露宿主机端口?本文将介绍一种利用 DaemonSet + InitContainer 实现“服务自注册”的自动化方案,打造一套零侵入、高安全的集群内边缘代理系统。

1. 场景与痛点

假设我们维护着一个特殊的 Kubernetes 集群,其节点分散在多个不同的物理网络中(例如:节点 A 在北京办公室内网,节点 B 在上海机房内网)。

需求
我们需要在集群内部署 HTTP 代理,使得集群内的任意 Pod 可以通过指定“走节点 A 的代理”,来访问北京内网的设备;或者“走节点 B 的代理”,访问上海内网的服务器。

核心约束

  1. 安全性:绝不允许使用 NodePorthostNetwork 占用宿主机端口,代理服务必须对外隐藏,仅限集群内部(Pod IP 网络)访问。
  2. 自动化:节点增删频繁,手动为每个节点创建 Service 是不可接受的,必须自动管理。
  3. 定向路由:K8s 默认的负载均衡无法满足“指定通过某台物理机出网”的需求,我们需要一种机制精确控制流量出口。

2. 架构设计思路

为了解决上述问题,我们采用了一种 “Self-Hosted Operator(自托管运维)” 的设计模式:

  1. 代理层(DaemonSet)
    在每个节点部署轻量级 HTTP 代理(Tinyproxy),网络模式设为普通 Pod 模式(hostNetwork: false)。这样代理仅监听 Pod IP,外部网络无法直接扫描和连接,天然隔离。

  2. 自动注册(Auto-Registration)
    利用 DaemonSet 的 initContainer 和 K8s Downward API。Pod 启动前,先调用 kubectl 给自己打上当前节点名称的标签,并自动创建一个指向该节点的 ClusterIP Service(例如 nodeproxy-access-node-01)。

  3. 垃圾回收(Garbage Collection)
    部署一个 CronJob,定期比对当前的 Service 列表和活着的 Node 列表,自动清理掉已下线节点残留的僵尸 Service。

3. 完整落地方案 (YAML)

以下是经过验证的完整配置清单。该配置将所有资源部署在 project-nodeproxy 命名空间下,实现了权限控制、代理部署、服务注册及自动清理的全闭环。

文件名:node-proxy-stack.yaml

# ==========================================
# 1. 基础环境与命名空间
# ==========================================
apiVersion: v1
kind: Namespace
metadata:
  name: project-nodeproxy

---
# ==========================================
# 2. RBAC 权限设计
# 说明:赋予 Pod 自身修改标签、管理 Service 以及读取 Node 信息的权限
# ==========================================
apiVersion: v1
kind: ServiceAccount
metadata:
  name: nodeproxy-manager
  namespace: project-nodeproxy
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRole
metadata:
  name: nodeproxy-role
rules:
  - apiGroups: [""]
    resources: ["pods"]
    verbs: ["get", "patch", "list"]
  - apiGroups: [""]
    resources: ["services"]
    verbs: ["get", "create", "patch", "update", "delete", "list"]
  - apiGroups: [""]
    resources: ["nodes"]
    verbs: ["get", "list"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
  name: nodeproxy-rb
subjects:
  - kind: ServiceAccount
    name: nodeproxy-manager
    namespace: project-nodeproxy
roleRef:
  kind: ClusterRole
  name: nodeproxy-role
  apiGroup: rbac.authorization.k8s.io

---
# ==========================================
# 3. 核心代理与自动注册 (DaemonSet)
# ==========================================
apiVersion: apps/v1
kind: DaemonSet
metadata:
  name: nodeproxy-agent
  namespace: project-nodeproxy
  labels:
    app: nodeproxy-agent
spec:
  selector:
    matchLabels:
      app: nodeproxy-agent
  template:
    metadata:
      labels:
        app: nodeproxy-agent
    spec:
      serviceAccountName: nodeproxy-manager
      hostNetwork: false # 核心安全配置:不占用宿主机端口
      
      containers:
        # --- 业务容器:Tinyproxy ---
        - name: proxy
          image: dannydirect/tinyproxy:latest
          # 允许任意来源连接(由于 hostNetwork=false,实际仅限集群内容器 IP 连接,安全可控)
          args: ["ANY"]
          ports:
            - containerPort: 8888
              name: http

      initContainers:
        # --- 初始化容器:实现服务自动注册 ---
        - name: auto-registrar
          image: bitnami/kubectl:latest
          env:
            - name: NODE_NAME
              valueFrom:
                fieldRef:
                  fieldPath: spec.nodeName
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
          command:
            - /bin/sh
            - -c
            - |
              set -e
              # 转换节点名为小写,适配 K8s Service 命名规范
              NODE_NAME_LOWER=$(echo "$NODE_NAME" | tr '[:upper:]' '[:lower:]')
              
              echo ">>> Starting registration for node: $NODE_NAME"

              # 1. 为 Pod 打上 target-node 标签,用于 Service Selector 锚定
              echo ">>> Step 1: Patching Pod label..."
              kubectl patch pod $POD_NAME -n project-nodeproxy --type='json' \
                -p='[{"op": "add", "path": "/metadata/labels/target-node", "value": "'"$NODE_NAME"'"}]'

              # 2. 幂等地创建或更新 Service
              SERVICE_NAME="nodeproxy-access-${NODE_NAME_LOWER}"
              echo ">>> Step 2: Creating/Updating Service: $SERVICE_NAME"
              
              cat <<EOF | kubectl apply -f -
              apiVersion: v1
              kind: Service
              metadata:
                name: ${SERVICE_NAME}
                namespace: project-nodeproxy
                labels:
                  app: nodeproxy-system
                  type: access-point
                  managed-by: nodeproxy-daemonset
              spec:
                type: ClusterIP
                selector:
                  app: nodeproxy-agent
                  target-node: ${NODE_NAME}
                ports:
                - port: 8888
                  targetPort: 8888
                  protocol: TCP
              EOF

              echo ">>> Registration complete."

---
# ==========================================
# 4. 自动化运维:过期服务清理 (CronJob)
# ==========================================
apiVersion: batch/v1
kind: CronJob
metadata:
  name: nodeproxy-cleaner
  namespace: project-nodeproxy
spec:
  schedule: "0 3 * * 1" # 每周一凌晨 3:00 执行
  successfulJobsHistoryLimit: 1
  failedJobsHistoryLimit: 1
  jobTemplate:
    spec:
      template:
        spec:
          serviceAccountName: nodeproxy-manager
          restartPolicy: OnFailure
          containers:
            - name: cleaner
              image: bitnami/kubectl:latest
              command:
                - /bin/sh
                - -c
                - |
                  echo ">>> Starting cleanup task..."
                  
                  # 获取所有受管理的 Service
                  SERVICES=$(kubectl get svc -n project-nodeproxy -l managed-by=nodeproxy-daemonset -o jsonpath='{.items[*].metadata.name}')
                  
                  # 获取当前存活节点列表
                  NODES=$(kubectl get nodes -o jsonpath='{.items[*].metadata.name}')
                  
                  for svc in $SERVICES; do
                    # 提取 Service 名称中的节点后缀
                    target_node_suffix=${svc#"nodeproxy-access-"}
                    
                    # 检查节点是否存在 (忽略大小写)
                    if echo "$NODES" | grep -i -w -q "$target_node_suffix"; then
                      echo "KEEP: Node [$target_node_suffix] is alive."
                    else
                      echo "DELETE: Node [$target_node_suffix] is gone. Removing zombie service [$svc]."
                      kubectl delete svc $svc -n project-nodeproxy
                    fi
                  done
                  echo ">>> Cleanup finished."

4. 部署与验证

4.1 一键部署

将上述代码保存为 node-proxy-stack.yaml 并执行:

kubectl apply -f node-proxy-stack.yaml

4.2 验证服务生成

部署完成后,查看 Service 列表,你会发现每个节点都对应生成了一个 ClusterIP Service:

kubectl get svc -n project-nodeproxy
# 输出示例:
# NAME                          TYPE        CLUSTER-IP       PORT(S)
# nodeproxy-access-node-bj-01   ClusterIP   10.96.12.34      8888/TCP
# nodeproxy-access-node-sh-02   ClusterIP   10.96.56.78      8888/TCP

4.3 使用测试

假设你需要通过 node-bj-01 节点访问该节点局域网内的打印机(IP: 192.168.1.100)。

在集群内任意 Pod 中执行:

# 方法一:curl 指定代理
curl -x http://nodeproxy-access-node-bj-01.project-nodeproxy:8888 http://192.168.1.100/status

# 方法二:wget (设置环境变量)
export http_proxy=http://nodeproxy-access-node-bj-01.project-nodeproxy:8888
wget http://192.168.1.100/file.txt

5. 进阶场景:HostNetwork Pod 如何使用?

这是一个常见的“坑”。如果你的业务 Pod 配置了 hostNetwork: true(与宿主机共享网络),它默认会继承宿主机的 DNS 配置(通常是外部 DNS),导致它无法解析 K8s 内部的 nodeproxy-access-xxx 域名,也无法通过 127.0.0.1 访问代理(因为代理没监听宿主机端口)。

解决方案
必须在业务 Pod 的 YAML 中显式强制 DNS 策略指向 K8s DNS:

apiVersion: v1
kind: Pod
metadata:
  name: hostnet-client
spec:
  hostNetwork: true
  # 关键:强制 hostNetwork 的 Pod 使用集群内部 DNS 解析 Service 域名
  dnsPolicy: ClusterFirstWithHostNet 
  containers:
    - name: app
      image: curlimages/curl
      command: ["/bin/sleep", "3600"]

6. 总结

通过这套方案,我们实现了:

  1. 零端口暴露:代理服务完全隐身于 K8s 网络内部,宿主机端口扫描不可见。
  2. 全自动维护:节点上线即服务上线,节点下线自动清理垃圾数据。
  3. 精准路由:通过 DNS 名称即可指定流量从物理世界的哪个出口流出。

这为 K8s 在边缘计算、多地混合组网等复杂网络环境下的互联互通提供了一个优雅且低成本的解法。

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐