亚马逊AWS官方博客

在 EKS 上使用 Jenkins 构建多架构镜像

背景

DevOps 是实现现代化应用的一个重要手段,其核心是通过 CICD 工具链构建完善的持续集成和持续部署服务,加速软件团队的开发效率,提高交付质量。

Jenkins 是一款非常受欢迎开源的 CICD 工具,主要用于自动化构建、测试和部署流程。Jenkins 常用的部署方式是运行在 kubernetes(k8s)中,Jenkins master(controller)单独部署在 deployment 或 statefulset 的 POD 中,agent POD 按需动态生成。这种方式提高了 Jenkins 的可用性和弹性的能力,同时提高了资源利用率,维护的方式也更简单。但是 k8s 从 1.24 开始有个比较大的变化 – 正式移除对 Dockershim 的支持。通常 CI 的一个重要的任务是构建 docker 镜像,在 1.24 之前这依赖于 Jenkins agent POD 所在的宿主机的 docker daemon 进程执行镜像构建操作。1.24 之后由于 docker daemon 进程已被移除,默认使用 ContainderD,因此如何使用 Jenkins 在 containerd 环境构建镜像是我们需要考虑的问题。

此外我们注意到越来越多的客户的各种工作负载,尤其是容器环境的工作负载中已经使用或者希望使用 AWS Graviton 实例实现降本增效。AWS Graviton 是由 AWS 定制基于 ARM 架构的处理器。基于 Graviton 处理器的 EC2 实例可为云工作负载提供最佳性价比。AWS Graviton 实例是基于容器的工作负载的理想选择。利用基于 Graviton 的实例作为容器节点的优势的第一步是确保所有生产软件依赖项都支持 arm64 架构,这是因为为 x86_64 节点构建的映像是无法在 arm64 主机上运行,反之亦然。目前绝大多数容器生态系统都支持这两种架构(x86_64 和 arm64),并且通常通过多架构镜像实现透明化自动部署对应节点架构的正确镜像。

本篇将以 Jenkins 为例,深度剖析讲解如何在 EKS 中部署高可用、可扩展、高性能的 Jenkins 服务。同时我们也会利用 Karpenter 提高 Jenkins 的弹性能力,例子中包含基于 containerd 环境的可以构建多架构镜像的 Jenkins 流水线。

架构设计

在本文的例子中,我们将 Jenkins 部署在 EKS 中,并使用它构建 java springboot 应用多架构(x86 和 arm64)镜像。 

步骤说明:

  1. 提交代码到 Github,通过 webhook 通知到 Jenkins Master(controller)。
  2. Jenkins 通过流水线代码和配置需要分别启动 x86 和 arm64 的 Agent POD 执行构建任务。
  3. Karpenter 检测到 pending 状态的 Agent POD 会根据 POD 的 node selector 的属性和值创建和启动对应架构的 EC2 node。
  4. Agent POD 执行构建任务并将镜像上传到镜像仓库 ECR。
  5. Agent POD 任务完成后通知 Jenkins Master 回收 POD。Karpenter 也会根据配置进行 node 资源的回收。

弹性设计

Jenkins 依赖 Kubernetes plugin 在 EKS 中扩展 agent。它是一个用于在 Kubernetes 集群中运行动态代理的 Jenkins 插件。该插件为每个启动的 Agent 创建一个 Kubernetes Pod,并在每次构建完成后停止它。Agent 被启动为 inbound agent,因此 agent 容器会自动连接到 Jenkins 控制器。利用 Karpenter 弹性伸缩能力,调度 Jenkins Agents,提高调度效率,降低成本。

Karpenter 是由 AWS 开源的高性能 Kubernetes 集群自动扩缩程序。利用 Karpenter 弹性伸缩能力,调度 Jenkins Agents,提高调度效率。利用 Karpenter Provisioner 提供的灵活计算选项支持创建不同架构节点,优先启动 Spot 节点节省成本。利用 node selector 或亲和度等策略结合 karpenter 的 well-known labels,可以方便地将 Jenkins agent pod 调度到指定架构的 EKS 节点。

编译方式

我们需要评估开发语言、依赖库 对 arm64 的适配,通常来说:

解释和编译的 bytecode 语言无需修改即可运行

  • Python,Java,Ruby,PHP,Node.js,其他
  • .NET Core 支持 Linux 和 arm64

