情景模拟:
业务部署上线是每个运维都需要面对的问题,接下来分别从传统运维和k8s运维角度,梳理操作流程:
传统运维:
k8s运维:
分析:
两者都需要安装操作系统,初始化系统。 不同之处在于传统运维只需要单机配置环境部署服务即可。而k8s运维则需要部署搭建一个k8s集群,然后封装docker镜像,创建k8s的资源清单,才能完成项目的部署。相比较而言,k8s方式部署业务,前期从时间成本,资源使用考虑都是高于传统方式部署业务的。
业务上线初期,通常访问量很小,大多数情况都是单节点运行业务,那么如果出现硬件故障,例如磁盘坏盘,导致系统死机,用户无法访问业务服务。那么我们应该如何处理呢?
传统运维:
k8s运维:
分析:
如果是传统运维的话,服务器硬件故障可能一时半会不能修复,为了尽可能缩短业务中断时间,降低经济损失,只能快速再起一台服务器,再重复执行一遍业务上线的流程,最后修改DNS解析,或者修改公网ip,把流量引入新的机器中,恢复故障。
而如果是k8s运维的话,因为前期在部署上线时,已经搭建了k8s集群,当集群内有一个节点故障后,k8s发现节点宕机后,会将异常节点上的pod都变为unknow状态,并自动在其他机器上开启指定数量的副本pod。整个故障迁移过程中,运维人员不需要参与,用户也不会感知到服务异常,更不会对业务造成中断。
随着业务的不断发展,为了提高服务性能、避免单点故障,需要进行由单体服务向集群转变的操作。
传统运维:
k8s运维:
分析:
如果是传统运维,那就再执行几遍业务上线流程,如果机器过多,可以使用ansible等批量工具编写playbook执行,最后部署一个lvs 或者nginx 或者haproxy等反向代理软件,实现负载均衡,但过程中集群机器尽可能使用一样的操作系统和环境。
而如果是一个k8s运维,那么他需要做的操作,只是将副本数从1改为N即可,不需要关注如何实现负载均衡,因为k8s通过service已经实现了负载均衡。而且,面对一些瞬时暴增的流量,k8s可以通过HPA自动扩缩容,非常适合于一些流量波动大,机器资源吃紧,服务数量多的业务场景。
k8s运维:
分析:
以java项目为例,当打完jar包后,传统运维的话,因为整个业务是集群方式运行,所以首先就从负载均衡服务中,剔除那个将要更新的机器,防止流量进入。然后逐个更新,期间不停修改负载均衡服务后端地址列表。
而如果是k8s运维的话,只需要将最新jar包打包成一个新版本的业务docker镜像,然后修改资源清单文件的镜像版本即可,使用deployment控制器会自动完成滚动更新,实现零停机发布。
传统运维 | K8s运维 | |
---|---|---|
部署上线 | 配置环境,打包部署服务配置服务管理脚本 | 搭建集群、封装镜像、创建资源 |
健康检查 | 编写shell脚本或使用守护进程工具 | k8s提供就绪、存活探针,支持exec执行命令、HTTP状态码、TCP端口探测 |
故障转移 | 新机器部署服务,修改DNS解析指向新机器 | K8s内部自动实现故障转移 |
集群扩容 | 新机器部署服务,部署负载均衡服务,配置后端地址 | 修改k8spod资源副本数,service自动实现负载均衡 |
版本更新 | 修改负载均衡配置,逐个滚动停止服务,替换jar包,启动服务 | 更新docker镜像并上传,修改k8spod资源镜像信息,k8s自动实现滚动更新 |
以上五个运维场景相信也是大家在日常工作中会经常用到的,两种运维方式相比较而言,k8s只是在项目初期需要投入更多的时间和精力,但是当业务稳定运行后,k8s可以大大的简化运维的工作内容,是一种一劳永逸,更加优雅的运维方式。
将应用迁移到k8s,首先第一步就是要编写dockerfile,将原本在Linux服务器上运行的服务打包到容器中运行。我们回想传统方式部署业务的整体流程,我们一般是从最底层开始选择合适版本的操作系统,配置一系列的初始化操作,配置安全策略等。然后再部署运行环境,配置环境变量,最后上传项目代码到服务器中,配置启动脚本或者守护进程。在整个过程中,我们所有的操作分为了三个层面,最底层的操作系统,中间层的运行环境,上层的应用服务。
而编写dockerfile时,base镜像的选择也是从这几个层面考虑,如果环境层的镜像无法满足需求,就从系统层开始编写dockerfile。基础镜像可以从以下几个网站获取到
https://hub.docker.com/
https://quay.io/
http://gcr.io
通过下面几个常见案例说明:
案例1:
我们也已经明确知道了这个项目运行的系统和环境,也就是前面提到的三层中的系统层和环境层是已经确定的,使用docker镜像仓库中提供的基础镜像就可以满足我们的要求,我们只需要在应用层上将自己的代码加入镜像即可 。dockerfile基本内容参考文档:https://www.cuiliangblog.cn/detail/section/26458557
FROM openjdk:19-jdk-alpine3.16 # 使用alpine3.16操作系统,java19运行环境为基础镜像
ADD demo.jar /opt/app.jar # 复制项目jar包文件到容器/opt/目录下
EXPOSE 8888 # 声明容器暴露的端口
WORKDIR /opt # 指定容器工作目录
CMD ["java","-jar","app.jar"] # 指定启动命令为java –jar /app.jar
案例2:
在这个场景中,我们需要两个环境,一个是编译打包环境需要nodejs,一个是运行环境需要nginx。此时我们就需要用到多阶段构建,将打包阶段中的文件拷贝到后边的运行阶段中,实现编译环境和运行环境分离,dockerfile多阶段构建镜像参考文档:https://www.cuiliangblog.cn/detail/section/31400933。我们的dockerfile就这样写
FROM node:16.15.0 AS build # 使用node 16.15运行环境 用于打包项目,并将第一阶段命名为build
COPY . /vue # 复制项目代码到/vue目录下
WORKDIR /vue # 设置工作目录为/vue
RUN npm install && npm run build # 安装项目依赖并打包项目
FROM nginx:1.20.1 # 使用nginx 1.20运行环境为基础镜像
COPY --from=build /vue/dist /opt/vue/dist # 拷贝build阶段生成的打包文件dist到容器目录下
EXPOSE 80 # 声明容器暴露的端口
COPY vue.conf /etc/nginx/nginx.d/vue.conf # 拷贝nginx配置文件到容器nginx配置文件目录下
CMD ["nginx", "-g","daemon off;"] # 指定启动命令
案例3:
前两个案例中,我们都是使用默认的系统和环境,就可以实现我们的需求,我们只需要自定义应用层的东西即可。但如果碰到这样的需求,也就是说直接使用Python这个环境层的镜像无法满足要求了,因为他不支持yum命令安装sshd服务,直接拷贝ssh的二进制文件编译安装也非常麻烦,那我们就只能使用最基础的系统层镜像,然后自定义Python环境,完成镜像封装。
FROM centos:centos8 # 选取centos8为基础镜像
RUN dnf install openssh-server passwd python38 python38-devel -y # 安装相关软件包
RUN /bin/echo "123.com" | passwd --stdin root # 设置ssh密码
RUN /bin/sed -i 's/.session.required.pam_loginuid.so./session optional pam_loginuid.so/g' /etc/pam.d/sshd && /bin/sed -i 's/UsePAM yes/UsePAM no/g' /etc/ssh/sshd_config && /bin/sed -i "s/#UsePrivilegeSeparation.*/UsePrivilegeSeparation no/g" /etc/ssh/sshd_config # 修改sshd配置文件
RUN ssh-keygen -t rsa -f /etc/ssh/ssh_host_rsa_key && ssh-keygen -t rsa -f /etc/ssh/ssh_host_ecdsa_key && ssh-keygen -t rsa -f /etc/ssh/ssh_host_ed25519_key # 创建密钥
RUN echo -e "#! /bin/bash\n/usr/sbin/sshd -D" > /run.sh # 创建sshd服务启动脚本
ADD . /app # 拷贝代码到容器目录
RUN pip3.8 install -r /app/requirements.txt # 安装依赖
EXPOSE 22 # 声明容器暴露的端口
CMD ["/usr/sbin/sshd","-D"] # 指定启动命令
dockerfile构建镜像经验
当我们把应用封装成docker镜像后,接下来就是在kubernetes中启动镜像运行容器。因为Pod是Kubernetes管理的最小单元,Kubernetes不直接管理容器,而是管理Pod,Pod里面包含一个或多个容器。需要考虑是一个Pod中放置多个容器,还是一个Pod中放置一个容器。在一个pod中所有container共享,PID、Network、IPC
在有些业务场景中,例如一个web服务,对应的资源文件需要从远端实时监听更新,就需要使用边车 (sidercar)模式,一个pod中包含一个web容器一个file容器,通过共享存储方式实现。又比如一个用户的微服务包含:User API、User Control、User Data等三个模块,彼此之间紧耦合,对外只需要通过User API,这样类型的应用就可以放置在一个Pod中。
Pod资源清单建议
单一Pod如果出现故障,就会影响业务连续性,所以需要多副本,就像我们给一个Web应用做集群是一样的。Kubernetes提供了不同的Controller,需要根据应用的实际情况选择使用Deployment、DaemonSet、StatefulSet、Job、CronJob等,只需要在Pod的YAML模板上封装上对应的配置即可。
kubernetes提供的控制器如下:
由于每次pod重启都会随机生成新的ip,且使用控制器运行多副本pod时,该访问哪个具体的IP呢?
在传统的方式运行服务时,通常会提供一个VIP和service后端地址池。其中VIP负责对外暴露服务,监听请求。后端地址池复制监听后端服务的变化,随时更新地址池,并通过控制器实现轮循 哈希等负载均衡方式。
在kubernetes中,service便是这样的存在,service只是一个抽象的概念,他的工作主要由endpoint controller和kube-proxy搭配完成。
endpoints controller 是负责生成和维护所有 endpoints 对象的控制器,监听 service 和对应 pod 的变化,更新对应 service 的 endpoints 对象。当用户创建 service 后 endpoints controller 会监听 pod 的状态,当 pod 处于 running 且准备就绪时,endpoints controller 会将 pod ip记录到 endpoints 对象中,因此,service 的容器发现是通过 endpoints 来实现的。
而 kube-proxy 会监听 service 和 endpoints 的更新并调用其代理模块在主机上刷新路由转发规则。实际的路由转发都是由 kube-proxy 组件来实现的。
目前Service的负载均衡支持多种实现方式:User Space、iptable和ipvs。如果使用ipvs,当你创建Service的时候,kube-proxy会获取Service对应的Endpoint,调用LVS帮我们实现负载均衡的功能。
k8s同样也为我们提供了多种service使用:
ClusterIP(集群IP):集群内的服务间通信。 例如,应用程序的前端(front-end)和后端(back-end)组件之间的通信。
NodePort(节点端口):k8s集群内部服务暴露给外部时访问,可以通过k8s节点ip+端口方式访问服务。
LoadBalancer(负载均衡器):使用云厂商来托管您的 Kubernetes 集群时。由它接入外部客户端的请求并调度至集群节点相应的NodePort之上
ExternalName(外部名称):在 Kubernetes 内创建服务来表示映射外部服务名称,例如在 Kubernetes 中使用公有云数据库时,可以创建一个ExternalName资源,后续更换云数据库地址时,更新ExternalName配置即可
虽然用户可以通过service提供的nodeport方式或者loadbalancer方式访问k8s集群内部的服务,但是随着服务的增多,让普通用户通过ip+端口方式访问服务是不现实的。此时就需要通过不同的域名对应访问不同的服务,而这种全局的、为了代理不同后端 Service 而设置的负载均衡服务,就是 Kubernetes 里的 Ingress 服务。
Ingress同样也只是一个概念,它的具体的实现依赖控制器,Ingress控制器并不直接运行为kube-controller-manager的一部分,它是Kubernetes集群的一个重要附件,类似于CoreDNS,需要在集群上单独部署。Ingress控制器可以由任何具有反向代理(HTTP/HTTPS)功能的服务程序实现,如Nginx、Envoy、HAProxy、Vulcand和Traefik等。
在DevOps的部署流水线中,其中有一个核心的理念就是代码和配置的分离,这样更容易实现流水线的编排。我们只需要使用一套代码,配合不同的配置文件,就可以实现灵活发布到测试环境、预发布环境、生产环境等。
kubernetes同样也为我们提供了配置与文件管理方案:
容器中的存储都是临时的,因此Pod重启的时候,内部的数据会发生丢失。实际应用中,我们有些应用是无状态,有些应用则需要保持状态数据,确保Pod重启之后能够读取到之前的状态数据,有些应用则作为集群提供服务。这三种服务归纳为无状态服务、有状态服务以及有状态的集群服务,其中后面两个存在数据保存与共享的需求,因此就要采用容器外的存储方案。
kubernetes拥有众多类型的用于适配专用存储系统的网络存储卷。这类存储卷包括传统的NAS或SAN设备(如NFS、iSCSI、fc)、分布式存储(如GlusterFS、RBD)、云端存储(如gcePersistentDisk、azureDisk、cinder和awsElasticBlockStore)以及建构在各类存储系统之上的抽象管理层(如flocker、portworxVolume和vsphereVolume)等。
环境准备
ip | 主机名 | 角色 |
---|---|---|
192.168.10.100 | tiaoban | harbor私有仓库+nfs服务器+es服务+kibana |
192.168.10.10 | k8s-master | k8s master节点 |
192.168.10.11 | k8s-work1 | k8s work节点 |
192.168.10.12 | k8s-work2 | k8s work节点 |
nfs部署文档:https://www.cuiliangblog.cn/detail/section/116191364
traefik部署文档:https://www.cuiliangblog.cn/detail/section/94793162
harbor部署文档:https://www.cuiliangblog.cn/detail/section/15189547
es+kibana部署文档:https://www.cuiliangblog.cn/detail/section/117075458
构建docker镜像
我们将打包好的jar文件和dockerfile放在同一级目录下,执行docker build指令,即可完成镜像构建
[root@tiaoban springboot]# ls
demo demo-v1.jar demo-v2.jar Dockerfile
[root@tiaoban springboot]# cat Dockerfile
FROM openjdk:19-jdk-alpine3.16
ADD demo-v1.jar /opt/app.jar
EXPOSE 8888
WORKDIR /opt
CMD ["java","-jar","app.jar"]
# 打包镜像
[root@tiaoban springboot]# docker build -t springboot:v1 .
# 启动容器,访问测试
[root@k8s-master springboot]# docker run -d -p 8888:8888 springboot:v1
74a8bc2446d7f30195099c8f6f905481b2fdc6b585436e4b810849f1df50185d
[root@k8s-master springboot]# curl 127.0.0.1:8888
Hello SpringBoot Version:v1[root@k8s-master springboot]#
[root@k8s-master springboot]# curl 127.0.0.1:8888/healthy
ok[root@k8s-master springboot]#
推送至harbor私有镜像仓库
构建好镜像测试无误后,接下来就可以推送到harbor私有镜像仓库中
[root@k8s-master springboot]# docker tag springboot:v1 192.168.10.100/demo/springboot:v1
[root@k8s-master springboot]# docker push 192.168.10.100/demo/springboot:v1
创建pod资源调试
推送完成后,接下来我们开始编写pod的资源清单文件,镜像地址填写harbor仓库的镜像地址
[root@k8s-master springboot]# cat pod.yaml
apiVersion: v1
kind: Pod
metadata:
name: springboot
labels:
name: springboot
spec:
containers:
- name: springboot
image: 192.168.10.100/demo/springboot:v1
resources:
limits: # 资源使用最大值
memory: "1Gi"
cpu: "1"
requests:
memory: "128Mi"
cpu: "100m"
ports:
- containerPort: 8888
name: web
livenessProbe:
httpGet: # http存活检测,返回状态码为200表示正常
port: web
path: /healthy
initialDelaySeconds: 20 # 容器启动后20秒开始检测
readinessProbe:
tcpSocket: # tcp就绪检测,只有当8888端口开始监听,才会有流量引入
port: web
[root@k8s-master springboot]# kubectl apply -f pod.yaml
pod/springboot created
[root@k8s-master springboot]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nfs-subdir-external-provisioner-7c8776699f-mkpxg 1/1 Running 4 (56m ago) 97m 10.244.2.5 k8s-work2 <none> <none>
springboot 1/1 Running 0 46s 10.244.2.7 k8s-work2 <none> <none>
# pod启动后,可以通过pod的IP+端口方式访问服务,验证测试是否正常启动
[root@k8s-master springboot]# curl 10.244.2.7:8888
Hello SpringBoot Version:v1
创建deployment资源
pod资源创建完成测试无误,接下来就是使用deployment控制器管理pod,让它以多副本方式运行
[root@k8s-master springboot]# kubectl delete -f pod.yaml
pod "springboot" deleted
[root@k8s-master springboot]# cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: springboot
spec:
replicas: 2 # 副本数
selector:
matchLabels:
app: springboot
template:
metadata:
labels:
app: springboot
spec:
containers:
- name: springboot
image: 192.168.10.100/demo/springboot:v1
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "128Mi"
cpu: "100m"
ports:
- containerPort: 8888
name: web
livenessProbe:
httpGet:
port: web
path: /healthy
timeoutSeconds: 2 # 表示容器必须在2s内做出相应反馈给probe,否则视为探测失败
periodSeconds: 30 # 探测周期,每30s探测一次
readinessProbe:
tcpSocket:
port: web
initialDelaySeconds: 10 # 容器启动后10s开始探测
[root@k8s-master springboot]# kubectl apply -f deployment.yaml
deployment.apps/springboot created
[root@k8s-master springboot]# kubectl get pod -o wide
NAME READY STATUS RESTARTS AGE IP NODE NOMINATED NODE READINESS GATES
nfs-subdir-external-provisioner-7c8776699f-mkpxg 1/1 Running 4 (61m ago) 102m 10.244.2.5 k8s-work2 <none> <none>
springboot-55df548fdb-lr4v4 1/1 Running 0 32s 10.244.2.8 k8s-work2 <none> <none>
springboot-55df548fdb-pdpfh 1/1 Running 0 32s 10.244.1.6 k8s-work1 <none> <none>
# deployment控制器资源部署成功后,我们继续使用pod IP+端口方式访问测试
[root@k8s-master springboot]# curl 10.244.2.8:8888
Hello SpringBoot Version:v1
[root@k8s-master springboot]# curl 10.244.1.6:8888/healthy
ok
创建service资源
控制器资源创建无误后,接下来我们部署service资源,然后就可以通过clusterIP+端口方式访问服务
[root@k8s-master springboot]# cat service.yaml
apiVersion: v1
kind: Service
metadata:
name: springboot
spec:
type: ClusterIP
selector:
app: springboot
ports:
- name: http
port: 8888
protocol: TCP
targetPort: 8888
[root@k8s-master springboot]# kubectl apply -f service.yaml
service/springboot created
[root@k8s-master springboot]# kubectl get svc
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
kubernetes ClusterIP 10.96.0.1 <none> 443/TCP 127m
springboot ClusterIP 10.100.116.123 <none> 8888/TCP 6s
[root@k8s-master springboot]# curl 10.100.116.123:8888
Hello SpringBoot Version:v1
创建ingress资源
至此,资源创建基本完成,接下来我们配置一个ingress域名资源,客户端配置hosts地址后,就可以通过域名方式访问服务
[root@k8s-master springboot]# cat ingress.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: springboot
spec:
entryPoints:
- web
routes:
- match: Host(`springboot.test.com`) # 域名
kind: Rule
services:
- name: springboot # 与svc的name一致
port: 8888 # 与svc的port一致[root@k8s-master springboot]#
[root@k8s-master springboot]# kubectl apply -f ingress.yaml
ingressroute.traefik.containo.us/springboot created
[root@k8s-master springboot]# kubectl get ingressroute
NAME AGE
springboot 18s
构建docker镜像
在这个案例中,我们只有git仓库的地址。因此我们首先要克隆代码,然后再执行打包并测试。
[root@k8s-master vue]# git clone https://gitee.com/cuiliang0302/vue3_vite_element-plus.git
[root@k8s-master vue]# docker build -f Dockerfile-vue -t vue:v1 .
[root@k8s-master vue]# docker run -d -p 90:80 vue:v1
09b2ad25f1aa1f7d71f9977d965f82b17edfced266dc3f9287c143fef02dfd9c
[root@k8s-master vue]# docker build -f Dockerfile-img -t img:v1 .
[root@k8s-master vue]# docker run -d img:v1
c212f514f52dfa84f2b499c73a684ee2037c2ebf9654b4d37ee2d0bebde3b9a6
root@k8s-master vue]# docker exec -it c212f514f5 sh
/media # cd /media/
/media # ls -lh
total 332K
-rw-r--r-- 1 root root 329.7K Mar 7 12:17 images.jpg
推送至harbor私有镜像仓库
[root@k8s-master vue]# docker tag vue:v1 192.168.10.100/demo/vue:v1
[root@k8s-master vue]# docker push 192.168.10.100/demo/img:v1
[root@k8s-master vue]# docker tag img:v1 192.168.10.100/demo/img:v1
[root@k8s-master vue]# docker push 192.168.10.100/demo/img:v1
创建configmap资源
我们分别创建两套nginx的配置文件,分别对应开发环境和生产环境。
[root@k8s-master vue]# cat configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: vue-dev
data:
default.conf: |-
server {
listen 80;
server_name vue-dev.test.com; # 开发环境域名
location /img { # 访问img路径下资源时,重定向到百度页面
return 301 https://www.baidu.com;
}
location / {
root /opt/vue/dist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Access-Control-Allow-Origin *;
}
}
---
apiVersion: v1
kind: ConfigMap
metadata:
name: vue
data:
default.conf: |-
server {
listen 80;
server_name vue.test.com; # 生产环境域名
location /img { # 访问img路径下资源时,从远端获取,保存到/media目录下
alias /media/;
}
location / {
root /opt/vue/dist;
index index.html index.htm;
try_files $uri $uri/ /index.html;
add_header Access-Control-Allow-Origin *;
}
}
[root@k8s-master vue]# kubectl apply -f configmap.yaml
configmap/vue-dev created
configmap/vue created
创建deployment资源
生产和开发环境对应两个不同的控制器,他俩的区别在于使用的configmap不同,在生产模式下,使用sidecar定期从远端获取资源,存放在/media共享路径下。
[root@k8s-master vue]# cat deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vue-dev
spec:
replicas: 1
selector:
matchLabels:
app: vue-dev
template:
metadata:
labels:
app: vue-dev
spec:
containers:
- name: vue
image: 192.168.10.100/demo/vue:v1
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "128Mi"
cpu: "100m"
ports:
- containerPort: 80
name: web
livenessProbe:
httpGet:
port: web
path: /
readinessProbe:
tcpSocket:
port: web
volumeMounts:
- mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
name: nginx-config
volumes: # 使用开发环境的nginx配置文件
- name: nginx-config
configMap:
name: vue-dev
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: vue
spec:
replicas: 1
selector:
matchLabels:
app: vue
template:
metadata:
labels:
app: vue
spec:
containers:
- name: vue
image: 192.168.10.100/demo/vue:v1
resources:
limits:
memory: "1Gi"
cpu: "1"
requests:
memory: "128Mi"
cpu: "100m"
ports:
- containerPort: 80
name: web
livenessProbe:
httpGet:
port: web
path: /
readinessProbe:
tcpSocket:
port: web
volumeMounts:
- mountPath: /etc/nginx/conf.d/default.conf
subPath: default.conf
name: nginx-config
- mountPath: /media
name: media
- name: img
image: 192.168.10.100/demo/img:v1
resources:
limits:
memory: "128Mi"
cpu: "100m"
volumeMounts:
- mountPath: /media
name: media
volumes:
- name: nginx-config # 使用生产模式nginx配置文件
configMap:
name: vue
- name: media # 挂载空目录,用于存放远端资源
emptyDir: {}
[root@k8s-master vue]# kubectl apply -f deployment.yaml
deployment.apps/vue-dev created
deployment.apps/vue created
创建service资源
service资源同样也是两套,唯一不同的区别在于匹配不同的资源标签
[root@k8s-master vue]# cat service.yaml
apiVersion: v1
kind: Service
metadata:
name: vue-dev
spec:
type: ClusterIP
selector:
app: vue-dev
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
---
apiVersion: v1
kind: Service
metadata:
name: vue
spec:
type: ClusterIP
selector:
app: vue
ports:
- name: http
port: 80
protocol: TCP
targetPort: 80
[root@k8s-master vue]# kubectl apply -f service.yaml
service/vue-dev created
service/vue created
创建ingress资源
ingress资源同样也是两套,对应不同的环境域名
[root@k8s-master vue]# cat ingress.yaml
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: vue-dev
spec:
entryPoints:
- web
routes:
- match: Host(`vue-dev.test.com`) # 域名
kind: Rule
services:
- name: vue-dev
port: 80
---
apiVersion: traefik.containo.us/v1alpha1
kind: IngressRoute
metadata:
name: vue
spec:
entryPoints:
- web
routes:
- match: Host(`vue.test.com`) # 域名
kind: Rule
services:
- name: vue
port: 80
[root@k8s-master vue]# kubectl apply -f ingress.yaml
ingressroute.traefik.containo.us/vue-dev created
ingressroute.traefik.containo.us/vue created
访问验证
开发环境,当我们访问http://vue-dev.test.com/img/images.jpg时,会301重定向到百度页面。
生产环境,当我们访问http://vue.test.com/img/images.jpg时,会从远端服务器拉取资源文件并返回给客户端。
打包镜像并上传
[root@k8s-master python]# cat Dockerfile
FROM rockylinux:8
RUN dnf -y install wget gcc gcc-c++ net-tools telnet iproute procps tcpdump python38 python38-devel
ADD project /project
RUN pip3.8 install -r /project/requirements.txt -i https://pypi.doubanio.com/simple
WORKDIR /project
CMD ["python3.8","main.py"]
[root@k8s-master python]# docker build -t python:v1 .
[root@k8s-master python]# docker run python:v1
2023-03-07 14:49:33.213 | INFO | __main__:<module>:26 - 爬虫程序开始执行
2023-03-07 14:49:33.214 | INFO | __main__:get_data:10 - 获取的数据ID:43
2023-03-07 14:49:40.958 | INFO | __main__:get_data:14 - 获取的name值:oddish
2023-03-07 14:49:40.965 | INFO | __main__:save_data:22 - 文件写入完成,文件名:oddish1678200580.txt
2023-03-07 14:49:40.966 | INFO | __main__:<module>:29 - 爬虫程序执行完成
[root@k8s-master python]# docker tag python:v1 192.168.10.100/demo/python:v1
[root@k8s-master python]# docker push 192.168.10.100/demo/python:v1
创建pvc资源,用于持久化存储爬虫数据
[root@k8s-master python]# cat pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: python-pvc
spec:
storageClassName: nfs-client
accessModes:
- ReadWriteMany
resources:
requests:
storage: 1Gi
[root@k8s-master python]# kubectl apply -f pvc.yaml
persistentvolumeclaim/python-pvc created
创建cronjob资源,定时执行爬虫程序
[root@k8s-master python]# cat cronjob.yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: python
spec:
schedule: "* * * * *"
jobTemplate:
metadata:
name: python
spec:
template:
spec:
containers:
- name: python
image: 192.168.10.100/demo/python:v1
volumeMounts:
- name: python-data
mountPath: "/project/data"
- name: python-log
mountPath: "/project/log"
env:
- name: TZ
value: Asia/Shanghai
restartPolicy: Never
volumes:
- name: python-data
persistentVolumeClaim:
claimName: python-pvc
- name: python-log
hostPath:
path: /data/python-log
type: DirectoryOrCreate
[root@k8s-master python]# kubectl apply -f cronjob.yaml
cronjob.batch/python created
导出es证书
因为es从8开始,安装后默认开启了base基础用户验证和TLS证书。filebeat想要连接ES需要指定SSL证书,否则会报X509证书异常错误。
[root@tiaoban ~]# docker cp elasticsearch:/usr/share/elasticsearch/config/certs .
Successfully copied 21.5kB to /root/.
[root@tiaoban ~]# ls
anaconda-ks.cfg certs elasticsearch.yml
[root@tiaoban ~]# cd certs/
[root@tiaoban certs]# ls
http_ca.crt http.p12 transport.p12
[root@tiaoban certs]# scp http_ca.crt k8s-master:/root
创建secret资源
从docker容器中导出es证书后,创建secret资源,后续filebeat直接挂载证书资源即可。
[root@k8s-master ~]# kubectl create secret generic es --from-file=./http_ca.crt
secret/es created
[root@k8s-master ~]# kubectl describe secrets es
Name: es
Namespace: default
Labels: <none>
Annotations: <none>
Type: Opaque
Data
====
http_ca.crt: 1915 bytes
创建filebeat的configmap资源
[root@k8s-master python]# cat configmap.yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: filebeat-config
data:
filebeat.yml: |-
filebeat.inputs:
- type: log
enabled: true
paths:
- /project/log/*.log
setup.ilm.enabled: false #新版本的Filebeat则默认的配置开启了ILM 导致索引的命名规则被ILM策略控制
setup.template.name: "python"
setup.template.pattern: "python-*"
setup.template.overwrite: false
setup.template.settings:
index.number_of_shards: 1 #索引分片数
index.number_of_replicas: 0 #索引副本数
output.elasticsearch: #指定ES的配置
hosts: ["https://192.168.10.100:9200"]
username: "elastic"
password: "HufU-ybqd5aeEvh8xf6y"
index: "python-%{+yyyy.MM.dd}"
ssl.certificate_authorities: ["/secret/http_ca.crt"]
[root@k8s-master python]# kubectl apply -f configmap.yaml
configmap/filebeat-config created
创建daemonset资源,每个节点启动一个filebeat
[root@k8s-master python]# cat daemonset.yaml
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: filebeat
labels:
app: filebeat
spec:
selector:
matchLabels:
app: filebeat
template:
metadata:
labels:
app: filebeat
spec:
containers:
- name: filebeat
image: elastic/filebeat:8.6.2
resources:
limits:
memory: "128Mi"
cpu: "100m"
requests:
memory: "10Mi"
cpu: "10m"
args: ["-c","/etc/filebeat/filebeat.yml","-e"]
volumeMounts:
- name: filebeat-config
mountPath: /etc/filebeat/filebeat.yml
subPath: filebeat.yml
- name: python-log
mountPath: /project/log
- name: es-cert
mountPath: /secret
readOnly: true
volumes:
- name: python-log
hostPath:
path: /data/python-log
type: DirectoryOrCreate
- name: filebeat-config
configMap:
name: filebeat-config
- name: es-cert
secret:
secretName: es
[root@k8s-master python]# kubectl apply -f daemonset.yaml
daemonset.apps/filebeat created
访问kibana查看日志是否成功上传
[root@tiaoban default-python-pvc-pvc-b52722f6-1c82-41cb-9e79-fd7e991c78dd]# pwd
/data/nfs/default-python-pvc-pvc-b52722f6-1c82-41cb-9e79-fd7e991c78dd
[root@tiaoban default-python-pvc-pvc-b52722f6-1c82-41cb-9e79-fd7e991c78dd]# ls -lt
总用量 201
-rw-r--r-- 1 root root 183112 3月 8 23:03 pidgey1678287784.txt
-rw-r--r-- 1 root root 210376 3月 8 23:02 poliwag1678287723.txt
-rw-r--r-- 1 root root 193014 3月 8 23:01 cloyster1678287664.txt
-rw-r--r-- 1 root root 27881 3月 8 23:00 weedle1678287603.txt
-rw-r--r-- 1 root root 243530 3月 8 22:59 pikachu1678287544.txt
-rw-r--r-- 1 root root 278708 3月 8 22:58 drowzee1678287483.txt
-rw-r--r-- 1 root root 182849 3月 8 22:57 ninetales1678287423.txt
-rw-r--r-- 1 root root 234397 3月 8 22:56 vulpix1678287363.txt
通过上面几个实践案例,大家肯定会思考这样的问题。部署一个服务,需要创建ConfigMap、PV、PVC、Deployment 、Service、Ingress这一堆资源。
kubernetes为了解决上述问题,推出了helm。
Helm 是 Kubernetes 的包管理器。包管理器类似于我们在 Ubuntu 中使用的apt、Centos中使用的yum 或者Python中的 pip 一样,能快速查找、下载和安装软件包。能够将一组K8S资源打包统一管理, 是查找、共享和使用Kubernetes构建的软件的最佳方式。
类似于docker hub一样,helm也为我们提供了hub仓库,地址为https://artifacthub.io/。我们只需要浏览helm仓库,找到合适的helm安装包,添加仓库源,然后安装即可。以使用helm部署elk为例,参考文档https://www.cuiliangblog.cn/detail/section/15189467
除了浏览官方的helm仓库安装应用外,自定义服务部署也可以使用helm方式实现,开发者只需要按照 Helm Chart 的格式,将应用所需的资源文件包装起来,通过模版化 (Templating) 的方式将一些可变字段(比如我们之前提到的暴露哪个端口、使用多少副本)暴露给用户,最后将封装好的应用包,就完成了Helm Chart的制作,最后集中存放在统一的仓库中供其他用户浏览下载。参考文档https://www.cuiliangblog.cn/detail/section/15287438
Operator 模型基于 Kubernetes 中的两个概念结合而成:自定义资源和自定义控制器。要想理解operator,首先就要知道自定义资源和自定义控制器
CRD(Custom Resource Definitions),也就是自定义K8S资源类型。
当内置的POD、Deployment、Configmap等资源类型不满足需求时,我们就需要扩展k8s,常用方式有三种:
在 Kubernetes中,资源是 Kubernetes API中的一个端点,用于存储一堆特定类型的API对象。它允许我们通过向集群添加更多种类的对象来扩展Kubernetes。添加新种类的对象之后,我们可以像其他任何内置对象一样,使用 kubectl 来访问我们自定义的 API 对象,CRD无须修改Kubernetes源代码就能扩展它支持使用API资源类型。
Kubernetes 的所有控制器,都有一个控制循环,负责监控集群中特定资源的更改,并确保特定资源在集群里的当前状态与控制器自身定义的期望状态保持一致。
Controller是需要CRD配套开发的程序,它通过Apiserver监听相应类型的资源对象事件,例如:创建、删除、更新等等,然后做出相应的动作,例如一个 Deployment 控制器管控值集群里的一组 Pod ,当你 Kill 掉一个 Pod 。控制器发现定义中期望的Pod数量与当前的数量不匹配,它就会马上创建一个 Pod 让当前状态与期望状态匹配。
operator 是一种 kubernetes的扩展形式,利用自定义资源对象CRD来管理应用和组件,允许用户以 Kubernetes 的声明式 API 风格来管理应用及服务。operator 定义了一组在 Kubernetes 集群中打包和部署复杂业务应用的方法,operator主要是为解决特定应用或服务关于如何运行、部署及出现问题时如何处理提供的一种特定的自定义方式。
例如MySQLOperator可以实现:
例如ESOperator(ECK)可以实现:
例如KafkaOperator(Strimzi)可以实现:
Helm | Operator | |
---|---|---|
资源类型 | 标准对象,自定义对象 | 自定义对象 |
运维能力 | 手工,较弱 | 自动故障恢复及异常处理 |
设计理念 | 资源模板化,配置分离 | 复杂应用的自动化管理 |
实现方式 | 传统镜像 | k8s控制器 |
实现难度 | 较低 | 偏高 |
灵活性 | 高度灵活修改YAML文件 | 依赖控制程序,不太灵活 |
方式 | 场景 | 举例 |
---|---|---|
手写YAML | 部署业务项目、需要频繁需改配置或架构、需要高度灵活性 | Prometheus、Traefik |
Helm | 部署无状态集群服务、快速部署实验测试环境 | Redis、Consul、Grafana |
Operator | 部署有状态集群服务 | MySQL、Kafka、Elasticsearch |
gitee:https://gitee.com/cuiliang0302/blog_demo
github:https://github.com/cuiliang0302/blog_demo
崔亮的博客-专注devops自动化运维,传播优秀it运维技术文章。更多原创运维开发相关文章,欢迎访问https://www.cuiliangblog.cn