Kubernetes 学习笔记(二) - 部署和访问应用

前边折腾了各种安装 Kubernetes 集群的操作,还跑到 AWS 上撸了一把 EKS,也在 Kubernetes 上部署过服务。继续更深一步的学习如何部署应用和怎么通过 Service 去访问 Pod 中的应用,顺带看看内部的网络是怎么流转的。

测试平台还是以本地启动的三个 Vagrant 虚拟机组成的 Kubernetes 集群,安装方法见 Kubernetes 学习笔记(一) - 初上手

  1. k8s-master (172.28.128.14)
  2. k8s-node1 (172.28.128.10)
  3. k8s-node2 (172.28.128.11)

测试应用的镜像为 yanbin/python-web, 代码见 github 上的 yabqiu/python-web-docker/app.py, 一个默认启动在  80 端口上的 Flask Web 应用,输出为当前 hostname  和一个唯一标识符。

部署应用

《每天5分玩转Kubernetes》里用的 Kubernetes 是 1.7 版本,其中还在用 kubectl run 的方式来部署应用(它会产生一个隐式的 deployment 对象),该方式已在 Kubernetes 1.12 中不推荐使用了,建议用 kubectl create deployment...,而实际中更应该用 yaml 文件编排后再 kubectl apply -f <your-yaml-file>, 这样多种对象可以编写在一起,更方便日后同样的命令更新各种对象,或者用 kubectl delete -f <your-yam-file> 批量删除所创建的对象。

命令方式部署

命令方式创建一个 deployment 对象,并让它启动 3 个 pod
$ kubectl create deployment python-web-app --image=yanbin/python-web:latest
$ kubectl scale -n default deployment python-web-app --replicas=3
等它们全部就续后
1$ kubectl get deploy -A
2NAMESPACE     NAME             READY   UP-TO-DATE   AVAILABLE   AGE
3default       python-web-app   3/3     3            3           3m47s
4kube-system   coredns          2/2     2            2           117m
5$ kubectl get pods -o wide
6NAME                              READY   STATUS    RESTARTS   AGE     IP           NODE       ...
7python-web-app-68d7bbd7f5-dzjxf   1/1     Running   0          3m43s   10.244.2.3   k8s-node2  ...
8python-web-app-68d7bbd7f5-jt54g   1/1     Running   0          3m42s   10.244.2.4   k8s-node2  ...
9python-web-app-68d7bbd7f5-l5wkb   1/1     Running   0          3m48s   10.244.1.2   k8s-node1  ...

看到创建了一个 python-web-app deployment 对象,并按照要求启动了三个 pod,分配到了两个工作节点上。先不讨论怎么去访问上面启动的服务,而是应该了解用 yaml 文件的部署方式。在这之前我们把前面的 deployment 对象删掉,命令是
$ kubectl delete deploy python-web-app
这会把 deployment 对象并先前的三个 python-web-app-* pod 全部清除掉。

yaml 文件方式部署

需创建一个描述文件, 我们命名为 python-web-app.yaml(也可用 yml 为文件后缀),内容如下:
 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: python-web-app
 5spec:
 6  replicas: 3
 7  selector:
 8    matchLabels:
 9      app: python-web-app
10  template:
11    metadata:
12      labels:
13        app: python-web-app
14    spec:
15      containers:
16      - name: python-web
17        image: yanbin/python-web:latest

然后只要一条命令就完成了部署并启动三个 pod
$ kubectl apply -f python-web-app.yaml
相应的删除都不用事先查找到 deployment 对象是什么名称,而只需
$ kubectl delete -f python-web-app.yaml
以后有什么修改的话,只要改下 python-web-app.yaml 文件,比如把 replicas 调整为 4, 接着再做一遍 kubectl apply -f python-web-app.yaml 就行了。

所以从以上操作不难想像,使用 yaml 文件来管理 Kubernetes 中的对象还能把对象状态存储到版本服务器上。

通过 Service 访问 Pod 应用

回到前面搁下的话题,这是个大话题,将会涉及到 LoadBalance, Cluster IP 和 NodePort 的概念。现在我们看到了启动了三个 python-web-app pod, 它们是运行在不同工作节点上的 docker 容器,隔着有两层,又没有暴露出端口,该如何它们呢?

Pod IP 地址访问

