Contents hide

概述

本文档记录了在低配置 Kubernetes 集群环境中,对 Ingress 架构设计进行完整技术验证(PoC)的详细过程。验证过程包括部署、配置、测试和流量追踪等全链路环节。

验证环境

  • Kubernetes 版本: v1.29.15
  • 集群节点:
    • k8s-master: 10.4.4.15 (Control Plane)
    • k8s-node1: 10.4.0.17 (Worker Node)
  • 资源限制: Worker 节点内存紧张,需要轻量化部署

准备工作:资源适配策略

副本数调整

将 Controller 副本数从 3 降为 1(Worker 节点内存紧张,跑 1 个足矣)。

Service 类型选择

如果你的 K8s 是自建的(裸金属/虚拟机)且没有配置 MetalLB,LoadBalancer 类型的 Service 会一直处于 Pending 状态。为了验证,我们将 Controller 的 Service 临时改为 NodePort,这样你可以直接通过 节点IP:端口 访问。


步骤 1:部署 Ingress Controller (轻量化)

我们使用 Helm 部署,但通过命令行参数覆盖默认配置,以适应微型集群。

1.1 添加 Helm 仓库

helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

1.2 安装 Controller (适配参数)

请直接执行以下命令:

helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.replicaCount=1 \
  --set controller.service.type=NodePort \
  --set controller.service.nodePorts.http=30080 \
  --set controller.resources.requests.cpu=100m \
  --set controller.resources.requests.memory=90Mi \
  --set controller.admissionWebhooks.enabled=false \
  --set controller.allowSnippetAnnotations=true

参数解释

  • replicaCount=1: 节省资源
  • type=NodePort: 确保在没有云负载均衡器的情况下也能访问
  • nodePorts.http=30080: 固定端口,方便测试
  • admissionWebhooks.enabled=false: 关闭准入校验 Webhook,避免在小集群因网络波动导致从 API Server 无法连接 Controller 而造成部署失败
  • allowSnippetAnnotations=true: 允许使用 snippet 注解(如需要)

1.3 验证 Controller 启动

kubectl get pods -n ingress-nginx
<em># 等待状态变为 Running (1/1)</em>

验证结果

NAME                                        READY   STATUS    RESTARTS   AGE
ingress-nginx-controller-744f64bf78-x8tl6   1/1     Running   0          27s

步骤 2:模拟业务服务 (Backend)

为了验证流程,我们在 a-api 命名空间部署一个简单的 Nginx 服务作为后端。

2.1 创建命名空间

kubectl create ns a-api

2.2 部署业务应用 (Deployment)

kubectl create deployment a-api-demo --image=nginx:alpine --replicas=1 -n a-api

2.3 暴露服务 (Service)

kubectl expose deployment a-api-demo --name=a-api-service --port=80 --target-port=80 -n a-api

2.4 验证业务运行

kubectl get pod,svc -n a-api

验证结果

NAME                            READY   STATUS    RESTARTS   AGE
pod/a-api-demo-575dc6578b-dkprf   1/1     Running   0          45m

NAME                    TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
service/a-api-service   ClusterIP   10.104.181.16   <none>        80/TCP    45m

步骤 3:应用 Ingress 规则

这里我们直接应用设计文档中的逻辑,创建一个指向 a-api 的 Ingress 资源。

3.1 创建 Ingress YAML 文件

创建文件 ingress-v129.yaml

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: a-api-ingress
  namespace: a-api
spec:
  ingressClassName: nginx  <em># 对应我们安装的 ingress-nginx</em>
  rules:
  - host: a-api.test.com   <em># 测试域名</em>
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: a-api-service
            port:
              number: 80

3.2 应用配置

kubectl apply -f ingress-v129.yaml

3.3 检查 Ingress 状态

kubectl get ingress -n a-api
<em># ADDRESS 列如果出现 IP (通常是 NodeIP) 说明 Controller 已经成功识别并注入了状态</em>

验证结果

NAME            CLASS   HOSTS             ADDRESS   PORTS   AGE
a-api-ingress   nginx   a-api.test.com              80      7s

步骤 4:全链路技术验证

现在通过”外部”请求来验证整个链路:流量路径:curl -> NodePort(30080) -> Ingress Controller -> a-api-service -> Pod

由于我们没有配置真实的 DNS 解析,我们需要通过 curl 的 -H 参数伪造 Host 头。

请在你的 Master 节点或任何能 ping 通集群节点的机器上执行:

<em># 假设你的 Worker 节点 IP 是 192.168.1.x (请替换为真实IP)</em>
<em># 如果不知道 IP,用 kubectl get nodes -o wide 查看 INTERNAL-IP</em>
<em># 发送测试请求</em>
curl -v -H "Host: a-api.test.com" http://<你的Worker节点IP>:30080/

实际执行

kubectl get nodes -o wide

输出:

NAME         STATUS   ROLES           AGE    VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE         KERNEL-VERSION     CONTAINER-RUNTIME
k8s-master   Ready    control-plane   2d19h  v1.29.15  10.4.4.15     <none>        Ubuntu 24.04 LTS 6.8.0-71-generic   containerd://2.2.1
k8s-node1    Ready    <none>          2d18h  v1.29.15  10.4.0.17     <none>        Ubuntu 24.04 LTS 6.8.0-71-generic   containerd://2.2.1
curl -v -H "Host: a-api.test.com" http://10.4.0.17:30080/

预期结果

你应该能看到 Nginx 的默认欢迎页 HTML 代码:

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
<style>
html { color-scheme: light dark; }
body { width: 35em; margin: 0 auto;
font-family: Tahoma, Verdana, Arial, sans-serif; }
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
working. Further configuration is required.</p>

