本文首发于:Jenkins 中文社区html
原文连接 做者:Alberto Alvarezjava
咱们如何运用 Terraform、Packer、Docker、Vault、ELB、ASG、ALB 或 EFS 等 AWS 服务实现 Jenkins Cloud-native,以及咱们一路走来的收获node
咱们使用 Jenkins 搭建持续交付流水线,和其余不少团队同样,这些年咱们围绕 Jenkins 建立了不少工做流程和自动化。Jenkins 是咱们团队取得成功的关键,让咱们可以在上一季度顺利进入生产677次,搭建及部署时长平均为12分钟。git
咱们的大部分应用和基础设施能够看做云原生,但当时 Jenkins 服务并不彻底适合这个分类:服务在单个服务器上运行,同时不少任务直接在 master 上运行,其部分手动配置包括 secret、插件、定时任务和 startup hacking 的普通膨胀,该膨胀是自2014年首次搭建起不断累积而成。github
Jenkins 不只变成了单体服务和单点故障,并且拆除及重建 Jenkins 对企业也是很大的风险。docker
咱们决定必须作出改变。这篇博客说明了咱们如何运用 Terraform、Packer、Docker、Vault、和 ELB、ASG、ALB 或 EFS 等 AWS 服务实现 Jenkins Cloud-native,以及咱们一路走来的收获。npm
当时不得不面对的关键问题是:若是咱们将 Jenkins 服务置于一个容器/自动缩放实例中,咱们须要恢复何种状态?编程
问题的答案并不简单,值得一提的是,有个 Jenkins 特别兴趣小组(SIG)已经识别出全部致使这一 Jenkins 状态的存储组件。这是一个很棒的起点,由于咱们至少得确保那篇文章列出的全部存储类型都考虑在内。vim
这不是新问题。不少团队使用 Docker 容器运行 Jenkins,官方 Jenkins Docker 镜像也获得良好维护。如《Jenkins Dokcer 镜像》文档中解释的:缓存
docker run -p 8080:8080 -p 50000:50000 -v jenkins_home:/var/jenkins_home jenkins/jenkins:lts
复制代码
这会把 workspace 存在 /var/jenkins_home。全部的 Jenkins 数据(包括插件和配置)都存在上述目录里。建立一个明确的 volume 能够方便管理和附加到另外一个容器进行升级。
上述示例装载主机上的 jenkins_home,其中包括全部 Jenkins 状态。而后该目录能够存在一个外部磁盘上,好比 Kubernetes 持久化存储卷。或者,若是 Jenkins 在 EC2 上运行,该目录可存在一个外部 EBS 或 EFS 卷上。
这是一种有效的方法,但咱们认为这个方法不能达到咱们的标准,由于 jenkins_home 不只包括状态,还包括配置。Block storage 拥有大量用户案例,但一个小小的配置修改就必须进行 snapshot 恢复操做,这彷佛并不算是好的解决方案。此外,咱们并非想转移问题:外部存储没法免去手动配置、凭据储存在文件系统等问题。
过去,咱们用了 Jenkins 备份插件,该插件基本上把配置修改备份在源码控制里,容许配置恢复。这个插件的设计想法很棒,但咱们决定不使用它,由于咱们没法轻松控制哪些数据实现备份,并且该插件自2011年就没有任何更新了。
这样的话,若是咱们把 jenkins_home 建立成我的 Git repo,并自动提交对 Jenkins 所作的修改呢?此处的关键是排除单独储存的任何二进制文件、secrets 或大型文件(稍后详细介绍)。咱们的 .gitignore 文件以下所示:
/.bash_history /.java/ /.kube/ /.ssh/ /.viminfo /identity.key.enc /jobs/ /logs/ /caches/ # Track static worker and exclude ephemeral ones /nodes/** !nodes/static-node/config.xml /org.jenkinsci.plugins.github_branch_source.GitHubSCMProbe.cache/ /plugins/ /saml-idp-metadata.xml /saml-jenkins-keystore.jks /saml-jenkins-keystore.xml /saml-sp-metadata.xml /scm-sync-configuration/ /scm-sync-configuration.success.log /secret.key /secret.key.not-so-secret /secrets/ /updates/ /workspaces/ 复制代码
几乎全部的纯文本配置都正在 Git 实现持久化。为了给 Jenkins 提供这一配置,咱们要作的就是检查 startup 上的 repo;事情渐渐成形。
Jenkins 要访问不少地方,也就是说咱们须要一个安全的 secret 存储空间。由于咱们是 HashiCorpVault 的重度用户,因此天然而然就选了这个工具,不过遗憾的是,Vault 没法涵盖全部场景。好比,scm-branch-source 流水线插件须要 SCM 的认证凭据,并默认为 Jenkins 凭据插件。每次从 Vault 动态检索这些,咱们都须要同步一个仓库,这可能致使错误,也会须要额外的精力去维护。
这就是为何咱们采用 Vault 与 Jenkins 凭据混合的方法: 1. 在 startup 实例中,Jenkins 进行认证,VAult采用 IAM 认证方法。 2. 一个引导脚本检索 Jenkins master.key 和凭据插件所用的其余加密密钥。更多详情请参阅这篇文章。 3. 储存在 jenkins_home/credentials.xml 上的凭据如今可由 Jenkins 解密和访问。
用 Vault 彻底取代凭据插件是咱们将来可能探索的问题,不过咱们很开心这个方法知足了安全性要求, 同时能轻松与 Jenkins 的其他功能实现集成。
问题从这一步开始变得棘手:jenkins_home/jobs and jenkins_home/workspaces 都含有介于非结构化数据、建立制品和纯文本之间的混合体。这个信息颇有价值,能够帮助咱们审计、理解以前的流水线 build。这些 build 尺寸很大,并且不太适合 SCM 同步,所以这两个目录都排除在 .gitignore 以外了。
那咱们把这些储存在哪儿呢?咱们认为 block storage 最适合存储这种数据。做为 AWS 的重度用户,使用 EFS 彻底说得通,由于 EFS 的文件存储可扩展、可用性高并能够经过网络访问,很是易于使用。咱们使用 Terraform 整合了 AWS EFS资源,并用 AWS 备份服务制定了一份按期备份计划。
在 startup,咱们将 EFS 卷 、符号连接 jenkins_home/jobs 和 jenkins_home/workspaces 装载到 EFS 目录上,而后启动 Jenkins 服务。
接下来,Jenkins 服务是惟一能够读写任务 /workspace 数据的界面。值得一提的是,咱们有一个 Jenkins 任务按期删除几周前的任务和 workspace 数据,这样数据不会一直增长。
你可能想知道这些是如何凑在一块儿的?我甚至没说过在哪里运行 Jenkins!咱们普遍使用 Kubernetes,花了一些时间思考将 Jenkins 做为容器来运行,可咱们决定使用 Packer 和 EC2 来运行 Jenkins master,用短暂 EC2 实例运行这些任务。
尽管将 master 和 worker 双双做为容器运行的想法颇有用,但咱们在当前 Kubernetes 集群里没有找到存储 Jenkins 的地方。并且只是为了 Jenkins 就新建一个集群彷佛有点儿“杀鸡用牛刀”。此外,咱们想保留从其他服务中解耦的基础设施的关键部分。这样的话,若是 Kubernetes 升级对咱们的 app 有影响,咱们但愿至少能够运用 Jenkins 进行回滚。 运行“Docker in Docker”还有另外一个问题,这个问题有解,不过仍是须要说明一下,由于咱们的 build 常常用到 Docker 命令。
其体系架构以下:
能使用 EC2 实例让过渡更顺畅:咱们当时经过 Jenkins EC2 插件用临时 worker node 运行流水线工做,并在声明式流水线代码上调用了这一逻辑,因此没必要重构就能用 Dokcer 代理节点是一个加分项。其他工做就是 Packer 和 Terraform 代码,这是咱们已经很熟悉的部分了。
由于插件也是状态!咱们在这个项目里想要解决的问题之一就是更好地审计、管理插件。在手动场景中,插件管理可能不受控制,很难了解安装插件的时间和缘由。
大多数 Jenkins 级别的插件配置能够在常规 Jenkins 配置 xml 文档中找到,但安装插件也致使 jar 制品、元数据、图片和其余文件存在 jenkins_home/plugin 目录。
一种方法是在 EFS 中存储插件,不过咱们想将 EFS 使用率保持在最低水平,这没法解决问题,只是转移问题。这就是为何咱们选择对插件安装进行“Packer 化”。
基本上,在咱们的 AMI 定义中,有一个插件文件罗列了插件和版本,大体以下:
# Datadog Plugin required to send build metrics to Datadog datadog:0.7.1# Slack Plugin required to send build notifications to Slack slack:2.27 复制代码
而后,咱们的 AMI provision 脚本解析该文件,用 Jenkins CLI 安装插件和所选版本:
# Wrapper function for jenkins_cli jenkins_cli() { java -jar "$JENKINS_CLI_JAR" -http -auth "${user}:${pw}" "$@" }for plugin in "${plugins[@]}"; do echo "Installing $plugin" jenkins_cli install-plugin "$plugin" -deploy done 复制代码
而后,任何须要安装的新插件或升级到当前安装版本的版本升级都须要 GitHub Pull Request,这会触发搭建新 AMI。完美!
根据定义,Jenkins 要安装不少软件才能建立、测试和部署。首先,咱们不想让 master node 运行任何任务,因此咱们避免安装任何与任务相关的软件。Master 的主要任务是在其余短暂 worker node 上提供界面、编排 builds。
这意味着咱们能够在 worker node 上安装所需工具,但咱们决定尽量多地使用 docker run。这是由于咱们是使用 Scala、Java、Node、Golang、Python等其余编程语言的多语言组织。为全部这些软件栈维护不一样 build 工具可能让 worker node 设置变得有点儿复杂。
以 JavaScript 为例,咱们想让 Jenkins 针对 install 和 test 等 app 运行 yarn 命令。简单将加载检查过的 repo 目录做为一个 volume 安装到 Docker 容器里,从该容器中运行任何命令。如下为运用 Groovy 工做流代码的例子:
def node(command, image) { def nodeCmd = [ 'docker run -i --rm', '-u 1000', // Run as non-root user '-v ~/.npmrc:/home/node/.npmrc:ro', '-v ~/.yarn:/home/node/.yarn', '-e YARN_CACHE_FOLDER=/home/node/.yarn/cache', "-v ${env.WORKSPACE}:/app", '--workdir /app', "${image}" ].join(' ') sh "${nodeCmd} ${command}" } 复制代码
而后,咱们检查仓库后能够调用这个功能:
checkout scm node('yarn install --frozen-lockfile', 'node:12.6.0-alpine') 复制代码
漂亮收尾!由于除了 Docker 后台程序或 kubectl,咱们没必要在 worker machine 上安装、维护所用工具的多个版本。咱们也相信 build 命令在本地和 CI 环境之间是一致的,由于用的是同一个 Docker 镜像。
运用临时 node 建立时要记得缓存依赖。好比,一个 worker node 重建后,咱们丢失了 sbt 缓存,因为缓存必须重建,这致使建立时间变慢。若是外部依赖不可用,这甚至会致使失败。咱们决定将相关依赖缓存在另外一个外部 EFS 上,以求得到更快、更可靠的 build。
Jenkins 是一个很棒的工具,但在管理外部状态上略有不足,所以以 cloud native 的方式建立 Jenkins 较有难度。咱们的方法并不完美,但咱们相信这个方法结合了二者的精华,并且确保安全性、操做简单、有弹性。使人高兴的是,咱们完成这个项目,并把全部的生产 build 迁移到新的 Jenkins 服务以后,能够终止 master server,让自动缩放在几分钟内完成重建,而不会影响之前储存的状态。