最直接的想法是可以登陆到具体的 pod(docker 容器, 这儿一个 pod 中只运行了一个 docker 容器),访问它的 80 端口上的 web 服务
root@k8s-master# kubectl exec -it python-web-app-68d7bbd7f5-dzjxf -- /bin/sh
/ # hostname
python-web-app-68d7bbd7f5-dzjxf
/ # ifconfig | grep 10.244
inet addr:10.244.2.3 Bcast:0.0.0.0 Mask:255.255.255.0
/ # wget -qO- 10.244.2.3:80
Served by python-web-app-68d7bbd7f5-dzjxf/4a8a9e75
/ # exit
root@k8s-master# kubectl exec -it python-web-app-68d7bbd7f5-l5wkb -- /bin/sh
/ # hostname
python-web-app-68d7bbd7f5-l5wkb
/ # wget -qO- localhost
Served by python-web-app-68d7bbd7f5-l5wkb/916a8ed9
Alpine Linux 自带 wget 命令,不用安装 curl 也行

进入到 docker 容器去访问服务肯定是没什么意义的,或许通过 worker 节点稍微现实一些,可是我们启动 docker 容器时未映射端口。来到
python-web-app-68d7bbd7f5-dzjxf 1/1 Running 0 3m43s 10.244.2.3 k8s-node2
所在的 worker 节点 k8s-node2 上
1root@k8s-node2:/# docker ps
2CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS  ...
310ebc5d4ecdd        yanbin/python-web       "python app.py"          11 hours ago        Up 11 hours                ...

端口未映射没法通过节点访问
root@k8s-node1:/# curl 10.244.2.3
Served by python-web-app-68d7bbd7f5-dzjxf/4a8a9e75
发现不需要在 python-web-app.yaml 中加
ports:
- containerPort: 80
也能在各工作节点上访问到所运行的容器的 80 端口

ClusterIP 访问服务

我们进一步修改部署文件为
 1apiVersion: apps/v1
 2kind: Deployment
 3metadata:
 4  name: python-web-app
 5spec:
 6  replicas: 3
 7  selector:
 8    matchLabels:
 9      app: python-web-app
10  template:
11    metadata:
12      labels:
13        app: python-web-app
14    spec:
15      containers:
16      - name: python-web
17        image: yanbin/python-web:latest
18        ports:
19        - containerPort: 80
20---
21apiVersion: v1
22kind: Service
23metadata:
24  name: python-web-svc
25spec:
26  selector:
27    app: python-web-app
28  ports:
29  - protocol: TCP
30    port: 80
31    targetPort: 80

再次 kubectl apply -f python-web-app.yaml, 查看 service 的 CLUSTER-IP

--- 可以把多个对象的配置放在同一个文件中,其实上面的 Service 等价于下面的命令(中括号中内容可选)
# kubectl expose deployment python-web-app --name=python-web-svc --port 80 [--target-port 80 --protocol TCP]
1root@k8s-master:/# kubectl get svc
2NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
3kubernetes       ClusterIP   10.96.0.1        <none>        443/TCP   6h19m
4python-web-svc   ClusterIP   10.110.181.172   <none>        80/TCP    6h15m

现在可以在任意工作节点上用上面的 CLUSTER-IP 来访问容器内的服务
root@k8s-master:/# curl 10.110.181.172
Served by python-web-app-78b9d9d7f-f2gxt/c5413dba
root@k8s-master:/# curl 10.244.2.7
Served by python-web-app-78b9d9d7f-m478c/32c2f2bd
root@k8s-master:/# curl 10.244.2.8
Served by python-web-app-78b9d9d7f-tzqml/98e656a6
这里的 ClusterIP 10.110.181.172 实质是一个 iptables 实现的虚拟 IP,同样是由 iptables 实现了负载均衡

域名访问 Service

这里的域名是 Service 的 ClientIP 地址对应有一个域名,所以它仍然是局限于内部访问服务。Kubernetes 集群有自己的 DNS 服务(kube-dns)
1root@k8s-master:/# k get deploy -n kube-system
2NAME      READY   UP-TO-DATE   AVAILABLE   AGE
3coredns   2/2     2            2           6h39m
4root@k8s-master:/home/vagrant# k get pods -n kube-system -o wide | grep dns
5coredns-66bff467f8-2m26v             1/1     Running   0          6h39m   10.244.2.2      k8s-node2
6coredns-66bff467f8-5gvf9             1/1     Running   0          6h39m   10.244.1.2      k8s-node1

DNS 是给 Pod 用的,所以只要工作节点上有。

Kubernetes 中每一个服务都有自己的名称,完整名称为 <SERVICE_NAME>.<NAMESPACE_NAME>。所以我们进到某一个 Pod
 1root@k8s-master:/# kubectl get svc
 2NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)   AGE
 3kubernetes       ClusterIP   10.96.0.1        <none>        443/TCP   6h43m
 4python-web-svc   ClusterIP   10.104.132.106   <none>        80/TCP    11m
 5root@k8s-master:/# kubectl exec -it python-web-app-68d7bbd7f5-dzjxf -- sh
 6/ # cat /etc/resolv.conf
 7nameserver 10.96.0.10
 8search default.svc.cluster.local svc.cluster.local
 9options ndots:5