<p>For online documentation and support please refer to
<a href="http://nginx.org/">nginx.org</a>.<br/>
Commercial support is available at
<a href="http://nginx.com/">nginx.com</a>.</p>

<p><em>Thank you for using nginx.</em></p>
</body>
</html>

实际验证结果

*   Trying 10.4.0.17:30080...
* Connected to 10.4.0.17 (10.4.0.17) port 30080
> GET / HTTP/1.1
> Host: a-api.test.com
> User-Agent: curl/8.5.0
> Accept: */*

< HTTP/1.1 200 OK
< Date: Tue, 23 Dec 2025 03:27:48 GMT
< Content-Type: text/html
< Content-Length: 615
< Connection: keep-alive
< Last-Modified: Tue, 09 Dec 2025 19:41:33 GMT
< ETag: "69387b6d-267"
< Accept-Ranges: bytes

<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

结论:如果看到了这个页面,恭喜你!你的 Ingress 设计流程已经跑通了。


步骤 5:验证”路由隔离” (可选)

为了证明 Ingress 是根据域名路由的,你可以试着访问一个不存在的域名:

curl -v -H "Host: b-api.test.com" http://<你的Worker节点IP>:30080/

预期结果

你应该看到 404 Not Found,且是由 nginx 返回的。这证明请求到了 Ingress Controller,但 Controller 发现没有匹配 b-api.test.com 的规则,所以拦截了。


验证进阶:多命名空间 + 多服务 + 差异化响应

既然你想要”多验证几个不一样的”,我们来做两个更有趣的实验,模拟真实的微服务场景:

  • 场景 A (基于域名的路由): 不同的域名 (b-api.test.com) 分发到另一个命名空间的不同服务
  • 场景 B (基于路径的路由): 同一个域名,通过 /a 和 /b 分发给不同的业务

为了让你直观地看到区别,我们第二个服务不使用 Nginx,而是使用 Apache (httpd)。这样,服务 A 会返回 “Welcome to nginx”,服务 B 会返回 “It works!”,一眼就能看出区别。

实验 1:部署 b-api (使用 Apache)

我们模拟第二个业务团队 b-api。

1.1 创建命名空间和服务

<em># 创建命名空间</em>
kubectl create ns b-api

<em># 部署 httpd (Apache),它的默认首页是 "It works!",和 Nginx 很容易区分</em>
<em># 使用 alpine 版本以节省你宝贵的内存</em>
kubectl create deployment b-api-demo --image=httpd:alpine --replicas=1 -n b-api

<em># 暴露服务</em>
kubectl expose deployment b-api-demo --name=b-api-service --port=80 --target-port=80 -n b-api

1.2 创建 b-api 的 Ingress 规则

我们创建一个新的 Ingress 资源,这次绑定域名 b-api.test.com。

cat <<EOF > ingress-b-api.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: b-api-ingress
  namespace: b-api
spec:
  ingressClassName: nginx
  rules:
  - host: b-api.test.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: b-api-service
            port:
              number: 80
EOF

<em># 应用配置</em>
kubectl apply -f ingress-b-api.yaml

1.3 验证差异化路由

现在我们有了两个域名指向同一个 IP (你的 Worker 节点 10.4.0.17)。

测试 A (旧服务)

curl -H "Host: a-api.test.com" http://10.4.0.17:30080/ | grep "h1"

预期输出<h1>Welcome to nginx!</h1>

测试 B (新服务 – 跨命名空间)

curl -H "Host: b-api.test.com" http://10.4.0.17:30080/ | grep "h1"

预期输出<html><body><h1>It works!</h1></body></html> (或者类似的 It works 字样)

实际验证结果

curl -v -H "Host: b-api.test.com" http://10.4.0.17:30080/

输出:

*   Trying 10.4.0.17:30080...
* Connected to 10.4.0.17 (10.4.0.17) port 30080
> GET / HTTP/1.1
> Host: b-api.test.com
> User-Agent: curl/8.5.0
> Accept: */*

< HTTP/1.1 200 OK
< Date: Tue, 23 Dec 2025 04:11:17 GMT
< Content-Type: text/html
< Content-Length: 191
< Connection: keep-alive
< Last-Modified: Fri, 07 Nov 2025 08:23:08 GMT
< ETag: "bf-642fce432f300"
< Accept-Ranges: bytes

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html>
<head>
<title>It works! Apache httpd</title>
</head>
<body>
<p>It works!</p>
</body>
</html>

原理说明:Ingress Controller 根据 HTTP 请求头中的 Host 字段,自动将流量切换到了不同的 Namespace (a-api vs b-api) 和不同的 Backend Pod。

实验 2:路径路由 (Fanout) – 一个域名,多个后端

这是企业中最常用的场景:example.com/api/v1 去一个服务,example.com/api/v2 去另一个。

我们创建一个统一的入口文件 ingress-fanout.yaml,放在 a-api 命名空间下(假设由统一网关组管理,或者使用 ExternalName Service 引用跨 NS 服务,这里为了简单,我们让它管理指向 a-api 的路径,同时演示路径匹配)。

这里我们演示:

  • 访问 all.test.com/a -> 去 a-api
  • 访问 all.test.com/ -> 默认去 a-api (兜底)

(注:跨命名空间引用 Service 在标准 Ingress 中通常需要 Service 位于同 Namespace,或者使用 ExternalName。为了不把流程搞复杂,我们这里仅演示在 a-api 里的路径路由)

2.1 创建路径路由 Ingress

