写点什么

Jenkins Agent 的低成本高弹性实践

作者:玄月九
  • 2022 年 5 月 29 日
  • 本文字数:7355 字

    阅读完需:约 24 分钟

1. 前言

Jenkins 是一个功能强大的自动化和 CI/CD 工具,在 Jenkins 的使用中,最大的问题就是如何管理并优化 Jenkins 的 Agent。而在 Kubernetes 上使用 Jenkins Agent 就是比较好的一种方式。

本文主要从以下几点分享在 Kubernetes 上使用 Jenkins Agent 的一些实践。

  • 如何让 Jenkins Agent 运行在 Kubernetes 上。

  • 如何实现 Jenkins Agent Pod 的低成本高弹性。

  • 用 Jenkins 实现 CI,如何在一个 Jenkins Agent 镜像中封装代码编译和构建容器镜像所需要的所有软件和命令。

  • 不用 Docker,如何在 Kubernetes 上构建容器镜像。

注意: 本文的 Jekins Master 是运行在 Kubernetes 集群外的。

2. 基于 Kubernetes 的动态 Jenkins Agent

下图是在 Kubernetes 集群上运行 Jenkins Agent 的简单示意图。

从图上可以看到 Jenkins Master 运行在 Kubernetes 集群外的服务器上,Jenkins Agent 以 Pod 形式运行在 Kubernetes 的各个节点上,但 Jenkins Agent 不是一直处于运行状态,它会按照需求动态的创建并自动删除。

那么在 Kubernetes 上使用 Jenkins Agent 有什么好处呢?

  • 动态伸缩,合理使用资源,每当有新的 Jenkins Pipeline Job,就会自动生成一个 Agent Pod,当 Pipeline 结束,Pod 会被自动删除,资源自动释放。

  • Kubernetes 会根据每个节点的资源使用情况,动态分配 Jenkins Agent 到空闲的节点上创建,降低出现 Job 排队等待的情况。

  • 扩展性好,当 Kubernetes 集群的资源严重不足而导致 Job 排队等待时,可以很容易的添加一个 Kubernetes 节点到集群中,从而实现扩展。

要实现 Jenkins Agent on Kubernetes,需要在 Jenkins Master 上安装插件 “kubernetes plugin”。

Kubernetes 插件的详细说明参见:

下面讲下如何配置 “kubernetes plugin”。

注意: 以下 “kubernetes plugin” 的配置也只适用于 Jenkins Master 部署在 Kubernetes 集群之外,如果 Jenkins Master 也部署在 Kubernetes 集群里,配置方法会稍有不同。

2.1 配置 Jenkins 连接 Kubernetes 的凭据

用 kubeconfig 文件中的 certificate-authority-dataclient-certificate-dataclient-key-data 生成 Kubernetes Client P12 Certificate File。


# 生成 Kubernetes Server Certificate Keyecho "<certificate-authority-data>" | base64 -d > ca.crtecho "<client-certificate-data>" | base64 -d > client.crtecho "<client-key-data>" | base64 -d > client.key
# 根据上面步骤生成的 ca.crt、client.crt、client.key 来生成 PKCS12 格式的 cert.pfx# 以下命令运行时,需要设置 4 位以上的密码,要记住这个密码,往 Jenkins 导入 cert.pfx 时会用到这个密码。openssl pkcs12 -export -out cert.pfx -inkey client.key -in client.crt -certfile ca.crt
复制代码

在 Jenkins 上配置 Kubernetes Credential,打开 Jenkins 全局凭据页,添加凭据。

这里需要将上一步生成的 cert.pfx 文件上传到 Jenkins Master,并输入生成 cert.pfx 文件时设置的密码。

2.2 在 Jenkins 上配置 Kubernetes Cloud

打开 Jenkins 的 Cloud 配置页,新增 Kubernetes Cloud 。

2.2.1 Kubernetes Cloud Details

  • Kubernetes 地址,就是 Kubernetes 的 APIServer

  • Kubernetes 命名空间,就是让 Jenkins Agent Pod 运行在哪个 Namespace 里,可以提前规划好 Namespace。

  • 凭据选择上一步添加的凭据。

  • Jenkins 地址,就是 Jenkins Master 的地址。用以 Agent 和 Master 的通信。

