3.9. Ingress 进阶:Contour 与 HTTPProxy

Ingress 只描述“域名和路径指向哪个 Service”,表达不了权重、镜像、重试和超时。Contour 用 Envoy 做数据平面,用 HTTPProxy 把七层流量策略变成类型化资源。本章从 Ingress v1 讲到 HTTPProxy,再完成流量分割与镜像实验。

层级:

L3 · 平台

难度:

进阶

预计:

90 分钟

前置:

Service 与 Ingress、TLS 基础

3.9.1. 你将学到

  • Ingress v1 的 ingressClassName、pathType 与 TLS 写法,以及 IngressClass 的作用。

  • Contour 的控制平面/数据平面分工,Envoy 如何通过 xDS 获得配置。

  • HTTPProxy 的 virtualhost、routes、conditions、services 与流量策略字段。

  • 用权重做流量分割、用 mirror 做影子流量,用 header 条件做灰度路由。

3.9.2. Ingress v1 与 IngressClass

networking.k8s.io/v1 在 v1.19 成为稳定版本,v1beta1 在 v1.22 被移除。v1 的关键变化是:ingressClassName 取代 kubernetes.io/ingress.class 注解,pathType 成为必填,后端改成嵌套的 service.name 与 service.port.number。pathType: Prefix 按路径段匹配,Exact 要求完全相等,ImplementationSpecific 则交给控制器解释。

IngressClass 描述控制器的名字与参数。集群里可以有多个 Ingress Controller,Ingress 通过 ingressClassName 选择其中一个;给 IngressClass 加 ingressclass.kubernetes.io/is-default-class: "true" 可以让未写 class 的 Ingress 落到它身上。Contour 的默认类名是 contour,对应 controller projectcontour.io/ingress-controller。

ingress-v1-demo.yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata: {name: demoapp, namespace: dev}
spec:
  ingressClassName: contour
  tls:
  - {hosts: ["www.ilinux.io"], secretName: tls-ingress-www-ilinux}
  rules:
  - host: www.ilinux.io
    http:
      paths:
      - path: /
        pathType: Prefix
        backend:
          service:
            name: demoapp
            port: {number: 80}

原书的 ingress-v1-demo.yaml 还留着 kubernetes.io/ingress.class 注解,同时没有写 ingressClassName。v1.31 上这样创建通常不会报错,但控制器可能根本不接管;迁移时把注解删掉,补上 ingressClassName。

3.9.3. Contour 架构

Contour 由两部分组成。控制平面是 projectcontour/contour Deployment,它 watch Ingress、HTTPProxy、Service、EndpointSlice 和 Secret,把期望的路由、集群、监听器计算成 Envoy 配置,通过 gRPC xDS 在 8001 端口下发给数据平面。数据平面是 envoy DaemonSet,直接监听节点的 80/443 或通过 NodePort/LoadBalancer Service 暴露,负责真正的 TLS 终止、路由和转发。

两者之间还有一份证书:contour-certgen Job 生成 contourcert 与 envoycert,contour 在 xDS 服务端启用 mTLS,Envoy 作为客户端出示证书,避免任何人伪造配置。ConfigMap contour 里的 contour.yaml 控制访问日志格式、最低 TLS 版本、fallback 证书等全局行为。

架构图

仓库里的 contour.yaml 是 v1.6.1 的整包清单,CRD 仍是 apiextensions.k8s.io/v1beta1,在 v1.31 上 apply 会失败。它适合阅读架构,不要直接用于新集群;安装时用官方 quickstart 或对应版本的 release manifest。

3.9.4. HTTPProxy 资源模型

HTTPProxy 是 Contour 自己的 CRD,比 Ingress 多出流量治理能力。一个 HTTPProxy 对应一个 virtualhost,fqdn 同时用于 Host 匹配和 TLS SNI;routes 按条件匹配请求,services 指定后端和权重。

字段

作用

virtualhost.fqdn

域名,也是证书 SNI