cat <<EOF > ingress-fanout.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: fanout-ingress
  namespace: a-api
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
  - host: all.test.com
    http:
      paths:
      # 访问 /foo 转给 a-api-service
      - path: /foo
        pathType: Prefix
        backend:
          service:
            name: a-api-service
            port:
              number: 80
      # 访问 /bar 也转给 a-api-service (实际场景通常是不同的service)
      - path: /bar
        pathType: Prefix
        backend:
          service:
            name: a-api-service
            port:
              number: 80
EOF

kubectl apply -f ingress-fanout.yaml

2.2 验证 Rewrite 和路径

<em># 访问 /foo</em>
curl -I -H "Host: all.test.com" http://10.4.0.17:30080/foo

预期HTTP/1.1 200 OK

这里最关键的是 nginx.ingress.kubernetes.io/rewrite-target: / 这个注解。

如果不加它,请求发给 Nginx Pod 的路径会是 /foo,但 Nginx 容器内部只有 / (index.html),没有 /foo 目录,就会报 404。加了这个注解,Ingress 会把 /foo 剥离掉,只把 / 发给后端。

实验 3:验证 HTTPS (自签名证书)

你的设计文档里提到了 SSL/TLS。虽然我们没有真证书,但可以生成一个自签名的来验证流程。

3.1 生成自签名证书

openssl req -x509 -nodes -days 365 -newkey rsa:2048 \
  -keyout tls.key -out tls.crt -subj "/CN=secure.test.com"

<em># 创建 Secret</em>
kubectl create secret tls secure-tls --key tls.key --cert tls.crt -n a-api

3.2 创建 HTTPS Ingress

cat <<EOF > ingress-tls.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: secure-ingress
  namespace: a-api
spec:
  ingressClassName: nginx
  tls:
  - hosts:
    - secure.test.com
    secretName: secure-tls
  rules:
  - host: secure.test.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: a-api-service
            port:
              number: 80
EOF

kubectl apply -f ingress-tls.yaml

3.3 验证 HTTPS

你需要获取 HTTPS 的 NodePort 端口(不是 30080 了,是随机的,除非我们之前固定了)。

让我们查一下 HTTPS 的端口:

kubectl get svc -n ingress-nginx ingress-nginx-controller

看 443: 后面映射的端口是多少(比如 32443)。

然后测试(加上 -k 忽略证书警告,因为是自签名的):

<em># 假设端口是 3xxxx</em>
export HTTPS_PORT=$(kubectl get svc -n ingress-nginx ingress-nginx-controller -o jsonpath='{.spec.ports[1].nodePort}')

curl -v -k -H "Host: secure.test.com" https://10.4.0.17:$HTTPS_PORT/

如果看到 SSL connection using ... 和 Welcome to nginx!,说明 TLS 终止(Offloading)成功了!

实际验证结果

curl -v -k -H "Host: secure.test.com" https://10.4.0.17:$HTTPS_PORT/

输出显示:

*   Trying 10.4.0.17:32443...
* Connected to 10.4.0.17 (10.4.0.17) port 32443
...
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
...
< HTTP/2 200
...
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...

流量流转验证:三种方法

要证明流量确实是 Client -> Ingress Controller -> Service -> Pod 这样流转的,而不是直接撞上了 Pod 或者 Service,我们有三种”铁证”可以验证。

方法一:查看 Ingress Controller 的实时访问日志(最直接的证据)

Ingress Controller 本质上是一个反向代理(Nginx),它会记录每一条经过它的请求。如果日志里有记录,说明流量确实经过了它。

1.1 打开一个终端窗口,实时监听 Controller 的日志

<em># -f 表示 follow (实时输出),-n 指定命名空间</em>
kubectl logs -f -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx

1.2 在另一个窗口发送请求

curl -H "Host: a-api.test.com" http://10.4.0.17:30080/

1.3 回到日志窗口观察

你应该能看到类似下面的一行日志立即滚动出来:

