如何使用 Envoy Gateway 在裸金属集群暴露服务
- Xunzhuo
- Envoy gateway
- December 21, 2023
本文介绍如何通过 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 流程的图解
在上面的图表中,节点 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 的优点是:
- 你对你使用的 IP 有完全的控制
- 你可以使用属于你的 ASN 的 IP,而不是云提供商的 ASN。
External IP 的缺点是:
- 我们现在将要进行的简单设置并不是高可用的。这意味着如果节点死亡,服务将不再可达,你需要手动解决问题。
- 需要完成一些手动工作来管理 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
生成根证书和私钥 用来签署其他证书:
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
为
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"
使用根证书签署 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
将证书和密钥存储在 Kubernetes Secret 中:
kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt
更新 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", }], }, }, }]'
验证 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
测试:
由于打通了内外 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 协议,暴露到集群外。