前言

我们有一些客户为了网络安全,在我们把服务部署完之后,就把服务器全部断开外网访问,在我们需要更新的时候,给我们一个 Windows 的跳板机,我们需要先找一台 Linux 的机器 dcoker pull image ,然后 docker save 保存 tar 包,scp 保存这个 tar 包到电脑,用 ToDesk 把这个 tar 包传到跳板机,然后在跳板机上 scp 这个 tar 包到 k8s 集群的各个节点上,然后 kubectl edit deployment.apps 修改应用负载配置文件里面 image 配置,把 tag 修改成新版的,然后 kubectl get pods 查看启动情况,如果没有启动再 kubectl logs -f 看日志

上面这套流程,实在是太过麻烦,公司有一套应用中心,可以直接滚动升级,之前的文章也稍稍提到过,但是这个应用中心设计的时候要求必须联网才能使用,需要和公有云上面跑的应用市场联动,如果没有外网就不能用它一键升级,也就得回到上面叙述的超复杂更新流程

体会了几次这样的流程,加上开发那边发版频繁,每次更新都走一遍这样的流程,实在是不能忍,反复的做这种没有意义的重复性工作实在是没有意思,于是一点点抽空做了个更新小助手,把自己和组里另一位从中解放出来

image-20231210111954677

↑别问为什么名字和封面是这个,就说优香酱可不可爱吧😋

(如果看下我的 Github ,我的小工具系列都是 yuuka+名称 的形式)

我们二次猿是这样滴

我们来康康实施对优香酱的评价

“是谁给整了这个封皮”2333

image-20231210122451345

设计目标和思路

目标很简单:做一个类似公司现有的简版应用中心,但是要离线使用,轻松更新,希望能在全平台运行

整理一下要做的事情:

  • 下载 Docker 镜像
  • 上传到 k8s 集群或者镜像仓
  • 修改对应应用的应用负载
  • 查看对应容器组的运行状态
  • 实现容器组重启
  • 看容器日志

来康康这些事情需要我们做些什么

  • 下载 Docker 镜像

调用本地的 Docker 客户端,但是往往别人提供的 Windows 跳板机没有,Windows 机器确实能装 Docker 但是费劲(实现是 WSL),我们组里各位的电脑也没有装 Docker ,所以最好自己实现一个 Docker 的客户端,只需要能做 docker pull & docker save & docker push 即可,因为进行的仅仅是 HTTP API 的交互,所以没有 WSL 的事儿

  • 上传到 k8s 集群或者镜像仓

和上面说的一样,这里涉及到的是 docker push 操作

  • 修改对应应用的应用负载

这里需要和 k8s 的 api-server 交互,修改应用负载的配置,更换里面镜像的版本(image tag)

  • 查看对应容器组的运行状态

同样是和 k8s 的 api-server 交互,查看容器组的状态

  • 实现容器组重启

依然是和 k8s 的 api-server 交互,删除一个 pod ,然后 api-server 会给我们重新启动一个 pod

  • 看容器日志

仍然是和 k8s 的 api-server 交互,查看指定 pod 的容器的 log

思路

经过上面的整理,其实我们要做的事情很简单:①实现 Docker 简版客户端 ②和 k8s 的 api-server 交互

Docker 简版客户端,我找到了这个

google/go-containerregistry: Go library and CLIs for working with container registries

狗歌开源的 go-containerregistry 是他们用来和镜像仓交互的一个库,同时也提供了二进制打包的 CLI 工具,我们用这个库就可以和标准的 docker 镜像仓交互,也就是我们想要的 Docker 简版客户端,如果不熟悉 Golang 的话,狗歌也提供了 python 版实现(python 版已经不维护哩)

和 k8s 的 api-server 交互,需要用到 k8s 的 api 客户端库,实现相当之多,有 C | C# | Go | Java | Javascript | Python | Rust 等等各种语言实现的,挑一个自己熟悉的就好,上面提到的公司的应用中心是用 Java 做的

客户端库 | Kubernetes

最后还有一个问题需要考虑,就是怎么交互这个问题,因为我不是很熟悉前端,加上这个程序是要在本地而不是 k8s 集群里面跑,所以我选择 GUI 客户端的形式来进行交互,Go 的 GUI 多数也还是前端套皮 Chrome 那套,留给我的选择就只有用 Go 写 GUI 的 fyne 了,Material Design ,还是挺好看的,之前也用它写过不少小工具,我的 Github 也开源了一些

fyne-io/fyne: Cross platform GUI toolkit in Go inspired by Material Design

实现

咱们来看客户端界面,然后一点点看怎么实现

镜像仓

这里主要做的保存客户端的凭据,收集基本信息,给后面拉镜像的时候使用

添加镜像仓

这个页面的功能很好做,仅仅是保存凭据到 SQLite 即可,为了使用方便,内置了公司镜像仓的信息可以一键导入

image-20231210130245134

镜像仓管理

和上面差不多的功能,SQL Select + Delete

image-20231210130539631

网关配置

前面提到,我们需要实现和 k8s 的 api-server 交互,我这边做了一个 k8s 应用,跑在集群里面,和客户端来交互,网关配置主要收集我们和谁来交互

配置网关

这里配置的信息会保存到 SQLite ,很简单的 CURD

image-20231210130853397

选择用户

这里用上一节拿到的信息,一是用一个全局变量保存要操作的对象,在标题显示,二是获取网关的版本,测试连通性

image-20231210131007049

滚动升级

这边是咱们要看的重头戏咯,所有的交互都在这里发生,客户端像网关发起获取信息请求,网关与 k8s api-server 交互

image-20231210131339106

获取应用负载