10.4.0.17 - - [23/Dec/2025:04:00:00 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/7.68.0" 86 0.002 [a-api-a-api-service-80] [] 10.1.2.3:80 615 0.002 200 ...

关键点分析

  • Host: 如果你开启详细日志,会看到 a-api.test.com
  • Upstream ([a-api-a-api-service-80]): 这证明 Controller 识别出了这是要去 a-api 命名空间的 a-api-service 服务
  • Upstream IP (10.1.2.3:80): 这里的 IP 必须是你后端业务 Pod 的 IP(见方法二)

实际验证结果

W1223 03:51:25.338445       7 controller.go:1469] Unexpected error validating SSL certificate "a-api/secure-tls" for server "secure.test.com": x509: certificate relies on legacy Common Name field, use SANs instead
W1223 03:51:25.338476       7 controller.go:1470] Validating certificate against DNS names. This will be deprecated in a future version
I1223 03:51:25.340746       7 controller.go:214] "Configuration changes detected, backend reload required"
I1223 03:51:25.390835       7 controller.go:228] "Backend successfully reloaded"
I1223 03:51:25.391624       7 event.go:377] Event(v1.ObjectReference{Kind:"Pod", Namespace:"ingress-nginx", Name:"ingress-nginx-controller-744f64bf78-x8tl6", UID:"3b1af039-c49b-4f5d-8880-5a10b5f5d7e6", APIVersion:"v1", ResourceVersion:"460791", FieldPath:""}): type: 'Normal' reason: 'RELOAD' NGINX reload triggered due to a change in configuration
I1223 03:52:02.957678       7 status.go:311] "updating Ingress status" namespace="a-api" ingress="secure-ingress" currentValue=null newValue=[{"ip":"10.104.181.16"}]
I1223 03:52:02.962960       7 event.go:377] Event(v1.ObjectReference{Kind:"Ingress", Namespace:"a-api", Name:"secure-ingress", UID:"3d83cbd8-361b-4265-9090-eab01250dbb1", APIVersion:"networking.k8s.io/v1", ResourceVersion:"464318", FieldPath:""}): type: 'Normal' reason: 'Sync' Scheduled for sync
W1223 03:52:02.963441       7 controller.go:1469] Unexpected error validating SSL certificate "a-api/secure-tls" for server "secure.test.com": x509: certificate relies on legacy Common Name field, use SANs instead
W1223 03:52:02.963526       7 controller.go:1470] Validating certificate against DNS names. This will be deprecated in a future version
10.4.0.17 - - [23/Dec/2025:03:52:28 +0000] "GET / HTTP/2.0" 200 615 "-" "curl/8.5.0" 30 0.001 [a-api-a-api-service-80] [] 192.168.36.78:80 615 0.001 200 8cf46d801eed33a14953870e4cc826db
10.4.0.17 - - [23/Dec/2025:04:07:13 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0" 77 0.001 [a-api-a-api-service-80] [] 192.168.36.78:80 615 0.001 200 01ca7520fdb1071cf38873d9223db8fe
10.4.0.17 - - [23/Dec/2025:04:08:18 +0000] "GET / HTTP/2.0" 200 615 "-" "curl/8.5.0" 30 0.001 [a-api-a-api-service-80] [] 192.168.36.78:80 615 0.001 200 a698f1311f1ffd12b7bf30dbba93d21a
10.4.0.17 - - [23/Dec/2025:04:08:47 +0000] "GET / HTTP/1.1" 200 615 "-" "curl/8.5.0" 77 0.001 [a-api-a-api-service-80] [] 192.168.36.78:80 615 0.001 200 62b640eeb06e1da08812e54dc41c4cc5
10.4.0.17 - - [23/Dec/2025:04:08:56 +0000] "GET / HTTP/1.1" 200 191 "-" "curl/8.5.0" 77 0.001 [b-api-b-api-service-80] [] 192.168.36.79:80 191 0.000 200 11c6853707a5932ce643078b26718a37

关键日志分析

  • [a-api-a-api-service-80] [] 192.168.36.78:80: Controller 识别出这是要去 a-api 命名空间的 a-api-service 服务,目标 Pod IP 是 192.168.36.78
  • [b-api-b-api-service-80] [] 192.168.36.79:80: Controller 识别出这是要去 b-api 命名空间的 b-api-service 服务,目标 Pod IP 是 192.168.36.79

方法二:验证 IP 对应关系(数据链路证据)

在你的 Dashboard 详情页中,有一个非常关键的信息:

Endpoints: 10.104.181.16

这个 IP 地址是 Ingress Controller 经过计算后,认为”流量应该发往的目标地址”。我们需要确认这个 IP 到底是谁。

请执行以下命令:

kubectl get pods -n a-api -o wide

预期结果

你应该会发现 a-api-demo-xxxxx 这个 Pod 的 IP 正是 192.168.36.78(或类似的 Pod IP)。

实际验证结果

NAME                            READY   STATUS    RESTARTS   AGE   IP             NODE        NOMINATED NODE   READINESS GATES
a-api-demo-575dc6578b-dkprf   1/1     Running   0          45m   192.168.36.78   k8s-node1   <none>           <none>

结论

  • Ingress 既然显示了 Endpoint 是 192.168.36.78,说明它已经通过 Service 找到了具体的 Pod
  • 如果流量不经过 Ingress,Ingress Controller 根本不需要知道这个 Endpoint 是多少

方法三:Header 响应头和 Pod 日志”露马脚”(针对 b-api)

这个方法利用了你刚才部署的 Apache (b-api) 和 Nginx (Ingress Controller) 的特性差异。

  • 你的后端 b-api 是 Apache (httpd)
  • 你的网关 ingress-nginx 是 Nginx

如果你直接访问 Apache,响应头里的 Server 应该是 Apache/2.4.x。

但如果经过了 Ingress,Nginx 通常会把 Server 头改写或者保留。

让我们测试一下:

curl -v -H "Host: b-api.test.com" http://10.4.0.17:30080/

观察输出中的 < Server: 字段

  • 如果看到 Server: nginx/1.x.x,这不仅证明了请求成功,还证明了是 Nginx(Ingress Controller)在回复你,它作为代理人把 Apache 的内容拿回来给了你
  • 有些配置下可能会显示 Server: Apache,但在 Header 里通常会多出一个 X-Real-IP 或者 X-Forwarded-For,这是反向代理注入的特征

你可以进到 b-api 的 Pod 里看 Apache 的访问日志,会发现来源 IP 不是你发送 curl 的机器 IP,而是 Ingress Controller Pod 的 IP。

验证命令

<em># 1. 获取 Ingress Controller Pod IP</em>
kubectl get pods -n ingress-nginx -o wide

输出:

NAME                                        READY   STATUS    RESTARTS   AGE   IP             NODE        NOMINATED NODE   READINESS GATES
ingress-nginx-controller-744f64bf78-x8tl6   1/1     Running   0          50m   192.168.36.77   k8s-node1   <none>           <none>
<em># 2. 查看 b-api 的日志</em>
kubectl logs -n b-api -l app=b-api-demo

实际验证结果

AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.36.79. Set the 'ServerName' directive globally to suppress this message
AH00558: httpd: Could not reliably determine the server's fully qualified domain name, using 192.168.36.79. Set the 'ServerName' directive globally to suppress this message
[Tue Dec 23 03:46:08.238030 2025] [mpm_event:notice] [pid 1:tid 1] AH00489: Apache/2.4.66 (Unix) configured -- resuming normal operations
[Tue Dec 23 03:46:08.238109 2025] [core:notice] [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND'
192.168.36.77 - - [23/Dec/2025:03:47:23 +0000] "GET / HTTP/1.1" 200 191
192.168.36.77 - - [23/Dec/2025:03:47:45 +0000] "GET / HTTP/1.1" 200 191
192.168.36.77 - - [23/Dec/2025:04:08:56 +0000] "GET / HTTP/1.1" 200 191
192.168.36.77 - - [23/Dec/2025:04:11:17 +0000] "GET / HTTP/1.1" 200 191

关键发现

  • Apache 日志中显示的来源 IP 是 192.168.36.77,这正是 Ingress Controller Pod 的 IP(192.168.36.77)
  • 这证明了流量不是从外面直接”偷渡”进来的,而是经过了 Controller 的代理

证据链闭环分析

我们提取出日志中的关键 IP 地址和角色:

IP 地址映射表

IP 地址角色来源
192.168.36.77Ingress Controller Podkubectl get pods -n ingress-nginx -o wide
192.168.36.78a-api 业务 Podkubectl get pods -n a-api -o wide
192.168.36.79b-api 业务 Pod从 Controller 日志推断出

线索 1:Controller 找到了正确的人 (向下看)

在验证 1 的 Controller 日志中,有这样一行:

[a-api-a-api-service-80] [] 192.168.36.78:80

解读:Controller 说:”我收到了去 a-api 的请求,我查了 Service 发现目标是 192.168.36.78,我已经把货发过去了。”

佐证:这就和验证 2 中 a-api Pod 的真实 IP (192.168.36.78) 完美吻合!

线索 2:业务 Pod 看到了正确的人 (向上看)

在验证 3 的 b-api (Apache) 日志中,有这样一行:

192.168.36.77 - - [23/Dec/2025:04:11:17 +0000] "GET / HTTP/1.1" 200 191

解读:Apache 说:”有个 IP 是 192.168.36.77 的家伙刚才访问了我。”

佐证:这个 IP 正是验证 3 中 Ingress Controller 自己的 Pod IP (192.168.36.77)!这证明了流量不是从外面直接”偷渡”进来的,而是经过了 Controller 的代理。

完整流量路径验证

客户端 (10.4.0.17 或外部)

NodePort Service (30080)

Ingress Controller Pod (192.168.36.77)
    ↓ (根据 Host 头路由)
Service (a-api-service / b-api-service)

业务 Pod (192.168.36.78 / 192.168.36.79)

关于日志中的警告信息

你可能注意到了验证 1 开头的警告:

W1223 ... Unexpected error validating SSL certificate ... x509: certificate relies on legacy Common Name field, use SANs instead

不用担心,这是正常的。

原因

我们用简单的 openssl 命令生成的自签名证书只使用了旧版的 CN (Common Name) 字段。现代的安全标准(如 Go 1.15+ 和 Chrome)要求使用 SANs (Subject Alternative Names)。

影响

仅仅是 Controller 发出的警告,不会阻断流量。在生产环境中,你会使用 Cert-Manager 或云厂商签发的正式证书,这个问题自然就没有了。

解决方案(生产环境)

在生产环境中,建议使用 Cert-Manager 自动管理证书:

apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
  name: a-api-tls
  namespace: a-api
spec:
  secretName: a-api-tls
  issuerRef:
    name: letsencrypt-prod
    kind: ClusterIssuer
  dnsNames:
  - a-api.example.com

总结与下一步

验证成果

你已经成功在低配环境验证了设计文档中的核心逻辑:

  1. ✅ Controller 集中管理:Ingress-nginx 成功运行
  2. ✅ Ingress 资源分布:在 a-api 和 b-api 命名空间定义的规则生效了
  3. ✅ 流量转发:成功将外部流量导入了内部 Service
  4. ✅ 多命名空间路由:不同域名正确路由到不同命名空间的服务
  5. ✅ TLS 终止:HTTPS 流量成功处理
  6. ✅ 流量追踪:完整验证了流量路径

环境清理(可选)

验证完成后,你可以执行以下命令清理环境,释放那宝贵的 1G 内存:

kubectl delete ns a-api
kubectl delete ns b-api
helm uninstall ingress-nginx -n ingress-nginx
kubectl delete ns ingress-nginx

下一步建议

既然你已经掌握了部署、路由、TLS(HTTPS) 和排错(日志分析),这个 PoC 环境的历史使命已经完成了。

如果你想继续深入,可以做以下两件事之一:

  1. 清理环境:释放那宝贵的 1G 内存
  2. 尝试 GitOps 流程:把你设计文档里的 ArgoCD 部分跑通,但这可能需要更多的内存资源

目前来看,你的 Ingress 架构设计在真实集群上是完全可行且逻辑通顺的!


深入理解:核心机制解析

在完成实践验证后,你可能会有一些更深层次的问题。本节将解答三个关键问题,帮助你深入理解 Ingress 的工作原理。

问题一:Ingress Controller 是怎么知道各个命名空间的路由规则的?

核心机制:Kubernetes 的”监听(Watch)”机制 + RBAC 权限

Ingress Controller (IC) 本质上是一个运行在 Pod 里的守护进程(Go 语言编写的程序),它并不是”轮询”去问 API Server,而是通过 Event(事件)驱动 的方式工作的。

1. 它是如何”跨命名空间”看到的?

还记得我们在部署 Controller 时,Helm 帮你创建了 ClusterRole 和 ClusterRoleBinding 吗?

  • ClusterRole: 赋予了它在集群级别(Cluster-wide)读取 Ingresses、Services、Endpoints、Secrets 的权限
  • 结果: 尽管 Controller 运行在 ingress-nginx 命名空间下,但它拥有”上帝视角”,能看到 a-apib-api 等所有命名空间里的资源

2. 它是如何”实时感知”变化的?

这是 K8s 控制器的标准工作模式(Informer Pattern):

  1. 订阅(Watch): Controller 启动时,会向 K8s API Server 注册一个”监听器”,订阅所有 Ingress 资源的变更事件(增、删、改)
  2. 事件触发: 当你在 a-api 下执行 kubectl apply -f ingress.yaml 时:
    • API Server 将数据存入 etcd
    • API Server 立刻推送一个 ADD 事件给 Controller
  3. 配置翻译: Controller 收到事件,读取你的 YAML 内容(Host, Path, ServiceName),将其翻译成 Nginx 原生的配置文件格式(nginx.conf)
  4. 重载 (Reload): Controller 修改 Pod 内部的 /etc/nginx/nginx.conf 文件,并执行 nginx -s reload
  5. 生效: 新的路由规则毫秒级生效

验证方法

你可以通过以下命令观察 Controller 的实时日志,看到配置重载的过程:

kubectl logs -f -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx | grep -i reload

当你创建或修改 Ingress 时,会看到类似这样的日志:

I1223 03:51:25.340746       7 controller.go:214] "Configuration changes detected, backend reload required"
I1223 03:51:25.390835       7 controller.go:228] "Backend successfully reloaded"