virtualhost.tls.secretName

同命名空间的 TLS Secret,Envoy 在此终止 TLS

virtualhost.tls.passthrough

不终止 TLS,按 SNI 转发 TCP

routes[].conditions

prefix 或 header,同一列表内是 AND

routes[].services

后端 name/port,可带 weight 或 mirror

permitInsecure: true 允许同一 fqdn 同时提供 HTTP 与 HTTPS;默认只接受 HTTPS。TLS Secret 必须与 HTTPProxy 同命名空间,跨命名空间引用要靠 TLSCertificateDelegation 显式授权。

3.9.5. 流量治理

路径与头路由:conditions 里写 prefix: /api 按路径匹配,写 header 按请求头匹配。多个 header 条件写在同一列表里是 AND 关系;没有条件的 route 作为兜底。把更具体的路由放在前面,兜底路由放最后,可读性最好。

流量分割与镜像:多个 service 的 weight 是相对权重,不是百分比;90/10 表示大约 9:1。mirror: true 的服务会收到请求的副本,但它的响应被丢弃,客户端只看到主服务的响应,适合在上线前用真实流量验证新版本。

重试、超时与健康检查:retryPolicy.count 是最大重试次数,perTryTimeout 限制单次尝试;timeoutPolicy.response 限制整个响应时间,idle 限制空闲连接。healthCheckPolicy 是 Envoy 主动探测,和 Kubernetes 的 readinessProbe 相互独立,两者都通过流量才会转发。

TLS 与 passthrough:常规模式由 Envoy 用 Secret 终止 TLS,后面走明文 HTTP;passthrough 模式只按 SNI 转发 TCP,Envoy 看不到 HTTP 内容,因此权重、头路由、重试全部失效。后端必须自己处理 TLS,适合 Kubernetes Dashboard、Redis 这类端到端加密服务。

httpproxy-tls-passthrough.yaml
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata: {name: dashboard, namespace: kubernetes-dashboard}
spec:
  virtualhost:
    fqdn: dashboard.ilinux.io
    tls: {passthrough: true}
  tcpproxy:
    services:
    - {name: kubernetes-dashboard, port: 443}

3.9.6. 与 ingress-nginx 的对比

维度

ingress-nginx

Contour

配置模型

大量 annotations

HTTPProxy CRD + Ingress

数据平面

NGINX

Envoy

流量分割

canary 注解

service weight

流量镜像

mirror 注解

service mirror: true

TLS passthrough

ssl-passthrough 注解

tls.passthrough + tcpproxy

ingress-nginx 的强项是生态成熟、注解覆盖广、调试资料多;Contour 的强项是类型化配置、动态更新和 Envoy 的 L7 能力。已有大量 nginx 注解的团队不必为了分流迁移,新建平台且需要精细流量治理时 Contour 更合适。

3.9.7. 动手实验

实验一:用 HTTPProxy 做 90/10 流量分割

目标:部署 Contour 与两个版本的 demoapp,把 90% 请求给 v1.0、10% 给 v1.1。

步骤 1 · 安装 Contour

kubectl apply -f https://projectcontour.io/quickstart/contour.yaml
kubectl -n projectcontour rollout status deploy/contour

步骤 2 · 部署两个版本

kubectl create namespace dev
kubectl -n dev create deployment demoapp --image=ikubernetes/demoapp:v1.0
kubectl -n dev create deployment demoappv11 --image=ikubernetes/demoapp:v1.1
kubectl -n dev expose deployment demoapp --port=80 --name=demoapp
kubectl -n dev expose deployment demoappv11 --port=80 --name=demoappv11

步骤 3 · 创建 HTTPProxy

httpproxy-traffic-splitting.yaml
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata: {name: demoapp-split, namespace: dev}
spec:
  virtualhost: {fqdn: www.ilinux.io}
  routes:
  - conditions: [{prefix: /}]
    services:
    - {name: demoapp, port: 80, weight: 90}
    - {name: demoappv11, port: 80, weight: 10}
