如何使用 Envoy Gateway 在裸金属集群暴露服务

如何使用 Envoy Gateway 在裸金属集群暴露服务

本文介绍如何通过 Envoy Gateway + Gateway Address + External IP 暴露服务

Kubernetes External IP

当你构建一个裸金属 Kubernetes 集群时,你可能会遇到一个常见的问题,除了使用 NodePort,你不知道如何将你的 Kubernetes 服务暴露给互联网。如果你使用 NodePort 服务类型,它会分配一个高端口号并且你必须允许你的防火墙规则连接到这些端口。

这对你的基础设施不好,尤其是当你将服务器暴露给外部互联网时。不过,还有另一种整洁的方式可以将你的 Kubernetes 服务暴露给世界,并且你可以使用它的原始端口号。例如,你可以在端口 3306 而不是端口 32767 上将位于你的 Kubernetes 集群中的 MySQL 服务暴露给外部世界。

答案是使用 Kubernetes External IP。

我发现这个话题在 Envoy Gateway 中讨论得不多,可能是因为许多人使用云提供商的负载均衡器或者在本地部署中使用 Metal LB,不过近期有公司找到我,去讨论是否通过 hostnetwork 模式去直接将 EnvoyProxy 暴露,这种模式发现有很多安全性问题,推荐使用本文所介绍的方案,

什么是 External IP Service

从官方 Kubernetes 文档中,这是 External IP 的描述

如果有 External IP 路由到一个或多个集群节点,Kubernetes 服务可以在这些 External IP 上暴露。进入集群的流量,以服务端口上的External IP(作为目的 IP),将被路由到服务端点之一。External IP 不由 Kubernetes 管理,而是集群管理员的责任。

这个解释对大多数人来说是可以理解的。这里最重要的是确保使用哪个 IP 来达到 Kubernetes 集群。使用 External IP,我们可以将服务绑定到用于连接集群的 IP。

如果你简要了解 Kubernetes 网络是如何工作的,那就好了。如果你不熟悉它,请查看这篇博客文章 ,以详细了解它们。这里需要知道的最重要的是 Kubernetes 网络是与 Overlay 网络一起工作的。这意味着一旦你到达集群中的任何节点(主节点或工作节点),你就可以在集群中虚拟地到达任何地方。

Kubernetes External IP 流程的图解

image-20231221121723604

在上面的图表中,节点 1 和节点 2 都有 1 个 IP 地址。节点 1 上的 IP 地址 1.2.3.4 绑定到 httpd 服务,而实际的 pod 位于节点 2,IP 地址 1.2.3.6 绑定到 nginx 服务,实际的 pod 位于节点 1。底层的 Overlay 网络使这成为可能。当我们 curl IP 地址 1.2.3.4 时,我们应该看到来自 httpd 服务的响应,而 curl 1.2.3.5 时,我们应该看到来自 nginx 服务的响应。

External IP 的优点和缺点

External IP 的优点是:

  1. 你对你使用的 IP 有完全的控制
  2. 你可以使用属于你的 ASN 的 IP,而不是云提供商的 ASN。

External IP 的缺点是:

  1. 我们现在将要进行的简单设置并不是高可用的。这意味着如果节点死亡,服务将不再可达,你需要手动解决问题。
  2. 需要完成一些手动工作来管理 IP。IP 不是为你动态提供的,因此需要人工干预。

如何在 Envoy Gateway 中使用 External IP

Gateway API 提供了一个可选的 GatewayAddress 字段,描述绑定到 Gateway 的 IP。通过 GatewayAddress,Envoy Gateway 能够为 EnvoyProxy Service 设置 External IP 地址。

步骤

创建集群

创建 kind 集群,并设置端口映射,将内外部 80 和 443 打通:

Kind 配置如下,保存为 kind-config.yaml

# kind-config.yaml
kind: Cluster
apiVersion: kind.x-k8s.io/v1alpha4
nodes:
- role: control-plane
  extraPortMappings:
  - containerPort: 80
    hostPort: 80
    protocol: TCP
  - containerPort: 443
    hostPort: 443
    protocol: TCP

创建集群:

kind create cluster --name eg --config kind-config.yaml

安装 Envoy Gateway

helm install eg oci://docker.io/envoyproxy/gateway-helm --version v0.0.0-latest -n envoy-gateway-system --create-namespace

等待 Envoy Gateway Ready:

kubectl wait --timeout=5m -n envoy-gateway-system deployment/envoy-gateway --for=condition=Available

创建 GatewayClass、Gateway、HTTPRoute 以及 Backend

创建 GatewayClass

cat <<EOF | kubectl apply -f -
kind: GatewayClass
apiVersion: gateway.networking.k8s.io/v1
metadata:
  name: eg
spec:
  controllerName: gateway.envoyproxy.io/gatewayclass-controller