问题二:我写的 ingress.yaml 本身占用资源(CPU/内存)吗?

简短回答:几乎不占用计算资源,只占用极微小的存储资源。

我们需要区分 “静态配置(Config)” 和 “运行实例(Process)”

Ingress Controller (Pod)

  • 是活的。它是一个运行中的进程
  • 占用资源: 它需要 CPU 来处理网络请求、SSL 加密解密、计算路由;需要内存来缓存连接、加载 Lua 脚本等。所以我们需要给它限制 CPU/Mem

Ingress.yaml (资源对象)

  • 是死的。它只是一段文本数据(配置记录)
  • 存储: 当你 apply 后,它只是在 K8s 的数据库(etcd)里存了一条几 KB 的文本记录
  • 计算: 它不消耗 Worker 节点的 CPU 和内存。它不会启动任何 Pod 或容器
  • 唯一消耗: 它会导致 Ingress Controller 加载的 nginx.conf 变大一点点(几行文本),对 Controller 的内存影响微乎其微(除非你有几万条规则)

结论: 你可以在集群里创建成百上千个 ingress.yaml,只要没有流量进来,它们对集群的负载压力几乎为零。

验证方法

<em># 查看 Ingress 资源占用的存储空间(实际上只是 etcd 中的一条记录)</em>
kubectl get ingress -A --no-headers | wc -l  <em># 统计 Ingress 数量</em>