另外还有全局 Pod Retention 配置,可选值如下:

  • Never :always delete the agent pod.

  • On Failure :keep the agent pod if it fails during the build.

  • Always :always keep the agent pod.

这里我们选 Never ,无论 Job 是否成功,Job 结束,Pod 就自动删除。

2.2.2 Pod Templates

一个 Kubernetes Cloud 下可以有多个 Jenkins Agent Pod Template,一个 Pod Template 主要包含以下部分:

  • 名称:Pod 模板的名称

  • 命名空间:Pod 所属的 Namespace ,为空则以 Kubernetes Cloud Details 下的命名空间为准。

  • 标签列表:Jenkins Master 用这里的标签来决定使用哪个 Pod Template 创建 Jenkins Agent。

  • 用法:默认是尽可能地使用这个节点,保持默认就行。

  • 容器列表( Container Template ):一个 Pod Template 里可以有多个 Container Template,一个 Container Template 主要包括:

  • 容器名称:一个 Pod 下的多个容器,必须有一个容器的名称是 jnlp 。jnlp 容器的作用就是和 Jenkins Master 通信。

  • 容器镜像:笔者在这里用的镜像是基于 Jenkins 官方 jenkins/inbound-agent:4.3-4 镜像制作的自定义镜像,加入了代码编译和镜像构建需要的各种命令,一个 Jenkins Agent 镜像适用于各种语言的 CI 。后文会对镜像的制作有详细介绍。

  • 工作目录:工作目录是用默认的 /home/jenkins/agent 就行。

  • :Pod 需要挂载的卷。maven 编译 java 代码和 go 编译 golang 代码时都需要相关模块和依赖,如果每次编译代码都从远程下载依赖,CI 时间会特别长。可以将这些依赖缓存下来,然后挂载到 Pod 里的目录,以提高代码编译的所读。

  • 注解( Pod Annotation ):Pod Metadata 中的 Annotation(下文会用到)。

  • Pod Retention:Pod 模板中的 Pod 保留策略,除了有和全局 Pod Retention 一样的可选值,还有默认值 Default ,会继承 Kubernetes Cloud Details 下的全局 Pod Retention。如果选择 Default 以外的策略,则以 Pod 模板中的 Retention 策略为准。

2.2.3 Jenkins Agent 挂载 Kubernetes PVC

先看下 Pod Template 中的卷的配置图:

上文有讲到,为了提高代码编译的速度,需要将依赖缓存。实现方式就是在 Kubernetes 上创建 PV 和 PVC ,然后将 PVC 挂载到 Jenkins Agent 的 Pod 。

笔者使用的 Kubernetes 是阿里云的 ACK 服务,因此在这里使用阿里云的极速 NAS 存储卷存放依赖缓存。PV 和 PVC 声明示例如下:

---apiVersion: v1kind: PersistentVolumemetadata:  name: cloud-jenkins-maven-m2  labels:    alicloud-pvname: cloud-jenkins-maven-m2spec:  accessModes:    - ReadWriteMany  capacity:    storage: 300Gi  csi:    driver: nasplugin.csi.alibabacloud.com    volumeAttributes:      path: /cloud-jenkins/maven-m2      server: example.cn-hangzhou.extreme.nas.aliyuncs.com      vers: '3'    volumeHandle: cloud-jenkins-maven-m2  persistentVolumeReclaimPolicy: Retain  volumeMode: Filesystem
---apiVersion: v1kind: PersistentVolumeClaimmetadata: name: cloud-jenkins-maven-m2 namespace: jenkinsspec: accessModes: - ReadWriteMany resources: requests: storage: 300Gi selector: matchLabels: alicloud-pvname: cloud-jenkins-maven-m2 volumeMode: Filesystem
复制代码

3. Jenkins Agent Pod 的低成本高弹性

上文实现了基于 Kubernetes 的动态 Jenkins Agent,工作流程大致为:当 Jenkins Master 接受到 Build 请求时,会根据 Pod Template 中配置的 Label 动态创建一个运行在 Pod 中的 Jenkins Agent 并注册到 Master 上,当运行完 Job 后,这个 Agent 会被注销并且这个 Pod 也会自动删除,恢复到最初状态。