kubectl -n dev apply -f httpproxy-traffic-splitting.yaml
kubectl -n dev get httpproxy
kubectl -n projectcontour port-forward svc/envoy 8080:80 &

步骤 4 · 验证比例

for i in $(seq 1 20); do
  curl -s -H 'Host: www.ilinux.io' http://127.0.0.1:8080/; echo
done | sort | uniq -c

预期大约 18 条来自 v1.0、2 条来自 v1.1。权重是概率,样本越小偏差越大;把权重改成 50/50 再跑 100 次,比例会更接近。kubectl -n dev describe httpproxy demoapp-split 可以看到 Valid 状态和每条 route 的后端。

实验二:用 mirror 复制线上流量

目标:把线上流量复制一份给 v1.1,观察镜像请求不影响主响应。

步骤 · 用 mirror 复制流量

httpproxy-traffic-mirror.yaml
apiVersion: projectcontour.io/v1
kind: HTTPProxy
metadata: {name: demoapp-mirror, namespace: dev}
spec:
  virtualhost: {fqdn: www.ilinux.io}
  routes:
  - conditions: [{prefix: /}]
    services:
    - {name: demoapp, port: 80}
    - {name: demoappv11, port: 80, mirror: true}

先删掉实验一的 HTTPProxy,避免同一个 fqdn 冲突:

kubectl -n dev delete httpproxy demoapp-split
kubectl -n dev apply -f httpproxy-traffic-mirror.yaml
kubectl -n dev logs -l app=demoappv11 --tail=10

客户端仍然只收到 v1.0 的响应,但 v1.1 的日志里会出现相同请求。镜像流量不参与响应,也不会因为镜像后端失败而影响主请求。

3.9.7.1. 验证清单

  • kubectl -n projectcontour get pods 中 contour 与 envoy 均 Running,Envoy Service 有地址。

  • kubectl -n dev get httpproxy 的 STATUS 为 Valid,FQDN 与预期一致。

  • 20 次请求中 v1.0 与 v1.1 的比例接近 9:1,且调整权重后比例随之变化。

  • 镜像模式下客户端只收到 v1.0,v1.1 日志能看到复制请求。

3.9.8. 常见问题与排错

现象

可能原因

处理方式

Ingress 创建成功但 404

没写 ingressClassName,或 IngressClass 不存在

kubectl get ingressclass;改用 ingressClassName: contour

HTTPProxy STATUS 为 Invalid

service 名/端口错误、fqdn 冲突、TLS Secret 缺失

kubectl describe httpproxy 看 message;同一 fqdn 只能有一个 HTTPProxy

Envoy 返回 503

后端 Endpoints 为空,或主动健康检查全部失败

查 Service selector 与 Endpoints;看 healthCheckPolicy 路径

TLS 握手失败

Secret 不在同一命名空间、证书与 fqdn 不匹配

查 kubectl -n dev get secret;跨命名空间用 TLSCertificateDelegation

passthrough 无法按路径/头路由

TCP 模式看不到 HTTP,按 SNI 整条转发

这是预期行为;需要 L7 路由就必须在 Envoy 终止 TLS

3.9.9. 进阶挑战

  1. 给 HTTPProxy 加 timeoutPolicy.response: 2s、retryPolicy.count: 3 和健康检查,用一个故意慢 3 秒的后端观察重试与超时。

  2. 用 TLSCertificateDelegation 让 dev 命名空间引用 cert-manager 命名空间的证书,再配 permitInsecure: false 强制 HTTPS。

3.9.10. 小结

  • Ingress v1 用 ingressClassName、pathType 和嵌套 backend;IngressClass 决定哪个控制器接管。

  • Contour 控制平面把对象翻译成 Envoy 配置,通过 xDS 下发;数据平面只负责转发。

  • HTTPProxy 用 weight 做分割、mirror 做影子流量、header/prefix 做路由,并补齐重试、超时与健康检查。

3.9.11. 延伸阅读