<em># 查看 Controller 的内存使用(这才是真正消耗资源的地方)</em>
kubectl top pod -n ingress-nginx

问题三:外部流量可能绕过 Ingress Controller,直接走到业务 Pod 吗?

这是一个非常关键的安全和架构问题。

答案取决于你的 Service 类型和网络策略。

情况 A:绝对无法绕过(标准生产架构)✅

在我们的 PoC 验证中,你的业务 Service (a-api-service) 配置如下:

  • TypeClusterIP (默认)
  • IP: 类似于 10.104.181.16

分析:

  • ClusterIP 是一个虚拟 IP,它只存在于 Kubernetes 集群内部网络中
  • 外部世界(你的笔记本、公网用户)根本就没有路由能通达这个 IP
  • 唯一入口: 外部流量只能通过你暴露的 NodePort:30080 (即 Ingress Controller) 进入

结论: 流量被迫经过 Ingress Controller -> Service -> Pod。无法绕过。

情况 B:可以绕过(配置不当/特殊需求)⚠️

如果你的业务 Service (a-api-service) 被你配置成了:

  1. Type: NodePort: 此时 K8s 会在所有节点上开一个端口(比如 31000)。外部用户如果知道这个端口,就可以直接 curl NodeIP:31000 访问你的 Pod,完全绕过 Ingress 的路由规则、限流和鉴权
  2. Type: LoadBalancer: 云厂商会直接给这个 Service 分配一个公网 IP。外部流量直接打进来,绕过 Ingress
  3. HostNetwork: true: Pod 直接占用宿主机网络,也能被绕过

验证方法

<em># 检查你的 Service 类型(应该是 ClusterIP)</em>
kubectl get svc -n a-api

<em># 尝试直接访问 ClusterIP(应该失败,因为外部无法路由)</em>
curl http://10.104.181.16/  <em># 这个 IP 只在集群内部可达</em>

总结: 只要你的业务 Service 类型保持为 ClusterIP(这是默认值,也是最佳实践),外部流量就物理上无法绕过 Ingress Controller。Ingress 变成了集群唯一的”守门人”。

最佳实践建议

  1. 业务 Service 使用 ClusterIP(默认值,无需修改)
  2. 仅 Ingress Controller 使用 NodePort 或 LoadBalancer 作为外部入口
  3. 使用 NetworkPolicy 进一步限制 Pod 间的网络访问(可选,但推荐)
  4. 定期审计 Service 类型,确保没有业务服务被误配置为 NodePort 或 LoadBalancer

实践验证:外部流量绕过 Ingress 实验