编译的应用程序需要针对 arm64 重新编译

  • 主流的编译器均支持 arm64
  • C,C++,Go,Rust 均支持 arm64

AMIs:大多数主要的 Linux 发行版,包括 NetBSD/FreeBSD 都有 arm64 版本

大多数 AWS 工具和开发工具包透明地支持 Graviton2:

  • AWS CLI v1,AWS CLI v2,CloudWatch agent,SSM agent
  • SDKs for C/C++,node.js,Python,Go,Java,.NET

多架构编译包含多种方式,以下实我们针对不同的架构编译程序的方式对比和总结:

编译方式 优点 缺点

本地编译

native compile

效率最高,操作也比较容易 需要具备不同架构的主机,从而支持本地编译

仿真

Emulation

若目标系统不满足本地编译条件,可以采用仿真模拟硬件。 由于硬件是仿真模拟的,编译效率比本地编译低

交叉编译

cross compile

适用的目标系统一般都是内存较小、显示设备简陋甚至没有,没有能力在其上进行本地编译; 效率低且困难,主要难点在两个方面:

  1. 不同体系架构用不同的及其特性(Word size,Endianness,Alignment,Default signedness,NOMMU)
  2. 交叉编译时的主机环境与目标环境不同

三种方式中推荐本地编译,其效率最高,操作也比较容易,实际案例中采用最多。

构建工具

由于我们需要在 Jenkins agent POD 容器中构建镜像,所以这里的构建方式和直接在虚拟机中构建镜像有所不同。在 EKS 1.24 之前需要依赖宿主机的 docker daemon 构建镜像,即是 Jenkins agent POD 挂载宿主机的 docker.sock,Jenkins agent 本身只是一个客户端,真正执行镜像构建的工作仍然交给宿主机的 docker daemon 完成。Kubernetes 1.24 之后 docker daemon 已被移除,因此默认配置下该方式已经不再适用。那么在 1.24+版本的 EKS 中还可以使用 docker 构建镜像吗,答案是可以的,可选的方案有 DooD(Docker Out Of Docker)以及 DinD(Docker in Docker)。当然还有一些其它的工具例如 Nerdctl,Kaniko 等可以取代 Docker。下面我们将重点介绍这几种方式的使用方法,特点和使用场景。

DooD(Docker out of Docker) – 这种方式和 1.24 之前版本类似,Docker Daemon 运行在宿主机上面,客户端通过 docker.sock 和 docker daemon 通信。不过这种方式需要以 K8s daemonset 方式安装 docker daemon。

DinD(Docker in Docker)- 这种方式 Docker 客户端和 docker daemon 在同一个 POD 中,我们使用 Sidecar 容器的概念,并创建一个包含 docker daemon 容器的 Kubernetes Pod。该容器在/var/lib/docker 上启动 Docker 守护程序。我们可以使用 EmptyDir,并将其挂载为 dind 容器内的/var/lib/docker。这使得在主容器中发出的 Docker 命令可以与 Docker 守护程序进行通信。

Nerdctl 是与 Docker 兼容的 CLI for containerd,其支持 Compose、Rootless、eStargz、OCIcrypt 和 IPFS,与 docker 命令行语法类似。

Kaniko 是一个用于在容器或 Kubernetes 集群中从 Dockerfile 构建容器镜像的工具。Kaniko 不依赖于 Docker 守护程序,并在用户空间内完全执行 Dockerfile 中的每个命令。这使得在不能轻松或安全地运行 Docker 守护程序的环境中构建容器镜像成为可能,比如标准的 Kubernetes 集群。

以下是 4 种构建工具和方法的对比:

Pros Cons Recommended
DOOD 避免在容器内部运行 Docker 守护程序的复杂性,而且不需要特权容器
避免在系统中有多个 Docker 镜像缓存(因为主机上只有一个 Docker 守护程序),这可能对存储空间有限的系统有益

上下文隔离较差,因为 Docker CLI 在不同的上下文中运行于 Docker 守护程序。这会导致一些问题,如容器命名冲突、挂载路径和端口映射

运行 Docker CLI 的容器可以操纵主机上运行的任何容器。它可以删除主机上由其他实体创建的容器,甚至创建不安全的特权容器,从而使主机面临风险