但是仍然有以下痛点:

  • Jenkins Agent 也就是 Pod 的并发数量受限于 Kubernetes 集群的节点资源。当多个 Job 同时执行而 Kubernetes 集群的节点资源严重不足时,如果没有及时地为 Kubernetes 添加节点,仍然会导致 Job 排队等待时。

  • Kubernetes 集群需要为 Jenkins Agent 预留资源,在 Jenkins 空闲时段,预留的资源就处于浪费状态。

笔者使用的 Kubernetes 是阿里云的 ACK 服务,因此我们可以借助阿里云的 ECI 解决这些问题。

3.1 阿里云 ECI 简介

弹性容器实例( Elastic Container Instance ,简称 ECI ) 是阿里云结合容器和 Serverless 技术提供的容器运行服务。通过使用 ECI ,在阿里云上部署容器时,无需购买和管理云服务器 ECS,可以直接在阿里云上运行 Pod 和容器。从购买配置 ECS 再部署容器( ECS 模式 )到直接部署容器( ECI 模式 ),ECI 省去了底层服务器的运维和管理工作,并且仅需要为容器配置的资源付费( 按量按秒计费 ),可以节约成本。

阿里云 Kubernetes Virtual Node 是通过 ack-virtual-node 组件实现,ack-virtual-node 组件是基于社区开源项目 Virtual Kubelet,扩展了对 Aliyun Provider 的支持,并做了大量优化,实现 Kubernetes 与弹性容器实例 ECI 的无缝连接。

当 Kubernetes 集群 Node 资源不足时,无需规划 Node 的计算容量,可以直接使用 ECI 按需创建 Pod ,每个 Pod 对应一个 ECI 实例 ,ECI 与集群中真实 Node 上的 Pod 之间网络互通。Virtual Node 和 ECI 就像是 Kubernetes 集群的 “魔法口袋” ,让我们摆脱 Node 计算力不足的烦扰,也避免了 Node 的闲置浪费,满足无限计算力的想象,Pod 按需创建,轻松应对计算的波峰波谷。

阿里云支持通过给 Pod 添加 Annotations 来声明只使用普通 Node 的资源或者 ECI 资源,或者在普通 Node 的资源不足时自动使用 ECI 资源,以满足不同场景下对弹性资源的不同需求。

对应的 Annotations 配置项为 alibabacloud.com/burst-resource ,取值如下:

  • 默认不填 Annotations 时,只使用集群现有普通 Node 的资源。

  • eci :当集群普通 Node 资源不足时,使用 ECI 弹性资源。

  • eci_only :只使用 ECI 弹性资源,不使用集群现有普通 Node 的资源。

3.2 Jenkins Agent 使用 ECI

Jenkins Agent 使用 ECI 的简单示意图如下:

只需要在 Jenkins Kubernetes Plugin 的 Pod Template 中配置注解( Pod Annotation ),就可以让 Jenkins Agent 运行在 ECI 上。

为了更好地使用 ECI,可以给 Jenkins Agent Pod 添加以下 Annotation。

  • alibabacloud.com/burst-resource: eci_only ,让 Jenkins Agent Pod 只能调度到 ECI 上。

  • k8s.aliyun.com/eci-image-cache: true ,表示自动匹配镜像缓存。阿里云会自动把 Jenkins Agent 的镜像缓存下来,创建 ECI 实例时也会自动使用缓存的镜像去起容器,提高 ECI 实例的启动速度。

  • k8s.aliyun.com/eci-network-config: nat_internet_vpc ,表示使用 Job 优化型的 ECI 实例,启动速度更快。

  • k8s.aliyun.com/eci-use-specs:ecs.c6.2xlarge ,指定 ECI 实例的规格。

4. 自定义 Jenkins Agent 容器镜像

上文有提到,笔者 Jenkins Agent 的 Pod 里只有一个容器,并且启动容器的镜像也只有一个,也就是说在用这个镜像启动的容器里,要完成一个 Jenkins Pipline 的所有步骤,包括代码获取、不同语言的代码编译、业务应用镜像构建等。

以 node10、java8、golang1.17 这三个版本的开发语言为例,让自定义的 Jenkins Agent 支持这些语言的代码编译。

自定义的 Jenkins Agent 镜像是基于 Jenkins 官方 jenkins/inbound-agent:4.3-4 镜像制作的。镜像的 Dockerfile 内容如下:

FROM gcr.io/kaniko-project/executor:v1.8.1 AS kanikoFROM jenkins/inbound-agent:4.3-4USER rootCOPY --from=kaniko /kaniko /kanikoCOPY file/ /tmp/ENV GOPATH="/home/go" \    GOROOT="/usr/local/go" \    PATH="$PATH:/usr/local/go/bin:/home/go/bin:/kaniko" \    DOCKER_CONFIG="/kaniko/.docker/"RUN mv /tmp/docker-config.json /kaniko/.docker/config.json && \    rm -f /etc/localtime && \    ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime && \    echo 'Asia/Shanghai' > /etc/timezone && \    apt update && apt install -y net-tools telnet dnsutils libltdl7 zip unzip curl wget git && \    curl -fsSL https://deb.nodesource.com/setup_10.x | bash -  && \    apt install -y nodejs && \    npm install -g cnpm --registry=https://registry.npmmirror.com && \    apt install -y maven && \    mv /tmp/maven-settings.xml /usr/share/maven/conf/settings.xml && \    wget -P /tmp https://go.dev/dl/go1.17.10.linux-amd64.tar.gz && \    tar -C /usr/local -xzf /tmp/go1.17.10.linux-amd64.tar.gz && \    mkdir -p /home/go && \    curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.41.1 && \    apt clean && apt autoclean && rm -rf /var/lib/apt/lists/* /tmp/*
复制代码

docker build 的上下文如下图:

其中 maven-settings.xml 是 maven 的配置文件, docker-config.json 的内容是 kaniko 连接远端镜像仓库时需要的认证信息。

熟悉 Dockerfile 的同学应该可以看出,自定义 Jenkins Agent 镜像里主要包含以下软件:

  • git:用以从 gitlab 上拉取代码。

  • node10、maven、golang1.17:分别编译 node10、java8、golang1.17 等语言的代码。Dockerfile 中没有体现出安装 java8 环境,是因为 Jenkins 官方 jenkins/inbound-agent:4.3-4 镜像里本来就有 openjdk8 。

  • kaniko:kaniko 是谷歌开源的一款用来构建容器镜像的工具。在 Dockerfile 中的安装方式是直接从 kaniko 官方镜像复制可执行文件。下文会对 kaniko 做详细介绍。

5. 使用 Kaniko 在 Kubernetes 中构建容器镜像

5.1 Docker 的限制

在介绍 Kaniko 之前,先看下如何使用 Docker 在 Kubernetes 上构建容器镜像。有两种方式:

(1)挂载 Kubernetes 节点主机上的 docker 和 socket 文件到容器内部。

其实就是在容器内使用 Kubernetes 节点主机上的 docker 来构建镜像。对应到上文的 Jenkins Pod Templates,卷的配置如下:

笔者不使用这种方式,是因为这种方式有以下限制:

  • 需要 root 权限。由于 docker 依赖于 docker daemon 进程, docker daemon 进程是 Unix Socket 连接,且 /var/run/docker.sock 文件只有 root 用户有权限,因此只有 root 用户才可以访问 docker daemon 进程。

  • Kubernetes 1.24 之后不再支持 docker 作为容器运行时,当 Kubernetes 升级后,节点主机上将不再有 docker 。

  • 阿里云的 ECI 实例不支持挂载 docker 。

可能有同学会想,docker build 镜像无非就是需要 docker 命令能运行成功,只要在容器里面安装一个 docker 不就行了吗?这也就是下面讲的第二种方法。

(2)dind(docker-in-docker)

docker-in-docker 的最简单用法就是使用 Docker 官方提供的 tag 带有 dind 的 docker 镜像,例如 docker:20.10.16-dind 。这种方式不需要挂载 Kubernetes 节点主机的 socket 文件,但是需要以 --privileged 权限来用 dind 镜像创建一个容器。

笔者不使用这种方式,是因为这种方式有以下限制:

  • 如果使用此方式,就需要基于 dind 镜像去制作自定义的 Jenkins Agent 镜像,还要自己安装 jenkins agent,太麻烦。

  • 需要给容器特权(privileged=true),特权模式下,容器能够直接操作 Kubernetes 节点主机,不安全。

  • 阿里云的 ECI 实例不支持容器使用--privileged 权限。综上,在 Kubernetes 上使用 Docker 构建容器镜像,并不是最佳选择。可以使用 Kaniko 代替 Docker。