为了更直观地理解 Service 类型对安全性的影响,我们可以通过两个阶段来验证:

  • 阶段一(现状验证):证明当前状态下,外部流量无法绕过 Ingress
  • 阶段二(模拟漏洞):人为修改配置,让外部流量成功绕过 Ingress(演示如果不小心配置错了会发生什么)

阶段一:验证”无法绕过” (安全基线)

目前你的 a-api-service 应该是默认的 ClusterIP 类型。这意味着它只有集群内部 IP,没有对外的端口。

1. 确认 Service 类型
kubectl get svc -n a-api

预期输出

NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
a-api-service   ClusterIP   10.97.242.22    <none>        80/TCP    ...

关键点TYPE 是 ClusterIP

实际验证结果

NAME            TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
a-api-service   ClusterIP   10.97.242.22    <none>        80/TCP    3h9m
2. 尝试直接”攻击”后端

我们知道 a-api 的 Pod 运行在 Worker 节点 (10.4.0.17) 上,且服务端口是 80。我们试着直接访问 Worker 节点的 80 端口,看看能不能摸到业务 Pod。

<em># 尝试直接连接 Worker IP 的 80 端口</em>
curl -v --connect-timeout 5 http://10.4.0.17:80/

预期结果

失败 (Connection refused 或 Timed out)。

原因:Worker 节点的宿主机上,并没有任何进程监听 80 端口。Pod 的 80 端口是在 CNI (容器网络) 的虚拟网络里的,外部物理网络直接连不通。

实际验证结果

*   Trying 10.4.0.17:80...
* Connected to 10.4.0.17 (10.4.0.17) port 80
> GET / HTTP/1.1
> Host: 10.4.0.17
...
< HTTP/1.1 200 OK
< Server: nginx/1.29.4
...
<title>InkSpace - 个人网站</title>

⚠️ 特殊情况说明

在实际验证中,访问 Worker 节点的 80 端口成功了,但返回的是”InkSpace – 个人网站”的内容,而不是 Kubernetes Pod 的内容。这是因为:

  • 宿主机上部署了博客系统,占用了 80 端口
  • 这个响应来自宿主机的应用,不是 Kubernetes Pod
  • 这证明了:即使宿主机有服务监听 80 端口,Kubernetes Pod 的 80 端口仍然是隔离的,外部无法直接访问

结论:在当前配置下,外部流量必须经过 NodePort 30080 (Ingress Controller) 才能访问到 Kubernetes 集群内的业务 Pod,无法绕过。

阶段二:模拟”绕过 Ingress” (制造漏洞)

现在我们修改 Service 配置,把它变成 NodePort 类型。这相当于在防火墙上打了一个洞,允许外部直接访问业务 Service。

1. 修改 Service 类型为 NodePort

执行以下命令,将 a-api-service 的类型”热更新”为 NodePort

kubectl patch svc a-api-service -n a-api -p '{"spec": {"type": "NodePort"}}'

实际执行结果

service/a-api-service patched
2. 查看暴露的端口

再次查看 Service,找到 K8s 随机分配的那个端口:

kubectl get svc -n a-api

实际输出

NAME            TYPE       CLUSTER-IP      EXTERNAL-IP   PORT(S)        AGE
a-api-service   NodePort   10.97.242.22     <none>        80:30925/TCP   3h14m

关键信息:分配的端口是 30925(请以你实际看到的为准)。

3. 执行绕过攻击

现在,我们不走 Ingress (端口 30080),而是直接走这个后门端口 (30925)。

<em># 替换 30925 为你上面查到的端口</em>
export BYPASS_PORT=$(kubectl get svc -n a-api a-api-service -o jsonpath='{.spec.ports[0].nodePort}')

curl -v http://10.4.0.17:$BYPASS_PORT/

预期结果

✅ 成功返回 <title>Welcome to nginx!</title>

实际验证结果

*   Trying 10.4.0.17:30925...
* Connected to 10.4.0.17 (10.4.0.17) port 30925
> GET / HTTP/1.1
> Host: 10.4.0.17:30925
...
< HTTP/1.1 200 OK
< Server: nginx/1.29.4
...
<!DOCTYPE html>
<html>
<head>
<title>Welcome to nginx!</title>
...
<h1>Welcome to nginx!</h1>
...

😱 后果:你成功绕过了 Ingress Controller!

阶段三:铁证如山 (日志对比)

为了证明这是”绕过”,我们需要看 Ingress Controller 的日志。

1. 打开日志监听
kubectl logs -f -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
2. 再次发送”绕过请求”

在另一个窗口执行刚才的 curl 命令(访问 30925 端口)。

curl -v http://10.4.0.17:30925/
3. 观察日志

结果:Ingress Controller 的日志里静悄悄的,完全没有这条访问记录。

结论:因为流量根本没经过它!就像你偷偷从后门进了大楼,前门的保安(Ingress)根本不知道你来了。

这就带来了严重的问题:

  • ❌ Ingress 上配置的 HTTPS 加密失效了(你是明文访问的)
  • ❌ Ingress 上配置的限流、鉴权、WAF 防火墙全部失效
  • ❌ 无法通过 Ingress 日志追踪和审计访问记录

阶段四:恢复环境 (堵上漏洞)

验证完成后,我们必须把这个漏洞堵上,恢复架构的安全性。

1. 恢复 Service 为 ClusterIP
kubectl patch svc a-api-service -n a-api -p '{"spec": {"type": "ClusterIP"}}'

