从 Jenkins 迁移到 Jenkins X:一场持续交付之旅

三世轮回 提交于 2019-11-26 13:59:11

背景

在 dailymotion,我们信奉 DevOps 最佳实践,并且重度使用了 Kubernetes。我们的部分产品(并非全部)已经部署在 Kubernetes 上。在迁移我们的广告技术平台时,为了赶时髦(作者你这么直白的吗?)我们希望完全采用“Kubernetes 方式”或云原生!这意味着我们需要重新定义我们的整个 CI/CD 管道,并使用按需分配的动态环境来替代永久性的静态环境。我们的目标是为我们的开发人员提供最好的支持、缩短产品上市时间并降低运营成本。

我们对新 CI/CD 平台的初始要求是:

  • 如果可能的话,尽量避免从头开始:我们的开发人员已经习惯使用 Jenkins 和声明性管道,目前这些东西都还好。

  • 采用公有云基础设施——谷歌云平台和 Kubernetes 集群。

  • 与 gitops 兼容——因为我们需要版本控制、评审和自动化。

CI/CD 生态系统中有不解决方案,但只有一个符合我们的要求,也就是 Jenkins X,它基于 Jenkins 和 Kubernetes,原生支持预览环境和 gitops。

Jenkins X: Kubernetes 上的 Jenkins

Jenkins X 是一个高度集成化的 CI/CD 平台,基于 Jenkins 和 Kubernetes 实现,旨在解决微服务体系架构下的云原生应用的持续交付的问题,简化整个云原生应用的开发、运行和部署过程。

你猜的没错,Jenkins X 只能在 Kubernetes 集群上运行

Jenkins X 的搭建过程非常简单,官方网站上已经提供了很好的文档。由于我们已经在使用 Google Kubernetes Engine(GKE),因此 jx 命令行工具可以自行创建所有的内容,包括 Kubernetes 集群。在几分钟内就可以获得一个完整的可运行系统真的让人印象深刻。

Jenkins X 提供了很多快速入门和模板,不过我们想重用现有代码库中的 Jenkins 管道。所以,我们决定另辟蹊径,并对我们的声明性管道进行重构,让它们与 Jenkins X 兼容。

实际上,重构工作并不是只针对 Jenkins X,而是为了能够使用 Kubernetes 插件 在 Kubernetes 上运行 Jenkins。

如果你习惯使用“经典”的 Jenkins,并在裸机或虚拟机上运行静态从节点,那么这里的主要变化是每个构建都将在自己的短存活期自定义 pod 上执行。你可以指定管道的每个步骤应该在哪个容器中执行。插件的源代码中提供了一些 管道示例 。

我们面临的挑战是如何定义容器的粒度,以及它们应该包含哪些工具:拥有足够多的容器让我们可以在不同的管道之间重用它们的镜像,但又不至于太多,这样容易维护——我们可不想要花太多时间重建容器镜像。

在之前,我们在 Docker 容器中运行大部分管道步骤,当我们需要自定义步骤时,就在管道中进行即时构建。

这种方式较慢,但更容易维护,因为所有内容都是在源代码中定义的。例如,升级 Go 运行时可以在单个拉取请求中完成。因此,需要预先构建容器镜像似乎是现有的设置中增加了更多的复杂性。它还具备一些优点:代码库之间的重复代码更少、构建速度更快,并且没有了因为第三方构建平台宕机而造成的构建错误。

在 Kubernetes 上构建镜像

在 Kubernetes 集群中构建容器镜像是一件很有趣的事情。

Jenkins X 提供了一组构建包,使用“Docker 中的 Docker”在容器内部构建镜像。但随着新容器运行时的出现,以及 Kubernetes 推出了 Container Runtime Interface(CRI),我们想知道其他选择是否可行。 Kaniko 是最成熟的解决方案,符合我们的需求。我们很激动,直到遇到以下 2 个问题。

  1. 第一个问题是阻塞性的:多阶段构建不起作用。通过使用搜索引擎,我们很快发现我们并不是唯一受到这个问题影响的人,而且当时还没有修复或解决方法。不过,Kaniko 是用 Go 语言编写的,而我们又是 Go 语言开发人员,所以为什么不看一下 Kaniko 的源代码呢?事实证明,一旦我们找到了问题的根本原因,修复工作就非常简单。Kaniko 维护人员很快就合并了修复,一天后,修复的 Kaniko 镜像就已经可用了。

  2. 第二个问题是我们无法使用相同的 Kaniko 容器构建两个不同的镜像。这是因为 Jenkins 并没有正确地使用 Kaniko——因为我们需要先启动容器,然后再进行构建。这一次,我们在谷歌上找到了一个解决方法:声明足够多的 Kaniko 容器来构建镜像,但我们不喜欢这个方法。所以我们又回到了源代码,在找到了根本原因后,修复就很容易了。

