通过ingress暴露内部服务

在kubernetes集群中,有一个常见的需求:如何将内部服务暴露出来,供外部访问?

快速入门Kubernetes一节中,我们使用了Service(Load Balancer)的方式,对外暴露了nginx服务。试想:如果我们有100个内部Deployment,能够使用LB的方式,对外暴露么?

如果你还有印象,LB的对外暴露,要占用一个独立的端口,当需要暴露的服务增多时,光是端口的占用和分配,就已经是一个头疼的问题了。

实际上,Kubernetes为我们提供了三种暴露内部服务的机制:

  • NodePort:在Kubernetes的所有节点上,开放一个端口,转发到内部具体的service上,与LoadBalancer相比,它不会绑定外网IP,多用于临时用途(如debug)

  • LoadBalancer:每个服务可以绑定一个外网IP、端口,当需要暴露的服务不多时,这是官方推荐的选择。

  • Ingress:像一个“智能路由器”,对外只暴露一个IP/端口,可以根据路径、头信息等变量,自动转发到内部的多个不同服务上。

本节,我们将介绍两种不同的Ingress,来实现“暴露内部多组服务这个需求”。

七层ingress

首先,我们来看一下Nginx Ingress Controller,这是一款较早退出的Ingress方案,基于Nginx实现了应用层(http)协议的暴露。

我们在上一节的基础上,添加另一组deployment:

kubectl create deployment my-httpd --image=httpd:2.4
kubectl scale deployment my-httpd --replicas=3

同时,我们将之前创建的ngxin,也缩容为3:

kubectl scale deployment my-nginx --replicas=3
kubectl get pods                              
NAME                        READY   STATUS    RESTARTS   AGE
my-httpd-84bdf5b4d9-jjvwv   1/1     Running   0          46s
my-httpd-84bdf5b4d9-n269p   1/1     Running   0          16s
my-httpd-84bdf5b4d9-rw2kk   1/1     Running   0          16s
my-nginx-7bc876dc4b-226g9   1/1     Running   2          4h46m
my-nginx-7bc876dc4b-872v2   1/1     Running   2          4h46m
my-nginx-7bc876dc4b-fzr8s   1/1     Running   2          4h46m

接着,我们创建上述两个deployment的service:

kubectl expose deployment/my-nginx --port=80
kubectl expose deployment/my-httpd --port=80

kubectl get services
NAME         TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)   AGE
kubernetes   ClusterIP   10.96.0.1       <none>        443/TCP   9m16s
my-httpd     ClusterIP   10.102.22.9     <none>        80/TCP    4s
my-nginx     ClusterIP   10.109.10.111   <none>        80/TCP    9s

在配置ingress之前,我们首先要启用ingress:

minikube addons enable ingress

如果你使用的是MacOS,可能会报错,此时需要一些额外的配置,请参考这个帖子

接下来,我们创建ingress.yaml文件:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: homs-ingress
  annotations:
    kubernetes.io/ingress.class: nginx  
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  tls:
  - hosts:
    - homs.coder4.com
    secretName: homs-secret
  rules:
  - host: homs.coder4.com
    http:
      paths:
      - path: /my-nginx/?(.*)
        pathType: Prefix
        backend:
          service:
            name: my-nginx
            port:
              number: 80
      - path: /my-httpd/?(.*)
        pathType: Prefix
        backend:
          service:
            name: my-httpd
            port:
              number: 80

解释一下:

  • 我们定义了Nginx的Ingress,并使用了转发前清除前缀(rewrite-target配置)

  • 定义了两个不同的前缀my-nginx和my-httpd,通过前缀指向内部服务

  • 同时支持了http和https解析,但https是自签证书,所以后面我们只用http

然后创建它:

kubectl apply -f ./ingress.yaml

稍等一会,ingress的IP分配成功后如下所示:

kubectl get ingress
NAME           CLASS    HOSTS             ADDRESS        PORTS     AGE
homs-ingress   <none>   homs.coder4.com   192.168.64.3   80, 443   34s

如上所示,“192.168.64.3”就是分配的ingressIP,但我们需要用DNS访问它,这里,我使用nip.io这个黑魔法来避免需要修改hosts的问题,即修改上述yaml中的host为“192.168.64.3.nip.io”。

我们登录到minikube集群内部,尝试访问:

curl -kL "http://192.168.64.3.nip.io/my-httpd"
<html><body><h1>It works!</h1></body></html>

curl -kL "http://192.168.64.3.nip.io/my-nginx"
<!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>

如上,我们成功的用prefix的路径(my-nginx / my-httpd),访问了两个不同的内部service!

修改转发前缀

在上述的配置中,我们实现了多服务的转发,但准法后的location存在一些问题,我们换一个service验证一下:

kubectl create deployment service1 --image=mendhak/http-https-echo:23
kubectl create deployment service2 --image=mendhak/http-https-echo:23

对外暴露服务:

kubectl expose deployment/service1 --port=8080
kubectl expose deployment/service2 --port=808

修改一下ingress:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: homs-ingress
  annotations:
    kubernetes.io/ingress.class: nginx  
    nginx.ingress.kubernetes.io/rewrite-target: /$1
spec:
  tls:
  - hosts:
    - homs.coder4.com
    secretName: homs-secret
  rules:
  - host: homs.coder4.com
    http:
      paths:
      - path: /service1/?(.*)
        pathType: Prefix
        backend:
          service:
            name: service1
            port:
              number: 8080
      - path: /service2/?(.*)
        pathType: Prefix
        backend:
          service:
            name: service2
            port:
              number: 8080

登录到minikube后curl:

{
  "path": "/",
  "headers": {
    "host": "192.168.64.11.nip.io",
    "x-request-id": "7a00b30a5d4fd4c084d2bcfbfd44f636",
    "x-real-ip": "192.168.64.11",
    "x-forwarded-for": "192.168.64.11",
    "x-forwarded-host": "192.168.64.11.nip.io",
    "x-forwarded-port": "443",
    "x-forwarded-proto": "https",
    "x-scheme": "https",
    "user-agent": "curl/7.76.0",
    "accept": "*/*"
  },
  "method": "GET",
  "body": "",
  "fresh": false,
  "hostname": "192.168.64.11.nip.io",
  "ip": "192.168.64.11",
  "ips": [
    "192.168.64.11"
  ],
  "protocol": "https",
  "query": {},
  "subdomains": [
    "11",
    "64",
    "168",
    "192"
  ],
  "xhr": false,
  "os": {
    "hostname": "service2-5686d4f68c-4vz7d"
  },
  "connection": {}
}

观察上述输出,发现转发后的location被重定向了,如果我们的服务想收到完整的请求,如何实现呢?

我们可以修改ingress配置,在路径上添加一段分组匹配,如下:

apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
  name: homs-ingress
  annotations:
    kubernetes.io/ingress.class: nginx  
    nginx.ingress.kubernetes.io/rewrite-target: /$1$2
    nginx.ingress.kubernetes.io/app-root: /service1
spec:
  tls:
  - hosts:
    - homs.coder4.com
    secretName: homs-secret
  rules:
  - host: 192.168.64.11.nip.io 
    http:
      paths:
      - path: /(service1/?)(.*)
        pathType: Prefix
        backend:
          service:
            name: service1
            port:
              number: 8080
      - path: /(service2/?)(.*)
        pathType: Prefix
        backend:
          service:
            name: service2
            port:
              number: 8080

生效后,再次curl:

curl -kL "http://192.168.64.11.nip.io/service2"
{
  "path": "/service2",
  "headers": {
    "host": "192.168.64.11.nip.io",
    "x-request-id": "b5759cf6f47d0ed713142178ddea4f96",
    "x-real-ip": "192.168.64.11",
    "x-forwarded-for": "192.168.64.11",
    "x-forwarded-host": "192.168.64.11.nip.io",
    "x-forwarded-port": "443",
    "x-forwarded-proto": "https",
    "x-scheme": "https",
    "user-agent": "curl/7.76.0",
    "accept": "*/*"
  },
  "method": "GET",
  "body": "",
  "fresh": false,
  "hostname": "192.168.64.11.nip.io",
  "ip": "192.168.64.11",
  "ips": [
    "192.168.64.11"
  ],
  "protocol": "https",
  "query": {},
  "subdomains": [
    "11",
    "64",
    "168",
    "192"
  ],
  "xhr": false,
  "os": {
    "hostname": "service2-5686d4f68c-4vz7d"
  },
  "connection": {}
}

成功!

Nginx Ingress也支持通过不同的Host来区分不同Service,也支持nginx的部分自定义配置,推荐你阅读[官方ingress例子](Introduction - NGINX Ingress Controller)。

四层ingress

在上述两个例子中,我们实现了7层http协议的暴露 & 转发,ingress也支持4层的TCP协议。

为了防止影响,我们首先重置集群,并重新启用ingress。

minikube delete
minikube start
minikube addons enable ingress

接着,创建一个TCP的服务,我们以redis为例:

kubectl create deployment redis --image=redis:6

kubectl expose deployment/redis --port=6379

接着,我们创建映射关系,TCP的ingress是通过ConfigMap额外配置的。

apiVersion: v1
kind: ConfigMap
metadata:
  name: tcp-services
  namespace: ingress-nginx
data:
  6379: "default/redis:6379"

最后,我们将端口映射,修改到ingress上:

kubectl edit service -n ingress-nginx ingress-nginx-controller

在规则处添加如下代码:

  - name: redis
    port: 6379
    protocol: TCP
    targetPort: 6379

这里我们并没有填写nodePort,这是系统会自动分配的,不用我们手动处理。

保存成功后,我们尝试通过ingress的端口连接:

kubectl get services --all-namespaces
NAMESPACE       NAME                                 TYPE        CLUSTER-IP      EXTERNAL-IP   PORT(S)                                     AGE
default         kubernetes                           ClusterIP   10.96.0.1       <none>        443/TCP                                     35m
default         redis                                ClusterIP   10.109.20.237   <none>        6379/TCP                                    33m
ingress-nginx   ingress-nginx-controller             NodePort    10.110.48.51    <none>        80:30958/TCP,443:32737/TCP,6379:32765/TCP   34m
ingress-nginx   ingress-nginx-controller-admission   ClusterIP   10.103.12.249   <none>        443/TCP                                     34m
kube-system     kube-dns                             ClusterIP   10.96.0.10      <none>        53/UDP,53/TCP,9153/TCP                      35m

我们本地使用redis连接:

redis-cli -h $(minikube ip) -p 32765
> info
info
# Server
redis_version:6.2.6
redis_git_sha1:00000000
redis_git_dirty:0
redis_build_id:1527eab61b27d3bf
redis_mode:standalone
os:Linux 4.19.182 x86_64
arch_bits:64
multiplexing_api:epoll
.....

成功!

至此,我们成功使用Ingress暴露了内部的TCP端口。

如果你仔细对比HTTP和TCP的Ingress,不难发现:

  • HTTP的Ingress更加实用,可以通过不同Host甚至不同Path,区分多个内部Service

  • TCP的Ingress相对来说,比较"凑合",虽然能够工作,但配置繁琐、还需要耗费多个端口,并不方便。

因此,再实际工作中,如果想从k8s集群外访问集群内的TCP服务,多采用网络打通的方式进行,我们将在后续章节介绍这一功能。