<em># 注意:patch 可能会报错说端口冲突或字段不可变</em>
<em># 如果报错,最简单的办法是删了重建 Service (不删 Deployment 不影响业务)</em>
kubectl delete svc a-api-service -n a-api
kubectl expose deployment a-api-demo --name=a-api-service --port=80 --target-port=80 -n a-api

实际执行结果

<em># 第一步:尝试 patch(可能失败)</em>
kubectl patch svc a-api-service -n a-api -p '{"spec": {"type": "ClusterIP"}}'
<em># service/a-api-service patched</em>

<em># 第二步:删除并重建(更稳妥)</em>
kubectl delete svc a-api-service -n a-api
<em># service "a-api-service" deleted</em>

kubectl expose deployment a-api-demo --name=a-api-service --port=80 --target-port=80 -n a-api
<em># service/a-api-service exposed</em>
2. 再次验证
kubectl get svc -n a-api

实际验证结果

NAME            TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
a-api-service   ClusterIP   10.100.197.170   <none>        80/TCP    8s

✅ 确认变回了 ClusterIP

3. 验证绕过端口已失效
<em># 尝试访问之前的 NodePort</em>
curl -v http://10.4.0.17:30925/

实际验证结果

*   Trying 10.4.0.17:30925...
* connect to 10.4.0.17 port 30925 from 10.4.4.15 port 53912 failed: Connection refused
* Failed to connect to 10.4.0.17 port 30925 after 1 ms: Couldn't connect to server

✅ 确认 NodePort 端口已关闭,无法绕过。

实验总结

通过这个实验,我们验证了:

  1. ✅ ClusterIP 是安全的防线:只要 Service 是 ClusterIP,外部流量就”物理上”无法直接触达业务 Pod(除非宿主机本身有服务占用端口,但那是宿主机层面的,不是 Kubernetes 层面的)
  2. ⚠️ NodePort 是潜在的漏洞:如果开发人员随意把业务 Service 改成 NodePort,就会导致 Ingress 被架空,所有安全策略失效
  3. 🔒 防御建议:这就是为什么在生产环境中,我们通常会配合 NetworkPolicy(网络策略) 来进一步加固:”只允许 Ingress Controller 的 Pod 访问后端业务 Pod,禁止其他任何来源访问。”

进一步加固:NetworkPolicy 示例

apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
  name: allow-ingress-only
  namespace: a-api
spec:
  podSelector:
    matchLabels:
      app: a-api-demo
  policyTypes:
  - Ingress
  ingress:
  - from:
    - namespaceSelector:
        matchLabels:
          name: ingress-nginx
    - podSelector:
        matchLabels:
          app.kubernetes.io/name: ingress-nginx
    ports:
    - protocol: TCP
      port: 80

这个策略确保:只有来自 ingress-nginx 命名空间的 Pod 才能访问 a-api 的 Pod,即使 Service 被误配置为 NodePort,外部流量也无法直接到达 Pod。


附录:完整命令清单

部署阶段

<em># 1. 安装 Helm(如未安装)</em>
curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3
chmod 700 get_helm.sh
./get_helm.sh

<em># 2. 添加 Helm 仓库</em>
helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx
helm repo update

<em># 3. 部署 Ingress Controller</em>
helm install ingress-nginx ingress-nginx/ingress-nginx \
  --namespace ingress-nginx \
  --create-namespace \
  --set controller.replicaCount=1 \
  --set controller.service.type=NodePort \
  --set controller.service.nodePorts.http=30080 \
  --set controller.resources.requests.cpu=100m \
  --set controller.resources.requests.memory=90Mi \
  --set controller.admissionWebhooks.enabled=false \
  --set controller.allowSnippetAnnotations=true

<em># 4. 验证 Controller</em>
kubectl get pods -n ingress-nginx

业务服务部署

<em># 创建命名空间</em>
kubectl create ns a-api
kubectl create ns b-api

<em># 部署 a-api (Nginx)</em>
kubectl create deployment a-api-demo --image=nginx:alpine --replicas=1 -n a-api
kubectl expose deployment a-api-demo --name=a-api-service --port=80 --target-port=80 -n a-api

<em># 部署 b-api (Apache)</em>
kubectl create deployment b-api-demo --image=httpd:alpine --replicas=1 -n b-api
kubectl expose deployment b-api-demo --name=b-api-service --port=80 --target-port=80 -n b-api

Ingress 配置

<em># a-api Ingress</em>
cat <<EOF > ingress-v129.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: a-api-ingress
  namespace: a-api
spec:
  ingressClassName: nginx
  rules:
  - host: a-api.test.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: a-api-service
            port:
              number: 80
EOF

<em># b-api Ingress</em>
cat <<EOF > ingress-b-api.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: b-api-ingress
  namespace: b-api
spec:
  ingressClassName: nginx
  rules:
  - host: b-api.test.com
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: b-api-service
            port:
              number: 80
EOF

<em># 应用配置</em>
kubectl apply -f ingress-v129.yaml
kubectl apply -f ingress-b-api.yaml

验证命令

<em># 获取节点 IP</em>
kubectl get nodes -o wide

<em># 测试 HTTP</em>
curl -v -H "Host: a-api.test.com" http://10.4.0.17:30080/
curl -v -H "Host: b-api.test.com" http://10.4.0.17:30080/

<em># 查看日志</em>
kubectl logs -f -n ingress-nginx -l app.kubernetes.io/name=ingress-nginx
kubectl logs -n a-api -l app=a-api-demo
kubectl logs -n b-api -l app=b-api-demo

<em># 查看 Pod IP</em>
kubectl get pods -n ingress-nginx -o wide
kubectl get pods -n a-api -o wide
kubectl get pods -n b-api -o wide