照着 api 撸就可以,起手式是需要建立一个到某命名空间的客户端,连接到 k8s 的 api-server 有讲究,如果你是在集群外面跑的,你需要弄个 RBAC 授权,使用授权信息(是个证书)去连接,而如果你是在集群里面跑的,可以直接自动获取配置信息,我推荐在集群里面跑

config, err := clientcmd.BuildConfigFromFlags("", "")
if err != nil {
logrus.Panicln(err)
} // 创建连接
HayaseYuukaK8sClient, err := kubernetes.NewForConfig(config)
if err != nil {
logrus.Panicln(err)
}
// 获取某命名空间内的负载
deploymentList, err := HayaseYuukaK8sClient.AppsV1().Deployments("namespace").List(context.TODO(), metav1.ListOptions{})

然后有了某个命名空间内的负载列表,我们就可以获取应用负载的信息了,我们需要取应用负载名, label 和 image ,label 后面看容器组信息需要,应用负载名和 image 后面更新需要

// 打印容器标签和容器镜像版本的方法
for _, app := range deploymentList.Items {
logrus.Infoln("-------------------------")
logrus.Warnln(app.Name)
logrus.Infoln("label:", app.Labels["app"])
logrus.Infoln("image:", app.Spec.Template.Spec.Containers[0].Image)
}

获取容器组信息

一样的起手式,只不过这次拿 pod 信息

// opts 里面用 label 去取
opts := metav1.ListOptions{
LabelSelector: "app=" + appRequest.Label,
}

podList, err := HayaseYuukaK8sClient.CoreV1().Pods("cloud").List(context.TODO(), opts)

podList.Items 里面就有我们想要的和 kubectl get pods 一样的数据,容器组名称,运行状态,运行时间,重启次数都有,我们需要拿这些数据,一是展示,二是后面看容器组日志和重启容器组需要

重启容器组&容器组日志

重启容器组

其实就是删除容器组,需要的参数是容器组名称,里面需要的参数多是 int64 ,需要转一下

graceTime := int64(0)                                      // 立即删除
var deletePolicy metav1.DeletionPropagation = "Background" // 后台删除
err = HayaseYuukaK8sClient.CoreV1().Pods("namespace").Delete(context.TODO(), appRequest.PodName, metav1.DeleteOptions{
PropagationPolicy: &deletePolicy,
GracePeriodSeconds: &graceTime,
})
image-20231210133611759

打印日志

和上面的那个接口差不多,也是需要容器组名称

containerName := pod.Spec.Containers[0].Name
req := HayaseYuukaPodsClient.GetLogs(appRequest.PodName, &corev1.PodLogOptions{
Container: containerName,
SinceSeconds: &timeSince,
Follow: false,
Previous: getPrevios,
LimitBytes: &logSize,
})
podLog, err := req.Stream(context.Background())

升级应用

这里涉及上面说的两个方面的事情了

一是和 api-server 交互修改 image tag

这里用前面获取到的应用负载名称去取特定的应用负载,然后把它的 image 换成新版本的即可

deployment, err := HayaseYuukaK8sClient.AppsV1().Deployments("namespace").Get(context.TODO(), appRequest.DeploymentName, metav1.GetOptions{})
deployment.Spec.Template.Spec.Containers[0].Image = appRequest.Image
deployment, err = HayaseYuukaK8sClient.AppsV1().Deployments("namespace").Update(ctx, deployment, metav1.UpdateOptions{})

二是 Docker 简版客户端,上面说的狗歌的那个库,但是没啥文档,看 godoc 即可

其实并不难,库里面都已经实现好了,来看核心实现

docker pull & docker push

var newImage v1.Image
var craneErr error

// 构建认证信息
auth := &authn.Basic{
Username: registryInfo.RegistryUserName,
Password: registryInfo.RegistryPassword,
}

// pull
if registryInfo.RegistryInsecure {
newImage, craneErr = crane.Pull(registryImage, crane.WithAuth(auth), crane.Insecure)
} else {
newImage, craneErr = crane.Pull(registryImage, crane.WithAuth(auth))
}

// push
err := crane.Push(image, newImageName, crane.WithAuth(auth), crane.Insecure)

docker save & docker load

// docker save
err := crane.Save(newImage, registryImage, destLocation)

// docker load
image, err := crane.Load(tempFileLoc)

是不是比想象中的要简单太多~

有的客户的防火墙阻断了 PATCH 方法(调试发现上传不了,这是因为狗歌这个库和 Harbor 交互里面要用 PATCH 方法),所以可能会需要用网关中转一下,也就是 GUI 上网关代理上传的由来

这也是我喜欢 POST 一把梭的原因,Restful 是好,架不住人家防火墙天天拦,客户嘛也不懂,你让他防火墙放通一下跟要了他命一样,过于逆天,POST 一把梭就不会遇到这个问题

image-20231210134402715

更新之后多出来的容器组就是新版本的 pod ,等正常启动完成探针监测 ok ,api-server 就会把上面的旧版本 pod 删除

image-20231210134457215

结语

一定要提升自动化能力,把自己从重复性劳动中解放出来,去做更有意义的事情,所以一定要会开发

fyne 写的程序在虚拟机和 Windows 远程桌面跑不了,报 OpenGL 问题的,可以用这个来解决,这个大佬实现了 CPU 模拟跑 OpenGL

pal1000/mesa-dist-win: Pre-built Mesa3D drivers for Windows

另外 go 的这个 fyne 库只适合做简单的页面,稍微复杂一点点就有比较严重的性能问题,后面准备 flutter / godot 二选一,拿来做复杂一点点的 GUI ,目前比较偏向于 godot ,等后面学学康