10/ # wget -qO- python-web-svc
11Served by python-web-app-78b9d9d7f-f2gxt/c5413dba
12/ # wget -qO- python-web-svc.default
13Served by python-web-app-78b9d9d7f-m478c/32c2f2bd
14/ # ping python-web-svc
15PING python-web-svc (10.104.132.106): 56 data bytes

虽然在 Pod 内部,对 python-web-svcpython-web-svc.default 都会解析到  ClusterIP 10.104.132.106 上,因此与访问 ClusterIP  一样是通 iptables 实现的负载均衡。

这里有点绕,要进到 Pod 容器内部才能使用域名来访问 Service 对应的 ClusterIP,显然不是为外部提供服务的,它的关键用途是让 Pod(容器) 之间用 Service 名称互相访问。

下边是两种可由外部访问 Service

对于需向外提供服务的应用,这个才是 Kubernetes 的关键。可以通过 NodePort 或 LoadBalancer 的方式。

NodePort 方式

修改前面的 python-web.app.yaml 文件中的 Service 部分如下:
 1apiVersion: v1
 2kind: Service
 3metadata:
 4  name: python-web-svc
 5spec:
 6  type: NodePort
 7  selector:
 8    app: python-web-app
 9  ports:
10  - protocol: TCP
11    port: 80
12    targetPort: 80

应用它 kubectl apply -f python-web-app.yaml,默认时每个节点上会分配一个 3000~32767 之间的端口与服务端口映射,我们也可以在 yaml 文件中指定 nodePort 的值。
 1root@k8s-master:/# kubectl get svc
 2NAME             TYPE        CLUSTER-IP       EXTERNAL-IP   PORT(S)        AGE
 3kubernetes       ClusterIP   10.96.0.1        <none>        443/TCP        7h13m
 4python-web-svc   NodePort    10.104.132.106   <none>        80:32592/TCP   41m
 5root@k8s-master:/# k get nodes -o wide
 6NAME         STATUS   ROLES    AGE     VERSION   INTERNAL-IP 
 7k8s-master   Ready    master   7h13m   v1.18.1   172.28.128.10
 8k8s-node1    Ready    <none>   7h13m   v1.18.1   172.28.128.11
 9k8s-node2    Ready    <none>   7h13m   v1.18.1   172.28.128.12
10
11root@k8s-master:/# curl 172.28.128.11:32592
12Served by python-web-app-78b9d9d7f-f2gxt/c5413dba
13root@k8s-master:/# curl 172.28.128.12:32592
14Served by python-web-app-78b9d9d7f-m478c/32c2f2bd

通过 Kubernetes 集群中任意节点的 IP 就可以访问容器内部的应用。工作节点的机器从架构上虽然是可以让外部进行访问,但这样做是不安全的。通过 NodePort 访问服务并非只是访问内部实现依然是借助于 iptables。

LoadBalancer 方式

需像 NodePort 一样,把 Service 部分的 type 改为  LoadBalancer
 1apiVersion: v1
 2kind: Service
 3metadata:
 4  name: python-web-svc
 5spec:
 6  type: LoadBalancer
 7  selector:
 8    app: python-web-app
 9  ports:
10  - protocol: TCP
11    port: 80
12    targetPort: 80

type 为 LoadBalancer 时会请求云服务提供商给它一个 EXTERAL-IP, 由此来向外提供服务。目前支持提供 LoadBalancer 的有 GCP, AWS, Azure 等。下面以 AWS 为例
1➜  $ kubectl get svc
2NAME             TYPE           CLUSTER-IP     EXTERNAL-IP                                 PORT(S)        AGE
3kubernetes       ClusterIP      10.100.0.1     <none>                                      443/TCP        45m
4python-web-svc   LoadBalancer   10.100.87.28   af611e...2083.us-east-1.elb.amazonaws.com   80:32624/TCP   36m

这样通过那个 ELB 域名就能连接到节点的 32624 端口,像 NodePort 一样访问到了每一个 Pod 的服务。

集群内部的 LoadBalance

前面列出各种访问 Kubernetes 内服务的方式:

  1. Pod IP 地址访问:确切来讲,直接用 Pod IP 访问的是容器内应用而非 Kubernetes 服务,只宜用于诊断容器内程序
  2. Cluster IP 访问 Service
  3. 域名访问 Service:在 Pod 容器内部用域名方式解析到 Cluster IP 上
  4. NodePort 访问 Service
  5. LoadBalancer 访问 Service