我们测试了一些方案,想自己为 CI 管道构建自定义的“工具”镜像,最后,我们选择使用单个代码库,每个分支使用一个镜像,也即一个 Dockerfile。因为我们的代码托管在 Github 上,并使用 Jenkins Github 插件来构建代码库,所以它可以构建所有的分支,并基于 webhook 触发事件为新分支创建新的作业,所以管理起来十分容易。每个分支都有自己的 Jenkinsfile 声明性管道文件,使用 Kaniko 构建镜像,并将构建好的镜像推送到容器注册表。Jenkins 帮我们做了很多事情,所以可以快速地添加新镜像或编辑现有的镜像。

声明所请求的资源

我们之前的 Jenkins 平台存在的一个主要问题来自于静态从属节点或执行程序,以及有时候会在高峰时段出现的长构建队列。Kubernetes 上的 Jenkins 可以轻松地解决这个问题,特别是运行在支持集群自动缩放器的 Kubernetes 集群上时。集群将根据当前的负载添加或移除节点。不过这是基于所请求的资源,而不是基于所使用资源的情况。

这意味着我们需要在构建 pod 模板中定义所请求的资源——比如 CPU 和内存。然后,Kubernetes 调度程序将使用这些信息查找匹配的节点来运行 pod——或者它可能决定创建一个新节点。这样就不会出现长队列了。

但是,我们需要谨慎定义所需资源的数量,并在更新管道时更新它们。因为资源是在容器级别而不是 pod 级别定义的,所以处理起来会更加复杂。 但我们不关心限制问题,我们只关心请求,所以我们只将对整个 pod 的资源请求分配给第一个容器(jnlp 那个)——也就是默认的那个。

以下是 Jenkinsfile 的一个示例,以及我们是如何声明所请求的资源的。

pipeline {
    agent {
        kubernetes {
            label'xxx-builder'
            yaml"""
kind: Pod
metadata:
  name: xxx-builder
spec:
  containers:
  - name: jnlp
    resources:
      requests:
        cpu:4
        memory:1G
  - name:go
    image: golang:1.11
    imagePullPolicy: Always
   command: [cat]
    tty: true
  - name: kaniko
    image: gcr.io/kaniko-project/executor:debug
    imagePullPolicy: Always
   command: [cat]
    tty: true
"""
        }
    }

    stages {
    }
}

Jenkins X 的预览环境

现在我们有了所有工具,可以为我们的应用程序构建镜像,我们已准备好进行下一步:部署到“预览环境”!

通过重用现有工具(主要是 Helm),Jenkins X 可以轻松部署预览环境,只要遵循一些约定,例如镜像标签的名称。Helm 是 Kubernetes 应用程序的包管理器。每个应用程序都被打包为一个“chart”,然后可以使用 helm 命令行工具将其部署为“release”。

可以使用 jx 命令行工具部署预览环境,这个工具负责部署 Helm 的 chart,并为 Github 的拉取请求提供注释。在我们的第一个 POC 中,我们使用了普通的 HTTP,因此这种方式奏效了。但现在没有人再用 HTTP 了,那我们使用加密的吧!

多亏了有 cert-manager ,在 Kubernetes 中创建摄入资源时可以自动获取新域名的 SSL 证书。我们尝试在设置中启用 tls-acme 标志——使用 cert-manager 进行绑定——但它不起作用。

于是我们阅读了 Jenkins X 的源代码——它也是使用 Go 开发的。稍后修改一下就好了,我们现在可以使用安全的预览环境,其中包含了 let’s encrypt 提供的自动证书。

