gitbook/深入剖析Kubernetes/docs/40008.md
2022-09-03 22:05:03 +08:00

17 KiB
Raw Permalink Blame History

12 | 牛刀小试:我的第一个容器化应用

你好,我是张磊。今天我和你分享的主题是:牛刀小试之我的第一个容器化应用。

在上一篇文章《从0到1搭建一个完整的Kubernetes集群》中我和你一起部署了一套完整的Kubernetes集群。这个集群虽然离生产环境的要求还有一定差距比如没有一键高可用部署但也可以当作是一个准生产级别的Kubernetes集群了。

而在这篇文章中我们就来扮演一个应用开发者的角色使用这个Kubernetes集群发布第一个容器化应用。

在开始实践之前我先给你讲解一下Kubernetes里面与开发者关系最密切的几个概念。

作为一个应用开发者,你首先要做的,是制作容器的镜像。这一部分内容,我已经在容器基础部分《白话容器基础(三):深入理解容器镜像》重点讲解过了。

而有了容器镜像之后你需要按照Kubernetes项目的规范和要求将你的镜像组织为它能够“认识”的方式然后提交上去。

那么什么才是Kubernetes项目能“认识”的方式呢

这就是使用Kubernetes的必备技能编写配置文件。

备注这些配置文件可以是YAML或者JSON格式的。为方便阅读与理解在后面的讲解中我会统一使用YAML文件来指代它们。

Kubernetes跟Docker等很多项目最大的不同就在于它不推荐你使用命令行的方式直接运行容器虽然Kubernetes项目也支持这种方式比如kubectl run而是希望你用YAML文件的方式把容器的定义、参数、配置统统记录在一个YAML文件中然后用这样一句指令把它运行起来

$ kubectl create -f 我的配置文件

这么做最直接的好处是你会有一个文件能记录下Kubernetes到底“run”了什么。比如下面这个例子

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.7.9
        ports:
        - containerPort: 80

像这样的一个YAML文件对应到Kubernetes中就是一个API ObjectAPI对象。当你为这个对象的各个字段填好值并提交给Kubernetes之后Kubernetes就会负责创建出这些对象所定义的容器或者其他类型的API资源。

可以看到这个YAML文件中的Kind字段指定了这个API对象的类型Type是一个Deployment。

所谓Deployment是一个定义多副本应用即多个副本Pod的对象我在前面的文章中也是第9篇文章《从容器到容器云谈谈Kubernetes的本质》曾经简单提到过它的用法。此外Deployment还负责在Pod定义发生变化时对每个副本进行滚动更新Rolling Update

在上面这个YAML文件中我给它定义的Pod副本个数(spec.replicas)是2。

而这些Pod具体的又长什么样子呢

为此我定义了一个Pod模版spec.template这个模版描述了我想要创建的Pod的细节。在上面的例子里这个Pod里只有一个容器这个容器的镜像spec.containers.image是nginx:1.7.9这个容器监听端口containerPort是80。

关于Pod的设计和用法我已经在第9篇文章《从容器到容器云谈谈Kubernetes的本质》中简单的介绍过。而在这里,你需要记住这样一句话:

Pod就是Kubernetes世界里的“应用”而一个应用可以由多个容器组成。

需要注意的是像这样使用一种API对象Deployment管理另一种API对象Pod的方法在Kubernetes中叫作“控制器”模式controller pattern。在我们的例子中Deployment扮演的正是Pod的控制器的角色。关于Pod和控制器模式的更多细节我会在后续编排部分做进一步讲解。

你可能还注意到这样的每一个API对象都有一个叫作Metadata的字段这个字段就是API对象的“标识”即元数据它也是我们从Kubernetes里找到这个对象的主要依据。这其中最主要使用到的字段是Labels。

顾名思义Labels就是一组key-value格式的标签。而像Deployment这样的控制器对象就可以通过这个Labels字段从Kubernetes中过滤出它所关心的被控制对象。