No
DIND 宿主机和 Docker 守护程序是分离的,它们具有层次结构,这使得它们更容易管理 您需要使用特权选项启动
默认情况下无法使用 docker-compose
No
Nerdctl 兼容 docker cli
支持 Compose、Rootless、eStargz、OCIcrypt 和 IPFS

不支持 manifest 命令

需要开启特权容器

目前不能支持 IRSA

Yes
Kaniko 无需 root 特权的情况下可以运行。不需要特权权限
能够构建符合 OCI(Open Container Initiative)规范的镜像,并且可以从常规的 Dockerfile 文件进行构建
只能在 Linux 上运行
不支持 manifest 命令
Yes

在 EKS containerd 环境我们推荐使用 Kaniko 或 Nerdctl 来构建您的镜像。如果您已经习惯使用 docker 的命令,可以 Nerdctl,否则可以使用 Kaniko。

另外,因为 Nerdctl 和 Kaniko 目前不支持 manifest 命令,因此需要单独使用第三方工具推送 manifest 推送多架构清单(https://github.com/estesp/manifest-tool)。

存储

Jenkins 对存储的需求包含以下几个方面:

  • Jenkins Controller HOME 目录 – 保存控制节点配置,保存任务配置,保存插件和用户凭据,系统日志记录,Jenkins 控制面本身不支持高可用此数据需要长期保存,且不能丢失。
  • Jenkins Agent WorkSpace 目录 – 每个任务数据相对独立,频繁的文件打开、关闭、压缩等动作,对 IOPS 以及读写延迟性能敏感,任务结束后中间数据不需要长期保存。
  • 本地 Repo 缓存目录 – 共享库目录如 Maven 本地 Repo 仓库,长期保存基础 JAR 包资源,包版本多需要大存储空间,可共享使用,但要留意权限配置。
  • 数据备份 – 集成 Thin Backup 插件,配置简单,支持指定路径、周期、保留配置。

根据需求我们建议的存储方案如下:

存储需求 持久性需求 可用性需求 性能需求 存储服务选择建议
JENKINS_HOME 共享文件存储
WorkSpace 高,跑实际构建任务 EBS / 实例存储
~/.m2 中,共享 Maven 基础库 共享文件存储,对象存储
Thin Backup 共享文件存储

使用 EFS 应对高可用和数据共享场景,实现:

  • Jenkins 控制面出现重启或迁移可用区后可以随时访问到配置文件
  • 在 Maven 构建中共享 .m2 本地库资源,加快构建速度
  • 对共享数据和备份数据进行自动生命周期管理以节省成本

前置条件

安装 EKS

参考 https://docs.thinkwithwp.com/eks/latest/userguide/getting-started.html

创建 EKS 集群。同时需要安装本次实验需要用到的 EKS 插件 – Karpenter,EBS CSI driver,EFS CSI Driver 以及 LoadBalancerController。

安装 Jenkins

可以从 https://github.com/gaussye/jenkins-multi-arch-workshop.git 获取标准 k8s 资源的 yaml 文件,整体文件列表如下。本次我们将使用手动部署的方式来进行配置和演示:

$ git clone https://github.com/gaussye/jenkins-multi-arch-workshop.git
$ tree helm-deployment /
kubernetes-jenkins/
├── Jenkins-sa.yaml
├── Jenkins-values.yaml
├── README.md 
└── volume.yaml

创建命名空间 NameSpace:

$ kubectl create namespace devops-tools

创建相关 Role 和 Service Account:

部署 Jenkins Controller 前需要先创建所需的 k8s role,rolebinding 和 service account 资源

$ kubectl apply -f Jenkins-sa.yaml  

准备持久化存储卷:

$ kubectl apply -f volume.yaml

部署 Jenkins Controller 资源:

$ helm repo add jenkinsci https://charts.jenkins.io
$ helm repo update
$ helm install jenkins -n jenkins -f jenkins-values.yaml jenkinsci/jenkins

$ kubectl get all -n devops-tools
NAME                READY   STATUS    RESTARTS   AGE
pod/jenkins-0   2/2        Running      0                3h8m

NAME                             TYPE                  CLUSTER-IP      EXTERNAL-IP                                                          PORT(S)              AGE
service/jenkins              LoadBalancer   172.20.12.146   xxx.xxxx.elb.ap-southeast-1.amazonaws.com   80:32671/TCP   3h8m
service/jenkins-agent   ClusterIP           172.20.251.67   <none>                                                                   50000/TCP        3h8m

NAME                       READY   AGE
statefulset.apps/jenkins   1/1     3h8m
  ...

镜像仓库

创建镜像仓库:

$ aws ecr create-repository --repository-name= java-demo

步骤

下面我们的例子通过 Jenkins 和 kaniko 编译打包 java springboot 应用,然后构建 amd64 和 arm64 镜像并推送到 ECR 镜像仓库。流水线设计如下:

1. Jenkins 安装好会自动生成管理员账号,账号名是 admin

账号密码通过以下方式获取:

jsonpath="{.data.jenkins-admin-password}"
secret=$(kubectl get secret -n devops-tools jenkins -o jsonpath=$jsonpath)
echo $(echo $secret | base64 --decode)

2. 我们这里通过 loadbalancer 暴露服务,可以通过 service 获取访问地址

$ Kubectl get svc -n devops-tools

3. 登录进 Jenkins 后我们创建一个 pipeline 执行构建任务

仓库地址输入:https://github.com/gaussye/jenkins-multi-arch-workshop.git

密钥:本次实验使用的是公共的仓库地址,并不需要输入密钥

分支选择使用默认的:*/master

script-path 输入:Jenkinsfile-kaniko

Jenkins 有 2 种流水线分别为声明式流水线与脚本化流水线,脚本化流水线是 Jenkins 早期版本使用的流水线脚本,我们推荐使用声明式流水线。声明式流水线可以完全通过代码编排流水线(pipeline as code),流水线的编辑方式更简单,更重要的是流行线本身可以被源代码控制,使团队能够通过 git 标准流程编辑、审查和迭代开发工作。

下面是我们代码仓库的结构和说明:

我们在一个 Jenkins 流水线里加入 parallel stage 可同步完成不同任务,amd64 和 arm64 的任务会并行运行 – 即同时启动两个 POD 分别执行任务,这样会提高整个构建的效率。最后通过 manifest 工具推送多架构镜像清单。

Jenkinsfile-kaniko:

因为我们推荐使用 Native 编译,因此例子中我们为 amd86 和 arm64 准备不同的 POD 模版(Jenkins-kaniko-amd64.yaml, Jenkins-kaniko-arm64),模版通过 nodeSelector 让 karpenter 拉起对应架构的 EKS node。

4. 简单配置后我们点击运行流水线,稍等一会我们便可以看到流水线运行成功了。

清理

执行以下命令清理 Jenkins 资源:

helm uninstall jenkins -n devops-tools
kubectl apply -f volume.yaml
kubectl apply -f Jenkins-sa.yaml
kubectl create namespace devops-tools

结论

在本篇博客中,我们详细介绍了在 EKS 中如何使用 Jenkins 基于 Native 编译方式构建多架构镜像,为在容器场景下采用基于 arm 架构的 Graviton 实例做好准备。同时我们也充分利用 AWS 开源的 Karpenter + Spot 提高伸缩效率,降低成本。在 containerd 环境中使用 Kaniko 进行镜像构建是一个不错的选择。此外,还结合了 Amazon EFS 和 EBS 的优势为 Jenkins 的不同目录设计不同的存储方案,极大提高了构建的性能。

本篇作者

叶鹏勇

亚马逊云科技解决方案架构师,负责容器以及 DevOps 相关领域的方案和架构设计工作。在容器,微服务,SRE 相关领域有多年的实战经验。

丁洁羚

AWS 弹性计算解决方案架构师,主要负责 AWS 弹性计算相关产品的技术咨询与方案设计。专注于弹性计算相关的产品和方向。

王志达

AWS 解决方案架构师,主要负责基础架构如计算、存储的云端设计、改造和优化方案。有多年存储、容器平台和 Devops 运维经验。

梁绮莹

亚马逊云科技解决方案架构师,专注于数字原生企业的云架构设计和咨询,负责支持全球头部电商公司云项目。在云网络、应用交付、应用层安全、CDN、容器及微服务等领域有丰富的实战经验。