EOF

创建 Gateway

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: Gateway
metadata:
  name: eg
spec:
  gatewayClassName: eg
  listeners:
    - name: http
      protocol: HTTP
      port: 80
EOF

创建 Backend

cat <<EOF | kubectl apply -f -
apiVersion: v1
kind: ServiceAccount
metadata:
  name: backend
---
apiVersion: v1
kind: Service
metadata:
  name: backend
  labels:
    app: backend
    service: backend
spec:
  ports:
    - name: http
      port: 3000
      targetPort: 3000
  selector:
    app: backend
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: backend
spec:
  replicas: 1
  selector:
    matchLabels:
      app: backend
      version: v1
  template:
    metadata:
      labels:
        app: backend
        version: v1
    spec:
      serviceAccountName: backend
      containers:
        - image: gcr.io/k8s-staging-ingressconformance/echoserver:v20221109-7ee2f3e
          imagePullPolicy: IfNotPresent
          name: backend
          ports:
            - containerPort: 3000
          env:
            - name: POD_NAME
              valueFrom:
                fieldRef:
                  fieldPath: metadata.name
            - name: NAMESPACE
              valueFrom:
                fieldRef:
                  fieldPath: metadata.namespace
EOF

创建 HTTPRoute:

cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
  name: backend
spec:
  parentRefs:
    - name: eg
  hostnames:
    - "www.example.com"
  rules:
    - backendRefs:
        - group: ""
          kind: Service
          name: backend
          port: 3000
          weight: 1
      matches:
        - path:
            type: PathPrefix
            value: /
EOF

查看 Gateway、Service 状态

Gateway:

❯ kubectl get gateway
NAME   CLASS   ADDRESS   PROGRAMMED   AGE
eg     eg                False        106s

Service:

❯ kubectl get svc -n envoy-gateway-system
NAME                            TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)               AGE
envoy-default-eg-e41e7b31       LoadBalancer   10.96.65.134   <pending>     80:30806/TCP          2m3s

可以看到默认创建的 LB 类型的 Service 因为没有 LoadBalancer 的实现,所以一直处于 Pending 的状态。

使用 GatewayAddress 指定 External IP

获取 Node IP:

❯ kg node -owide
NAME               STATUS   ROLES           AGE   VERSION   INTERNAL-IP   EXTERNAL-IP   OS-IMAGE             KERNEL-VERSION     CONTAINER-RUNTIME
eg-control-plane   Ready    control-plane   27m   v1.25.3   172.18.0.2    <none>        Ubuntu 22.04.1 LTS   5.15.49-linuxkit   containerd://1.6.9

将 node IP 写入 Gateway Address:

kubectl patch gateway eg --type=json --patch '[{
   "op": "add",
   "path": "/spec/addresses",
   "value": [{
      "type": "IPAddress",
      "value": "172.18.0.2"
   }]
}]'

查看 Gateway、Service 状态

Gateway:

❯ kubectl get gateway
NAME   CLASS   ADDRESS      PROGRAMMED   AGE
eg     eg      172.18.0.2   True         3m40s

Service:

❯ kubectl get svc -n envoy-gateway-system
NAME                            TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)               AGE
envoy-default-eg-e41e7b31       LoadBalancer   10.96.65.134   172.18.0.2    80:30806/TCP          3m46s

可以看到 Gateway 已经将 172.18.0.2 写入 Address,Service 的 External IP 也成功设置。

测试 HTTPRoute

我们映射了内部 80 端口到外部 80 端口,直接通过外部访问 80 端口:

❯ curl --verbose --header "Host: www.example.com" http://localhost:80/get
*   Trying 127.0.0.1:80...
* Connected to localhost (127.0.0.1) port 80 (#0)
> GET /get HTTP/1.1
> Host: www.example.com
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/1.1 200 OK
< content-type: application/json
< x-content-type-options: nosniff
< date: Thu, 21 Dec 2023 04:58:03 GMT
< content-length: 508
< x-envoy-upstream-service-time: 2
< server: envoy
<
{
 "path": "/get",
 "host": "www.example.com",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.1.2"
  ],
  "X-Envoy-Expected-Rq-Timeout-Ms": [
   "15000"
  ],
  "X-Envoy-Internal": [
   "true"
  ],
  "X-Forwarded-For": [
   "172.18.0.1"
  ],
  "X-Forwarded-Proto": [
   "http"
  ],
  "X-Request-Id": [
   "b272077f-38cf-48db-aea9-1f7c6ca894f1"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "backend-74888f465f-h27kk"
* Connection #0 to host localhost left intact
}%

测试 HTTPS

  1. 生成根证书和私钥 用来签署其他证书:

    openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt
    
  2. www.example.com 创建证书签名请求 (CSR) 和一个新的私钥:

    openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization"
    
  3. 使用根证书签署 CSR 生成 www.example.com 的证书:

    openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt
    
  4. 将证书和密钥存储在 Kubernetes Secret 中

    kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt
    
  5. 更新 Kubernetes Gateway,包括一个监听端口 443 的 HTTPS 监听器,并引用 example-cert Secret:

    kubectl patch gateway eg --type=json --patch '[{
      "op": "add",
      "path": "/spec/listeners/-",
      "value": {
       "name": "https",
       "protocol": "HTTPS",
       "port": 443,
       "tls": {
         "mode": "Terminate",
         "certificateRefs": [{
           "kind": "Secret",
           "group": "",
           "name": "example-cert",
         }],
       },
     },
    }]'
    
  6. 验证 Gateway 状态

    ❯ kubectl get gateway/eg
    NAME   CLASS   ADDRESS      PROGRAMMED   AGE
    eg     eg      172.18.0.2   True         10m
    ❯ kubectl get svc -n envoy-gateway-system
    NAME                            TYPE           CLUSTER-IP     EXTERNAL-IP   PORT(S)                      AGE
    envoy-default-eg-e41e7b31       LoadBalancer   10.96.65.134   172.18.0.2    80:30806/TCP,443:32624/TCP   11m
    
  7. 测试:

由于打通了内外 443 端口,直接通过外部 443 端口访问测试

❯ curl -v -HHost:www.example.com --resolve "www.example.com:443:127.0.0.1" \
--cacert example.com.crt https://www.example.com/get
* Added www.example.com:443:127.0.0.1 to DNS cache
* Hostname www.example.com was found in DNS cache
*   Trying 127.0.0.1:443...
* Connected to www.example.com (127.0.0.1) port 443 (#0)
* ALPN: offers h2,http/1.1
* (304) (OUT), TLS handshake, Client hello (1):
*  CAfile: example.com.crt
*  CApath: none
* (304) (IN), TLS handshake, Server hello (2):
* (304) (IN), TLS handshake, Unknown (8):
* (304) (IN), TLS handshake, Certificate (11):
* (304) (IN), TLS handshake, CERT verify (15):
* (304) (IN), TLS handshake, Finished (20):
* (304) (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / AEAD-CHACHA20-POLY1305-SHA256
* ALPN: server accepted h2
* Server certificate:
*  subject: CN=www.example.com; O=example organization
*  start date: Dec 21 05:03:44 2023 GMT
*  expire date: Dec 20 05:03:44 2024 GMT
*  common name: www.example.com (matched)
*  issuer: O=example Inc.; CN=example.com
*  SSL certificate verify ok.
* using HTTP/2
* h2 [:method: GET]
* h2 [:scheme: https]
* h2 [:authority: www.example.com]
* h2 [:path: /get]
* h2 [user-agent: curl/8.1.2]
* h2 [accept: */*]
* Using Stream ID: 1 (easy handle 0x154015600)
> GET /get HTTP/2
> Host:www.example.com
> User-Agent: curl/8.1.2
> Accept: */*
>
< HTTP/2 200
< content-type: application/json
< x-content-type-options: nosniff
< date: Thu, 21 Dec 2023 05:07:33 GMT
< content-length: 509
< x-envoy-upstream-service-time: 1
< server: envoy
<
{
 "path": "/get",
 "host": "www.example.com",
 "method": "GET",
 "proto": "HTTP/1.1",
 "headers": {
  "Accept": [
   "*/*"
  ],
  "User-Agent": [
   "curl/8.1.2"
  ],
  "X-Envoy-Expected-Rq-Timeout-Ms": [
   "15000"
  ],
  "X-Envoy-Internal": [
   "true"
  ],
  "X-Forwarded-For": [
   "172.18.0.1"
  ],
  "X-Forwarded-Proto": [
   "https"
  ],
  "X-Request-Id": [
   "36474a1f-e0a1-47dc-97b1-3e4ec1bf77d6"
  ]
 },
 "namespace": "default",
 "ingress": "",
 "service": "",
 "pod": "backend-74888f465f-h27kk"
* Connection #0 to host www.example.com left intact
}%

至此,你已经通过了 Envoy Gateway,在无 Loadbalancer 实现的集群,使用 Gateway Address 通过 Service External IP,将 80 / 443 端口的 LB 类型服务,通过 HTTP / HTTPS 协议,暴露到集群外。

comments powered by Disqus

Related Posts

Service 中的端口

Service 中的端口

本文简单对比 Service 几种类型

Read More
浅谈 Golang 代码覆盖率

浅谈 Golang 代码覆盖率

本文浅谈一下 Golang 代码测试覆盖率的一些细节与原理.

Read More
Envoy Gateway 通往 GA 之路

Envoy Gateway 通往 GA 之路

本文介绍了 Envoy Gateway 对 GA 做了哪些准备

Read More