预览环境的另一个问题与环境的清理有关。我们为每个拉取请求创建了一个预览环境,在合并或关闭拉取请求时需要删除相应的环境。这是由 Jenkins X 设置的 Kubernetes 作业负责处理的,它会删除预览环境使用的命名空间。问题是这些作业并不会删除 Helm 的 release——因此,如果你运行 helm list,仍然会看到旧的预览环境列表。

对于这个问题,我们决定改变使用 Helm 部署预览环境的方式。我们决定使用 helmTemplate 功能标志,只将 Helm 作为模板渲染引擎,并使用 kubectl 来处理生成的资源。这样,临时的预览环境就不会“污染”Helm release 列表。

将 gitops 应用于 Jenkins X

在初始 POC 的某个时候,我们对设置和管道非常满意,并准备将 POC 平台转变为可投入生产的平台。第一步是安装 SAML 插件进行 Okta 集成——允许内部用户登录。它运作得很好,但几天后,我发现 Okta 集成已经不在了。我在忙其他的一些事情,所以只是问了同事一下他是否做了一些更改,然后继续做其他事情。几天后再次发生这种情况,我开始调查原因。我注意到 Jenkins pod 最近重启过。但我们有一个持久的存储,而且作业也在,所以是时候仔细看看了!

事实证明,用于安装 Jenkins 的 Helm chart 有一个启动脚本通过 Kubernetes configmap 重置了 Jenkins 配置。当然,我们无法像管理在 VM 中运行的 Jenkins 那样来管理在 Kubernetes 中运行的 Jenkins!

我们没有手动编辑 configmap,而是退后一步从大局看待这个问题。configmap 是由 jenkins-x-platform 管理的,因此通过升级平台来重置我们的自定义更改。我们需要将“定制”内容保存在一个安全的地方,并对变化进行跟踪。

我们可以使用 Jenkins X 的方式,并使用一个 chart 来安装和配置所有内容,但这种方法有一些缺点:它不支持“加密”——我们的 git 代码库中包含了一些敏感的信息——并且它“隐藏”了所有子 chart。因此,如果我们列出所有已安装的 Helm 版本,只会看到其中一个。但是还有其他一些基于 Helm 的工具,它们更适合 gitops。 Helmfile 就是其中之一,它通过 helm-secrets 插件 和 sops原生支持加密。

迁移

从 Jenkins 迁移到 Jenkins X 以及如何使用 2 个构建系统处理代码库也是我们整个旅程的一个很有趣的部分。

首先,我们搭建新 Jenkins 来构建“jenkinsx”分支,同时更新了旧 Jenkins 配置,用来构建除“jenkinsx”分支之外的所有内容。我们计划在“jenkinsx”分支上构建新管道,并将其合并。

对于初始 POC,这样做没有问题,但当我们开始使用预览环境时,不得不创建新的拉取请求,并且由于分支的限制,那些拉取请求不是基于新的 Jenkins 构建的。因此,我们选择在两个 Jenkins 实例上构建所有内容,只是在 Jenkins 上使用 Jenkinsfile 文件名和在新 Jenkins 上使用 Jenkinsxfile 文件名。迁移之后,我们将会更新这个配置,并重命名文件。这样做是值得的,因为它让我们能够在两个系统之间平稳过渡,并且每个项目都可以自行迁移,不会影响到其他项目。

我们的目的地

那么, Jenkins X 是否适合所有人?老实说,我不这么认为 。并非所有功能和支持的平台——git 托管平台或 Kubernetes 托管平台——都足够稳定。但是,如果你有足够的时间进行深挖,并选择了适合自己用例的功能和平台,就可以改善你的管道。这将缩短发布周期,降低成本,如果你对测试也非常认真,那么对你的软件质量也应当充满信心。

我们的旅程还没有结束,因为我们的目标仍在继续:Jenkins X 仍然处于开发阶段,而且它本身正在走向 Serverless,目前正在使用 Knative build。它的目标是云原生 Jenkins。

我们的旅程也在继续,因为我们不希望它就这样结束。我们目前的完成的一些事情并不是我们的最终目的地,它只是我们不断演化的一个步骤。这就是我们喜欢 Jenkins X 的原因:与我们一样,它遵循了相同的模式。你也可以开始你自己的旅程~

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!