概述
本文档记录了在低配置 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.77 | Ingress Controller Pod | kubectl get pods -n ingress-nginx -o wide |
| 192.168.36.78 | a-api 业务 Pod | kubectl get pods -n a-api -o wide |
| 192.168.36.79 | b-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
总结与下一步
验证成果
你已经成功在低配环境验证了设计文档中的核心逻辑:
- ✅ Controller 集中管理:Ingress-nginx 成功运行
- ✅ Ingress 资源分布:在 a-api 和 b-api 命名空间定义的规则生效了
- ✅ 流量转发:成功将外部流量导入了内部 Service
- ✅ 多命名空间路由:不同域名正确路由到不同命名空间的服务
- ✅ TLS 终止:HTTPS 流量成功处理
- ✅ 流量追踪:完整验证了流量路径
环境清理(可选)
验证完成后,你可以执行以下命令清理环境,释放那宝贵的 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 环境的历史使命已经完成了。
如果你想继续深入,可以做以下两件事之一:
- 清理环境:释放那宝贵的 1G 内存
- 尝试 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-api、b-api等所有命名空间里的资源
2. 它是如何”实时感知”变化的?
这是 K8s 控制器的标准工作模式(Informer Pattern):
- 订阅(Watch): Controller 启动时,会向 K8s API Server 注册一个”监听器”,订阅所有 Ingress 资源的变更事件(增、删、改)
- 事件触发: 当你在
a-api下执行kubectl apply -f ingress.yaml时:- API Server 将数据存入 etcd
- API Server 立刻推送一个
ADD事件给 Controller
- 配置翻译: Controller 收到事件,读取你的 YAML 内容(Host, Path, ServiceName),将其翻译成 Nginx 原生的配置文件格式(nginx.conf)
- 重载 (Reload): Controller 修改 Pod 内部的
/etc/nginx/nginx.conf文件,并执行nginx -s reload - 生效: 新的路由规则毫秒级生效

验证方法:
你可以通过以下命令观察 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) 配置如下:
- Type:
ClusterIP(默认) - IP: 类似于
10.104.181.16
分析:
ClusterIP是一个虚拟 IP,它只存在于 Kubernetes 集群内部网络中- 外部世界(你的笔记本、公网用户)根本就没有路由能通达这个 IP
- 唯一入口: 外部流量只能通过你暴露的
NodePort:30080(即 Ingress Controller) 进入
结论: 流量被迫经过 Ingress Controller -> Service -> Pod。无法绕过。
情况 B:可以绕过(配置不当/特殊需求)⚠️
如果你的业务 Service (a-api-service) 被你配置成了:
- Type: NodePort: 此时 K8s 会在所有节点上开一个端口(比如 31000)。外部用户如果知道这个端口,就可以直接
curl NodeIP:31000访问你的 Pod,完全绕过 Ingress 的路由规则、限流和鉴权 - Type: LoadBalancer: 云厂商会直接给这个 Service 分配一个公网 IP。外部流量直接打进来,绕过 Ingress
- 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 变成了集群唯一的”守门人”。
最佳实践建议
- 业务 Service 使用 ClusterIP(默认值,无需修改)
- 仅 Ingress Controller 使用 NodePort 或 LoadBalancer 作为外部入口
- 使用 NetworkPolicy 进一步限制 Pod 间的网络访问(可选,但推荐)
- 定期审计 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 端口已关闭,无法绕过。
实验总结
通过这个实验,我们验证了:
- ✅ ClusterIP 是安全的防线:只要 Service 是
ClusterIP,外部流量就”物理上”无法直接触达业务 Pod(除非宿主机本身有服务占用端口,但那是宿主机层面的,不是 Kubernetes 层面的) - ⚠️ NodePort 是潜在的漏洞:如果开发人员随意把业务 Service 改成
NodePort,就会导致 Ingress 被架空,所有安全策略失效 - 🔒 防御建议:这就是为什么在生产环境中,我们通常会配合 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