比如在上面这个YAML文件中Deployment会把所有正在运行的、携带“app: nginx”标签的Pod识别为被管理的对象并确保这些Pod的总数严格等于两个。

而这个过滤规则的定义是在Deployment的“spec.selector.matchLabels”字段。我们一般称之为Label Selector。

另外在Metadata中还有一个与Labels格式、层级完全相同的字段叫Annotations它专门用来携带key-value格式的内部信息。所谓内部信息指的是对这些信息感兴趣的是Kubernetes组件本身而不是用户。所以大多数Annotations都是在Kubernetes运行过程中被自动加在这个API对象上。

一个Kubernetes的API对象的定义大多可以分为Metadata和Spec两个部分。前者存放的是这个对象的元数据对所有API对象来说这一部分的字段和格式基本上是一样的而后者存放的则是属于这个对象独有的定义用来描述它所要表达的功能。

在了解了上述Kubernetes配置文件的基本知识之后我们现在就可以把这个YAML文件“运行”起来。正如前所述你可以使用kubectl create指令完成这个操作

$ kubectl create -f nginx-deployment.yaml

然后通过kubectl get命令检查这个YAML运行起来的状态是不是与我们预期的一致

$ kubectl get pods -l app=nginx
NAME                                READY     STATUS    RESTARTS   AGE
nginx-deployment-67594d6bf6-9gdvr   1/1       Running   0          10m
nginx-deployment-67594d6bf6-v6j7w   1/1       Running   0          10m

kubectl get指令的作用就是从Kubernetes里面获取GET指定的API对象。可以看到在这里我还加上了一个-l参数即获取所有匹配app: nginx标签的Pod。需要注意的是在命令行中所有key-value格式的参数都使用“=”而非“:”表示。

从这条指令返回的结果中我们可以看到现在有两个Pod处于Running状态也就意味着我们这个Deployment所管理的Pod都处于预期的状态。

此外, 你还可以使用kubectl describe命令查看一个API对象的细节比如

$ kubectl describe pod nginx-deployment-67594d6bf6-9gdvr
Name:               nginx-deployment-67594d6bf6-9gdvr
Namespace:          default
Priority:           0
PriorityClassName:  <none>
Node:               node-1/10.168.0.3
Start Time:         Thu, 16 Aug 2018 08:48:42 +0000
Labels:             app=nginx
                    pod-template-hash=2315082692
Annotations:        <none>
Status:             Running
IP:                 10.32.0.23
Controlled By:      ReplicaSet/nginx-deployment-67594d6bf6
...
Events:

  Type     Reason                  Age                From               Message

  ----     ------                  ----               ----               -------
  
  Normal   Scheduled               1m                 default-scheduler  Successfully assigned default/nginx-deployment-67594d6bf6-9gdvr to node-1
  Normal   Pulling                 25s                kubelet, node-1    pulling image "nginx:1.7.9"
  Normal   Pulled                  17s                kubelet, node-1    Successfully pulled image "nginx:1.7.9"
  Normal   Created                 17s                kubelet, node-1    Created container
  Normal   Started                 17s                kubelet, node-1    Started container

在kubectl describe命令返回的结果中你可以清楚地看到这个Pod的详细信息比如它的IP地址等等。其中有一个部分值得你特别关注它就是Events事件

在Kubernetes执行的过程中对API对象的所有重要操作都会被记录在这个对象的Events里并且显示在kubectl describe指令返回的结果中。

比如对于这个Pod我们可以看到它被创建之后被调度器调度Successfully assigned到了node-1拉取了指定的镜像pulling image然后启动了Pod里定义的容器Started container

所以这个部分正是我们将来进行Debug的重要依据。如果有异常发生你一定要第一时间查看这些Events,往往可以看到非常详细的错误信息。

接下来如果我们要对这个Nginx服务进行升级把它的镜像版本从1.7.9升级为1.8,要怎么做呢?

很简单我们只要修改这个YAML文件即可。

...    
    spec:
      containers:
      - name: nginx
        image: nginx:1.8 #这里被从1.7.9修改为1.8
        ports:
      - containerPort: 80