5.2 Kaniko

5.2.1 Kaniko 原理

Kaniko 是谷歌开源的构建容器镜像的工具,其目的是消除对 Docker 守护程序的长期依赖。Kaniko 不依赖 Docker Daemon 进程,也不需要特权模式,更适合在 Kubernetes 上根据 Dockerfile 来构建容器镜像。

如上图是 Kaniko 的工作原理图。Kaniko 执行器从 Dockerfile 构建镜像,并将其推送到镜像仓库。主要分为以下几步:

  • 根据 Dockerfile 中 FROM 描述,提取基础镜像的文件系统。

  • 执行 Dockerfile 中的每条命令,每条命令执行完后会在用户空间下创建文件系统的快照,并和存储于内存中的上一个状态做对比。

  • 如果有变化,就将新的修改生成一个镜像层添加在基础镜像上,并且将相关的修改信息写入镜像元数据中。

  • 等所有命令执行完,将最终镜像推送到指定的远端镜像仓库。

5.2.2 安装和执行 Kaniko

上文提到自定义 Jenkins Agent 镜像中的 kaniko 是直接从 kaniko 官方镜像复制的可执行文件,复制到了 /kaniko 目录。

可以看到 /kaniko 目录下有四个可执行文件,但我们只用到了 /kaniko/executor 文件。

Kaniko 以容器的方式运行,同时需要三个参数:

  • --dockerfile:指定 Dockerfile 的路径。

  • --context:构建时的上下文。

  • --destination:远端镜像仓库地址。

jenkins pipeline 中的镜像构建命令如下。

/kaniko/executor --dockerfile=./Dockerfile --context=./ --destination=harbor.demo.com/demo/java-demo:20220528
复制代码

使用该命令构建镜像,构建完成后,Kaniko 会自动将镜像推送到指定的远端镜像仓库。

假如有 test、uat、prod 三个业务环境,每个环境各一个镜像仓库,如果让不同环境 CI 生成的镜像被推送到不同的镜像仓库,那就要为 Kaniko 配置多个镜像仓库的身份验证信息。

5.2.3 镜像仓库认证

上文 Dockerfile 中的 docker-config.json 就是 kaniko 连接远端镜像仓库时使用的认证文件,复制到自定义 Jenkins Agent 镜像时重命名为 config.json,因为 kaniko 默认使用的认证文件名称是 config.json

kaniko 默认从环境变量 DOCKER_CONFIG 的值获取认证文件的目录。

因此 Dockerfile 中指定了环境变量 DOCKER_CONFIG="/kaniko/.docker/"

config.json 文件内容如下:

{  "auths": {    "prod-harbor.demo.com": {      "auth": "DhjiDDFYTREswlpohqmxncJ="    },    "uat-harbor.demo.com": {      "auth": "AhjiDLKpdsdwmkgbncFEDSV="    },    "test-harbor.demo.com": {      "auth": "QWcdvsfiypolSDlpohqmxnF="    }  }}
复制代码

其中 auth 的值是执行以下命令得到的,其中 username 和 password 是镜像仓库的账号和密码。

echo -n "<username>:<password>" | base64
复制代码

也可以在一台装有 docker 的机器上 docker login 到所有镜像仓库,然后将 /root/.docker/config.json 文件直接复制出来用。Docker 认证仓库的 config.json 和 Kaniko 认证仓库的 config.json 完全一样。

6. 总结

本文分享了基于 Kubernetes 和阿里云 ECI 实现 Jenkins Agent 低成本高弹性的一些实践。其中使用 Kubernetes 实现了 Jenkins Agent 的动态伸缩,使用阿里云 ECI 实现了 Jenkins Agent 的低成本高弹性,而 Kaniko 和自定义 Jenkins Agent 镜像又是 Jenkins Agent on Kubernetes 的基础。

发布于: 刚刚阅读数: 3
用户头像

玄月九

关注

人生就是不停地战斗 2018.08.08 加入

亦狂亦侠亦温文

评论

发布
暂无评论
Jenkins Agent 的低成本高弹性实践_Kubernetes_玄月九_InfoQ写作社区