以上除第一种方式外,只要发起了请求或者是外部的请求进入了集群,Kubernetes 自己的负载均衡(请求分布)便介入了工作。请求首先会被引到 ClusterIP 上来,这是一个 iptables 实现的虚拟 IP。在集群内部协助它实现的是节点之间的网络服务,如 flannel 网络,它是运行在每一个节点上的 Daemon Pod。
1root@k8s-master:~# kubectl get pod -n kube-system -o wide
2NAME                                 READY   STATUS    RESTARTS   AGE   IP              NODE     
3kube-flannel-ds-amd64-jfn2s          1/1     Running   0          9h    172.28.128.12   k8s-node2  
4kube-flannel-ds-amd64-sqh42          1/1     Running   0          9h    172.28.128.10   k8s-master 
5kube-flannel-ds-amd64-zzgsr          1/1     Running   0          9h    172.28.128.11   k8s-node1  

查看某一个 kube-flannel 上的日志如下:
 1root@k8s-master:~# kubectl logs kube-flannel-ds-amd64-jfn2s -n kube-system | tail -15
 2I0409 17:42:14.180010       1 vxlan_network.go:60] watching for new subnet leases
 3I0409 17:42:14.182489       1 iptables.go:145] Some iptables rules are missing; deleting and recreating rules
 4I0409 17:42:14.182506       1 iptables.go:167] Deleting iptables rule: -s 10.244.0.0/16 -j ACCEPT
 5I0409 17:42:14.182891       1 iptables.go:145] Some iptables rules are missing; deleting and recreating rules
 6I0409 17:42:14.183330       1 iptables.go:167] Deleting iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
 7I0409 17:42:14.185017       1 iptables.go:167] Deleting iptables rule: -d 10.244.0.0/16 -j ACCEPT
 8I0409 17:42:14.279752       1 iptables.go:167] Deleting iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE --random-fully
 9I0409 17:42:14.280162       1 iptables.go:155] Adding iptables rule: -s 10.244.0.0/16 -j ACCEPT
10I0409 17:42:14.282071       1 iptables.go:167] Deleting iptables rule: ! -s 10.244.0.0/16 -d 10.244.2.0/24 -j RETURN
11I0409 17:42:14.383168       1 iptables.go:155] Adding iptables rule: -d 10.244.0.0/16 -j ACCEPT
12I0409 17:42:14.384063       1 iptables.go:167] Deleting iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE --random-fully
13I0409 17:42:14.386864       1 iptables.go:155] Adding iptables rule: -s 10.244.0.0/16 -d 10.244.0.0/16 -j RETURN
14I0409 17:42:14.389205       1 iptables.go:155] Adding iptables rule: -s 10.244.0.0/16 ! -d 224.0.0.0/4 -j MASQUERADE --random-fully
15I0409 17:42:14.484011       1 iptables.go:155] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.2.0/24 -j RETURN
16I0409 17:42:14.488072       1 iptables.go:155] Adding iptables rule: ! -s 10.244.0.0/16 -d 10.244.0.0/16 -j MASQUERADE --random-fully

在任意节点上也可以用 iptables-save 查看当前节点的 iptables 怎么去按比例分发请求到相应的服务对应的 Pod 上去的。每次新建一个 Kubernetes 服务,或者 Deployment 发生更新后都会有一系列的 iptables 规则的变更。

我们可以查看一个服务的描述
 1root@k8s-master:~# kubectl describe svc  python-web-svc
 2Name:                     python-web-svc
 3Namespace:                default
 4Labels:                   <none>
 5Annotations:              Selector:  app=python-web-app
 6Type:                     NodePort
 7IP:                       10.104.132.106
 8LoadBalancer Ingress:     af611e.....082083.us-east-1.elb.amazonaws.com
 9Port:                     <unset>  80/TCP
10TargetPort:               80/TCP
11NodePort:                 <unset>  32592/TCP
12Endpoints:                10.244.1.6:80,10.244.2.7:80,10.244.2.8:80
13Session Affinity:         None
14External Traffic Policy:  Cluster
15Events:                   <none>

上面服务显示出各种可能的访问方式,包括:ClusterIP(IP), LoadBalancer Ingress, NodePort, 以及请求会被分发到的 Pod 容器(即 Endpoints)

以下图片来自于 《Kubernetes in Action》(Second Edition) 一书,它有助于我们理解 Kubernetes 集群请求分发的逻辑。

从外部的 Load balancer 不管连接到哪个节点的 NodePort(30838), 进一步经由节点上 iptables 规则的转发,请求最终有可能被任意一个 Pod 容器进行处理。

最后应该还有一种 kube-proxy 的方式可以访问到 Service 的,下次用到时再细究。 还是官方的关于 Services 的文档非常详细,根本没必要看我前面乱七八糟写的什么东西啊。

链接:

  1. Metallb - 贫苦 K8S 用户的负载均衡支持
永久链接 https://yanbin.blog/kubernetes-learning-2-run-service/, 来自 隔叶黄莺 Yanbin's Blog
[版权声明] 本文采用 署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0) 进行许可。