可是这个修改目前只发生在本地如何让这个更新在Kubernetes里也生效呢

我们可以使用kubectl replace指令来完成这个更新

 $ kubectl replace -f nginx-deployment.yaml

不过在本专栏里我推荐你使用kubectl apply命令来统一进行Kubernetes对象的创建和更新操作具体做法如下所示

$ kubectl apply -f nginx-deployment.yaml

# 修改nginx-deployment.yaml的内容

$ kubectl apply -f nginx-deployment.yaml

这样的操作方法是Kubernetes“声明式API”所推荐的使用方法。也就是说作为用户你不必关心当前的操作是创建还是更新你执行的命令始终是kubectl apply而Kubernetes则会根据YAML文件的内容变化自动进行具体的处理。

而这个流程的好处是它有助于帮助开发和运维人员围绕着可以版本化管理的YAML文件而不是“行踪不定”的命令行进行协作从而大大降低开发人员和运维人员之间的沟通成本。

举个例子一位开发人员开发好一个应用制作好了容器镜像。那么他就可以在应用的发布目录里附带上一个Deployment的YAML文件。

而运维人员拿到这个应用的发布目录后就可以直接用这个YAML文件执行kubectl apply操作把它运行起来。

这时候如果开发人员修改了应用生成了新的发布内容那么这个YAML文件也就需要被修改并且成为这次变更的一部分。

而接下来运维人员可以使用git diff命令查看到这个YAML文件本身的变化然后继续用kubectl apply命令更新这个应用。

所以说如果通过容器镜像我们能够保证应用本身在开发与部署环境里的一致性的话那么现在Kubernetes项目通过这些YAML文件就保证了应用的“部署参数”在开发与部署环境中的一致性。

而当应用本身发生变化时开发人员和运维人员可以依靠容器镜像来进行同步当应用部署参数发生变化时这些YAML文件就是他们相互沟通和信任的媒介。

以上就是Kubernetes发布应用的最基本操作了。

接下来我们再在这个Deployment中尝试声明一个Volume。

在Kubernetes中Volume是属于Pod对象的一部分。所以我们就需要修改这个YAML文件里的template.spec字段如下所示

apiVersion: apps/v1
kind: Deployment
metadata:
  name: nginx-deployment
spec:
  selector:
    matchLabels:
      app: nginx
  replicas: 2
  template:
    metadata:
      labels:
        app: nginx
    spec:
      containers:
      - name: nginx
        image: nginx:1.8
        ports:
        - containerPort: 80
        volumeMounts:
        - mountPath: "/usr/share/nginx/html"
          name: nginx-vol
      volumes:
      - name: nginx-vol
        emptyDir: {}

可以看到我们在Deployment的Pod模板部分添加了一个volumes字段定义了这个Pod声明的所有Volume。它的名字叫作nginx-vol类型是emptyDir。

那什么是emptyDir类型呢

它其实就等同于我们之前讲过的Docker的隐式Volume参数不显式声明宿主机目录的Volume。所以Kubernetes也会在宿主机上创建一个临时目录这个目录将来就会被绑定挂载到容器所声明的Volume目录上。

备注不难看到Kubernetes的emptyDir类型只是把Kubernetes创建的临时目录作为Volume的宿主机目录交给了Docker。这么做的原因是Kubernetes不想依赖Docker自己创建的那个_data目录。

而Pod中的容器使用的是volumeMounts字段来声明自己要挂载哪个Volume并通过mountPath字段来定义容器内的Volume目录比如/usr/share/nginx/html。

当然Kubernetes也提供了显式的Volume定义它叫作hostPath。比如下面的这个YAML文件

 ...   
    volumes:
      - name: nginx-vol
        hostPath: 
          path:  " /var/data"

这样容器Volume挂载的宿主机目录就变成了/var/data。

在上述修改完成后我们还是使用kubectl apply指令更新这个Deployment:

$ kubectl apply -f nginx-deployment.yaml

