3. 独乐乐不如众乐乐。搭桥修路,迎接用户

0

上篇文章有提到,通过POD ID只能够在k8s集群内部进行访问,作为一个博客!只给自己看...好像也行啊...
但是每次都要先登录到集群节点中才能看...这就...(脑海里闪过成吨shell... ssh@192.10o... kubectl get po...,噗~!
这是在玩自己吗?

言归正传

集群都有了,服务也部了,咋就不能让他与万物互联了呢?
接下来我们就一同探秘,k8s中服务是如何暴露出去的

Kubernetes 的Pod的寿命是有限的。它们出生然后死亡,它们不会复活。ReplicationController是特别用来动态的创建和销毁Pods(如:动态伸缩或者执行rolling updates中动态的创建和销毁pod)。尽管每个Pod有自己的IP地址,随着时间变化随着时间推移即使这些IP地址也不能被认为是可靠的。这带来一个问题:如果一些Pod的集合(让我们称之为backends)为集群中的其他的Pod提供了一些功能(让我们称它们为frontends),这些frontends应该如何找到并一直知道哪些backends在这样的集合中呢?

Kubernetes的Service是一种抽象,它定义了一组Pods的逻辑集合和一个用于访问它们的策略 - 有的时候被称之为微服务。
举个例子,想象一下我们的博客后端运行了三个副本。这些副本都是可以替代的 - 前端不关心它们使用的是哪一个后端。尽管实际组成后端集合的Pod可能会变化,前端的客户端却不需要知道这个变化,也不需要自己有一个列表来记录这些后端服务。Service抽象能让你达到这种解耦。

定义一个Service

Kubernetes中的Service是一个REST对象,这点与Pod类似。正如所有的REST对象一样,向apiserver POST一个Service的定义就能创建一个新的实例。以上篇文章中的echoserver为例,每一个Pod都开放了80端口,并且都有一个"app=my-blog"的标签。

{
    "kind": "Service",
    "apiVersion": "v1",
    "metadata": {
        "name": "my-blog"
    },
    "spec": {
        "selector": {
            "app": "my-blog"
        },
        "ports": [
            {
                "protocol": "TCP",
                "port": 8080,
                "targetPort": 80
            }
        ]
    }
}

这个定义会创建一个新的Service对象,名字为”my-blog”,它指向所有带有”app=my-blog”标签的Pod上面的80端口。这个Service同时也会被分配一个IP地址(有时被称作”cluster ip”),它会被服务的代理所使用(见下面)。这个Service的选择器,会不断的对Pod进行筛选,并将结果POST到名字同样为“my-blog”的Endpoints对象。

注意一个Service能将一个来源的端口映射到任意的targetPort。默认情况下,targetPort会被设置成与port字段一样的值。可能更有意思的地方在于,targetPort可以是一个字符串,能引用一个后端Pod中定义的端口名。实际指派给该名称的端口号在每一个Pod中可能会不同。这为部署和更新你的Service提供了很大的灵活性。例如,你可以在你的后端的下一个版本中更改开放的端口,而无需导致客户出现故障。

Kubernetes的Service支持TCP和UDP协议。默认是TCP。

Endpoint

说到service不得不提 endpoint。在 Service 创建的同时,还生成了一个 Endpoints。 该 Endpoints 与 Service 同名,它所暴露的地址信息正是对应 Pod 的地址。由此猜测是 Endpoints 维护了 Service 与 Pod 的映射关系。
为了验证我们的猜测,我们手动删除 Endpoints,发现之前能成功访问到 Pod 的 VIP,现在已经已经访问不到了。

ServiceController 主要处理的还是与 LoadBalancer 相关的逻辑,但是 EndpointController 的作用就没有这么简单了,我们在使用 Kubernetes 时虽然很少会直接与 Endpoint 资源打交道,但是它却是 Kubernetes 中非常重要的组成部分。

EndpointController 本身并没有通过 Informer 监听 Endpoint 资源的变动,但是它却同时订阅了 Service 和 Pod 资源的增删事件,它会根据 Service 对象规格中的选择器 Selector 获取集群中存在的所有 Pod,并将 Service 和 Pod 上的端口进行映射生成一个 EndpointPort 结构体,对于每一个 Pod 都会生成一个新的 EndpointSubset,其中包含了 Pod 的 IP 地址和端口和 Service 的规格中指定的输入端口和目标端口,在最后 EndpointSubset 的数据会被重新打包并通过客户端创建一个新的 Endpoint 资源。所以,除了 Service 的变动会触发 Endpoint 的改变之外,Pod 对象的增删也会触发。

在我的理解,service就是将不断变动的pod做了一个固定的端口映射(别管pod怎么变化,怎么漂移,这是我的事),你只需要处理service暴露出来的固定端口即可。到底是谁来处理service暴露出来的端口呢?那当然是代理了。

kube-proxy

我们知道 Service 的代理是由 kube-proxy 实现的。而它的代理模式(Proxy mode)主要有两种:userspace 与 iptables。自 K8S v1.2 开始,默认的代理模式就是 iptables(/ipvs),并且它的性能也是要高于 userspace 的

userspace是在用户空间,通过kuber-proxy实现LB的代理服务。这个是kube-proxy的最初的版本,较为稳定,但是效率也自然不太高。
iptables的方式。是纯采用iptables来实现LB。是目前一般kube默认的方式。
ipvs:这种模式从Kubernetes 1.11进入GA,并在Kubernetes 1.12成为kube-proxy的默认代理模式。ipvs模式也是基于netfilter,对比iptables模式在大规模Kubernetes集群有更好的扩展性和性能,支持更加复杂的负载均衡算法(如:最小负载、最少连接、加权等),支持Server的健康检查和连接重试等功能。ipvs依赖于iptables,使用iptables进行包过滤、SNAT、masquared。ipvs将使用ipset需要被DROP或MASQUARED的源地址或目标地址,这样就能保证iptables规则数量的固定,我们不需要关心集群中有多少个Service了。

我们现在要做的呢,是将 VIP 请求给转发到对应的 Pod 上。而实现此的正是 iptables(/ipvs)。
举个例子,现在有podA,podB,podC和serviceAB。serviceAB是podA,podB的服务抽象(service)。 那么kube-proxy的作用就是可以将pod(不管是podA,podB或者podC)向serviceAB的请求,进行转发到service所代表的一个具体pod(podA或者podB)上。 请求的分配方法一般分配是采用轮询方法进行分配。

那有了kube-proxy,service是如何暴露的呢?
service的ServiceTypes能让你指定你想要哪一种服务。默认的和基础的是ClusterIP,这会开放一个服务可以在集群内部进行连接。NodePort 和LoadBalancer是两种会将服务开放给外部网络的类型。

apiVersion: v1
kind: Service
metadata:
  # Service 实例名称
  name: my-blog
spec:
  ports:
    - protocol: TCP
      # Service 端口地址
      port: 8080
      # Pod 端口地址
      targetPort: 80
  selector:
    # 匹配符合标签条件的 Pod
    app: my-blog
  type: NodePort    <- Service Type

ServiceType字段的合法值是:
ClusterIP: 仅仅使用一个集群内部的IP地址 - 这是默认值,在上面已经讨论过。选择这个值意味着你只想这个服务在集群内部才可以被访问到。
NodePort: 在集群内部IP的基础上,在集群的每一个节点的端口上开放这个服务。你可以在任意<NodeIP>:NodePort地址上访问到这个服务。

如果定义为NodePort,Kubernetes master 将从给定的配置范围内(默认:30000-32767)分配端口,每个 Node 将从该端口代理到 Service。该端口将通过 Service 的 spec.ports[*].nodePort 字段被指定。那么我们就可以使用任意节点IP:NodePort来访问到my-blog容器的80端口

LoadBalancer: 在使用一个集群内部IP地址和在NodePort上开放一个服务之外,向云提供商申请一个负载均衡器,会让流量转发到这个在每个节点上以<NodeIP>:NodePort的形式开放的服务上。
在使用一个集群内部IP地址和在NodePort上开放一个Service的基础上,还可以向云提供者申请一个负载均衡器,将流量转发到已经以NodePort形式开发的Service上。

将上述service文件保存并执行命令

$ kubectl create -f service.yaml

service/my-blog created

表明我们成功创建了一个使用NodePort方式暴露的service
下面我们来验证一下

clipboard.png

通过上第一行命令,我们可以看到刚刚创建的service已经以NodePort的方式对外暴露(第一个红框),暴露的端口号是32388,通过另外两行命令我们的项目my-blog部署在哪一台物理机节点上(对应EXTERNAL-IP就是物理机的真实IP),这里害怕遇到调皮的同学,所以给它加了个衣服:P

尝试通过IP+Port的方式请求一下

clipboard.png

Amazing! 通了!终于不是独乐乐了~

不过... 以用户的习惯,通过IP+端口号来访问的姿势可能无法接受
可能我们还需要再搞个域名,让用户可以直接通过域名来访问我们的博客
这时候有些机灵鬼就跳出来了,这还不简单!上面再搭个nginx不就行了...
确实是这样...但是如果我再多部署一个博客呢?(那就再加一条nginx解析记录呗...我们不是一直这样用的吗???
那...我们还搞个毛k8s
k8s可不是为了让你部署一两个服务而诞生的...它是为了成千上万个服务~(那你为什么在教我们用k8s部署博客???我:你今天有点话多
如果每一个服务都要手动去修改nginx解析记录...你就不怕什么时候你手多抖两下吗?

Ingress

总结一下上面service直接暴露服务的一些缺点

  • 一个app 需要占用一个主机端口
  • 端口缺乏管理
  • L4转发, 无法根据http header 和 path 进行路由转发

而Ingress就是为了解决上面的问题而诞生的
Ingress 的实现分为两个部分 Ingress Controller 和 Ingress .

  • Ingress Controller 是流量的入口,是一个实体软件, 一般是Nginx 和 Haproxy 。
  • Ingress 描述具体的路由规则。

Ingress Controller 会监听 api server上的 /ingresses 资源 并实时生效。
Ingerss 描述了一个或者多个 域名的路由规则,以 ingress 资源的形式存在。

简单说: Ingress 描述路由规则, Ingress Controller 负责动态实现规则。

下面我们来动手实现一下ingress,通过动手来学习
首先需要在我们的集群中安装ingress-nginx-controller
官方文档:https://kubernetes.github.io/...

// 登录集群,在集群中执行以下命令安装 ingress-nginx
kubectl apply -f https://raw.githubusercontent.com/kubernetes/ingress-nginx/master/deploy/static/mandatory.yaml

验证:

$ kubectl get pod -n ingress-nginx
NAME                                        READY   STATUS    RESTARTS   AGE
nginx-ingress-controller-66f7bd6b88-fwshc   1/1     Running   0          8s

说明安装成功
但是官方提供的这个yaml并没有指定nginx-ingress-controller自己本身的暴露方式
下面我们来手动设置以hostNetWork的方式暴露服务

$ kubectl edit deploy nginx-ingress-controller -n ingress-nginx
// 修改hostNetWork为true
spec.template.spec.hostNetWork: ture

deployment.extensions/nginx-ingress-controller edited  

修改成功!下面我们来创建一个ingress资源

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  name: my-blog
  namespace: test
  annotations:
    nginx.ingress.kubernetes.io/ssl-redirect: "false"  // 这个参数标识 不强制要求使用HTTPS协议进行通信,文章结尾会讲到https的支持
spec:
  rules:
  - host: blog.holdno.com
    http:
      paths:
      - path: /
        backend:
          serviceName: my-blog
          servicePort: 8080

通过上面的yaml文件我们可以很清晰的看出,我们想要对用户暴露的域名是 blog.holdno.com,解析规则是根路径"/",对应的service为my-blog,转发到service的8080端口(前面我们service暴露的是8080端口到POD的80端口)

$ kubectl create -f ingress.yaml

ingress.extensions/my-blog created

$ kubectl get ingress -n test
NAME      HOSTS             ADDRESS   PORTS   AGE
my-blog   blog.holdno.com             80      10s

ingress我们就部署成功了

clipboard.png

通过上面两条命令找到nginx-ingress-controller所在的节点的真实IP(在部署ingress时最好是指定某几台性能较好的节点,通过nodeSelector进行调度)
通过域名解析平台将我们的域名blog.holdno.com解析到节点IP上

curl blog.holdno.com 
// 下面会出现完美的一幕,大家亲手实践一下吧

到此,我们便成功的建立了从服务到用户之间的桥梁!之后无论POD怎么漂移,对于上层的ingress都是无谓的
虽然桥通了,但是感觉部署一个服务的过程很是心累...
后面的文章,我们将通过k8s提供的client-go来为大家梳理出一套自动化的部署流程,让我们的博客可以一键上云!

Ingress https

既然支持http了 自然少不了https的需求
那么我们就动手一步一步实现 ingress https的支持吧

首先需要了解的是 k8s内置的 secret资源,用来保存证书信息
我们通过阿里云购买一个免费的证书,并下载到机器上。(假设保存路径为 /ssl/blog.crt 和 /ssl/blog.key)

$ kubectl create secret tls my-secret --cert /ssl/blog.crt --key ./ssl/blog.key --namespace test // 一定要注意,secret是区分namespace的,不同namespace下的ingress资源和secret资源互相不可见  

$ secret/blog created

修改我们的ingress配置

$ kubectl edit ingress my-blog -n test  

apiVersion: extensions/v1beta1
kind: Ingress
metadata:
  annotations:
    kubectl.kubernetes.io/last-applied-configuration: |
      {"apiVersion":"extensions/v1beta1","kind":"Ingress","metadata":{"annotations":{"kubernetes.io/ingress.class":"nginx","nginx.ingress.kubernetes.io/ssl-redirect":"false"},"name":"my-blog","namespace":"test"},"spec":{"rules":[{"host":"blog.holdno.com","http":{"paths":[{"backend":{"serviceName":"my-blog","servicePort":777},"path":"/"}]}}]}}
    kubernetes.io/ingress.class: nginx
    nginx.ingress.kubernetes.io/ssl-redirect: "false" // 如果要强制使用https协议,需要将这个参数改为 true
  creationTimestamp: "2019-09-17T12:20:01Z"
  generation: 3
  name: my-blog
  namespace: test
  resourceVersion: "1107154526"
  selfLink: /apis/extensions/v1beta1/namespaces/test/ingresses/my-blog
  uid: 78bac7dc-d945-11e9-b8cf-9a285cd2373e
spec:
  rules:
  - host: blog.holdno.com
    http:
      paths:
      - backend:
          serviceName: my-blog
          servicePort: 8080
        path: /
  tls:  <- 关键配置
  - hosts:
    - blog.holdno.com # 证书对应的host
    secretName: blog  # 对应的证书  也就是我们上一步创建的secret
status:
  loadBalancer: {}  

ingress.extensions/my-blog edited

接下里我们就可以在浏览器中访问 https://blog.holdno.com 来验证一下。


如果觉得我的文章对你有用,请随意赞赏

你可能感兴趣的

载入中...