Kubernetes 中的 Ingress:将应用公开到世界

1. 概述

当您的 cluster 内已有 Service 运行,但希望外部用户通过漂亮的域名(如 api.nonladev.com)访问,而不是混乱的 IP:port,这时就需要 Ingress。本文介绍 Ingress 是什么、与 Service type LoadBalancer 有何不同、如何安装 Ingress Controller 以及如何为常见场景编写 Ingress resource:基于路径的路由、基于主机的路由、TLS。阅读完毕后,您就能自己通过域名和 HTTPS 将 Spring Boot 应用公开到外部。

2. 为什么需要 Ingress

2.1. Service 的问题

Service type NodePort 或 LoadBalancer 虽然能用,但有局限性:

  • LoadBalancer:每个 Service 需要一个独立的外部 IP → 如果有很多 service 会很浪费(每个云 LB 都单独收费)。
  • NodePort:暴露奇怪的端口(30000-32767),没有 TLS termination,无法按域名/路径路由。

假设您有 5 个微服务需要对外公开,使用 LoadBalancer 意味着需要 5 个云 LB、5 个外部 IP。Ingress 通过使用一个入口点将流量路由到内部的多个 Service,基于主机名或路径来做出决定。

2.2. 什么是 Ingress

Ingress 是一个 API object,定义了从外部路由 HTTP/HTTPS 流量到 cluster 内 Service 的规则。Ingress 本身不做任何事情 — 它需要Ingress Controller(运行反向代理 Pod,如 NGINX、Traefik、HAProxy)来读取规则并执行。

Client → Ingress Controller (LoadBalancer/NodePort) → Ingress rules → Service → Pod

**注意:**如果 cluster 还没有安装 Ingress Controller,创建 Ingress resource 也没有作用 — 它只是配置文件“躺在那里等待”controller 读取。

3. 安装 Ingress Controller

最常见的是 ingress-nginx。用 Helm 安装:

<!-- 不适用 — 使用 Helm/kubectl,而不是 Maven dependency -->
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

此命令创建 namespace ingress-nginx,部署 Ingress Controller 并创建一个 type LoadBalancer 的 Service 以接收来自外部的流量。检查:

kubectl get pods -n ingress-nginx
kubectl get svc -n ingress-nginx
NAME                                       TYPE           EXTERNAL-IP     PORT(S)
ingress-nginx-controller                   LoadBalancer   34.123.45.67    80:31234/TCP,443:31235/TCP

这个外部 IP 正是 cluster 中所有 Ingress 的唯一入口点。

4. Deployment 和 Service 示例

在编写 Ingress 之前,需要有 Service 来路由到。假设应用 order-service:

apiVersion: apps/v1
kind: Deployment
metadata:
  name: order-service
spec:
  replicas: 2
  selector:
    matchLabels:
      app: order-service
  template:
    metadata:
      labels:
        app: order-service
    spec:
      containers:
        - name: order-service
          image: nonladev/order-service:1.0
          ports:
            - containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: order-service
spec:
  selector:
    app: order-service
  ports:
    - port: 80
      targetPort: 8080

Service order-service 暴露端口 80,转发到容器端口 8080。注意此 Service 不需要 type LoadBalancer,使用 ClusterIP(默认)就足够了,因为 Ingress Controller 会通过内部网络调用 Service。

5. 基础 Ingress resource — 基于主机的路由

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress
  annotations:
    nginx.ingress.kubernetes.io/rewrite-target: /
spec:
  ingressClassName: nginx
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

逐部分解释:

  • ingressClassName: nginx — 指定哪个 controller 处理这个规则(当 cluster 有多个 controller 时很重要)。
  • rules.host — 只有具有 header Host: api.nonladev.com 的流量才会匹配这个规则。
  • path + pathType: Prefix — 所有以 / 开头的请求(即全部)都会转发到 order-service:80。
  • annotation rewrite-target — 特定于 ingress-nginx,用于在转发前重写 URL(不同 controller 的 annotation 不同)。

应用并测试:

kubectl apply -f order-ingress.yaml
curl -H "Host: api.nonladev.com" http://34.123.45.67/
{"service":"order-service","status":"UP"}

在生产环境中,您需要将 DNS 记录 api.nonladev.com 指向 Ingress Controller 的外部 IP,不需要像上面那样假装 Host header。

6. 基于路径的路由 — 多个 service 共享域名