接下来你可以通过kubectl get指令查看两个Pod被逐一更新的过程

$ kubectl get pods
NAME                                READY     STATUS              RESTARTS   AGE
nginx-deployment-5c678cfb6d-v5dlh   0/1       ContainerCreating   0          4s
nginx-deployment-67594d6bf6-9gdvr   1/1       Running             0          10m
nginx-deployment-67594d6bf6-v6j7w   1/1       Running             0          10m
$ kubectl get pods
NAME                                READY     STATUS    RESTARTS   AGE
nginx-deployment-5c678cfb6d-lg9lw   1/1       Running   0          8s
nginx-deployment-5c678cfb6d-v5dlh   1/1       Running   0          19s

从返回结果中我们可以看到新旧两个Pod被交替创建、删除最后剩下的就是新版本的Pod。这个滚动更新的过程我也会在后续进行详细的讲解。

然后你可以使用kubectl describe查看一下最新的Pod就会发现Volume的信息已经出现在了Container描述部分

...
Containers:
  nginx:
    Container ID:   docker://07b4f89248791c2aa47787e3da3cc94b48576cd173018356a6ec8db2b6041343
    Image:          nginx:1.8
    ...
    Environment:    <none>
    Mounts:
      /usr/share/nginx/html from nginx-vol (rw)
...
Volumes:
  nginx-vol:
    Type:    EmptyDir (a temporary directory that shares a pod's lifetime)

备注作为一个完整的容器化平台项目Kubernetes为我们提供的Volume类型远远不止这些在容器存储章节里我将会为你详细介绍这部分内容。

最后你还可以使用kubectl exec指令进入到这个Pod当中即容器的Namespace中查看这个Volume目录

$ kubectl exec -it nginx-deployment-5c678cfb6d-lg9lw -- /bin/bash
# ls /usr/share/nginx/html

此外你想要从Kubernetes集群中删除这个Nginx Deployment的话直接执行

$ kubectl delete -f nginx-deployment.yaml

就可以了。

总结

在今天的分享中我通过一个小案例和你近距离体验了Kubernetes的使用方法。

可以看到Kubernetes推荐的使用方式是用一个YAML文件来描述你所要部署的API对象。然后统一使用kubectl apply命令完成对这个对象的创建和更新操作。

而Kubernetes里“最小”的API对象是Pod。Pod可以等价为一个应用所以Pod可以由多个紧密协作的容器组成。

在Kubernetes中我们经常会看到它通过一种API对象来管理另一种API对象比如Deployment和Pod之间的关系而由于Pod是“最小”的对象所以它往往都是被其他对象控制的。这种组合方式正是Kubernetes进行容器编排的重要模式。

而像这样的Kubernetes API对象往往由Metadata和Spec两部分组成其中Metadata里的Labels字段是Kubernetes过滤对象的主要手段。

在这些字段里面容器想要使用的数据卷也就是Volume正是Pod的Spec字段的一部分。而Pod里的每个容器则需要显式的声明自己要挂载哪个Volume。

上面这些基于YAML文件的容器管理方式跟Docker、Mesos的使用习惯都是不一样的而从docker run这样的命令行操作向kubectl apply YAML文件这样的声明式API的转变是每一个容器技术学习者必须要跨过的第一道门槛。

所以如果你想要快速熟悉Kubernetes请按照下面的流程进行练习

  • 首先在本地通过Docker测试代码制作镜像
  • 然后选择合适的Kubernetes API对象编写对应YAML文件比如PodDeployment
  • 最后在Kubernetes上部署这个YAML文件。

更重要的是在部署到Kubernetes之后接下来的所有操作要么通过kubectl来执行要么通过修改YAML文件来实现就尽量不要再碰Docker的命令行了

思考题

在实际使用Kubernetes的过程中相比于编写一个单独的Pod的YAML文件我一定会推荐你使用一个replicas=1的Deployment。请问这两者有什么区别呢

感谢你的收听,欢迎你给我留言,也欢迎分享给更多的朋友一起阅读。