常见场景:一个域名,多个 service 对应不同的路径。

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: api-gateway-ingress
spec:
  ingressClassName: nginx
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /orders
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80
          - path: /customers
            pathType: Prefix
            backend:
              service:
                name: customer-service
                port:
                  number: 80

请求到 api.nonladev.com/orders/123 会路由到 order-service,而 /customers/55 会路由到 customer-service。pathType: Prefix 表示按前缀匹配 — 不同于 Exact(必须完全匹配)和 ImplementationSpecific(由 controller 自行解释,通常用于 regex)。

**注意:**在 Ingress v1 中,路径的顺序不影响匹配 — controller 会自动选择最长的匹配路径优先。但编写清晰的路径、避免重叠,可以防止难以调试的 bug。

7. TLS — Ingress 的 HTTPS

要使用 HTTPS,需要一个包含证书和密钥的 Secret,然后在 Ingress 中引用它。

kubectl create secret tls nonladev-tls \
  --cert=tls.crt --key=tls.key
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress-tls
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.nonladev.com
      secretName: nonladev-tls
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

spec.tls 声明哪个主机使用哪个证书。Controller 会自动进行 TLS termination — 即在 Ingress 处解密 HTTPS,然后将纯 HTTP 转发到内部的 Service。您的 Pod 不需要知道任何关于 TLS 的事情。

在实际应用中,您通常使用 cert-manager 来自动获取 Let’s Encrypt 证书,而不是手动创建 Secret:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: order-ingress-tls
  annotations:
    cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
  ingressClassName: nginx
  tls:
    - hosts:
        - api.nonladev.com
      secretName: nonladev-tls
  rules:
    - host: api.nonladev.com
      http:
        paths:
          - path: /
            pathType: Prefix
            backend:
              service:
                name: order-service
                port:
                  number: 80

annotation cluster-issuer 要求 cert-manager 自动创建和续期 Secret nonladev-tls — 不需要手动做任何事情。

8. 编写测试以验证 expose 后的端点

Ingress 启动后,您可以编写简单的集成测试来验证 controller class 正确处理路径(在 Spring Boot 应用层测试,不直接测试 YAML):

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class OrderControllerIT {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void shouldReturnOrderWhenCalledWithOrdersPrefix() {
        ResponseEntity<String> response =
                restTemplate.getForEntity("/orders/123", String.class);

        assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
        assertThat(response.getBody()).contains("orderId");
    }
}

此测试确保 Spring Boot 应用正确响应 Ingress rewrite 的路径 /orders/123 — 将后端代码错误与 Ingress 配置错误分开。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>

9. 常见错误

  • 遗漏 ingressClassName:如果 cluster 有多个 Ingress Controller(例如 nginx 和 traefik),Ingress 不清楚哪个 controller 处理它 → 可能无法工作或被两个都错误处理。
  • Service type 错误:后端 Service 设置为 LoadBalancer 没有必要,会浪费资源 — 只需 ClusterIP。
  • pathType 不符合预期:当需要 Prefix 时使用 Exact,导致子请求(/orders/123)返回 404,因为只有 /orders 完全匹配。
  • DNS 未指向:在配置真实 DNS 之前,使用 curl -H "Host: ..." 测试,避免浪费时间等待 DNS 传播。
  • Rewrite annotation 用于错误的 controller:ingress-nginx 的 annotation 不适用于 Traefik 或 AWS ALB Ingress Controller — 每个 controller 有自己的 annotation 集,要仔细阅读相应文档。

**何时不应使用 Ingress:**如果应用使用非 HTTP/HTTPS 协议(例如原始 TCP、复杂的 gRPC streaming 或 UDP),标准 Ingress 不支持很好 — 需要查看 Gateway API 或直接用 Service type LoadBalancer 来处理 TCP/UDP。

10. 总结

Ingress 是集中的 HTTP/HTTPS 路由层,帮助您通过单一入口点将多个 Service 公开到外部,节省 LoadBalancer 成本,并且便于管理域名/路径/TLS。关键要点:Ingress resource 只是规则,需要 Ingress Controller 来执行;选择正确的 ingressClassName 和 pathType;使用 cert-manager 来自动化 TLS。对于非纯 HTTP 的应用,考虑使用 Gateway API 而不是强行使用 Ingress。

kubernetesingressnetworkingdevopsnginx