在 Kubernetes 中通过 Jenkins 和 Dynamic Slaves 实现 CI/CD

本文将介绍如何通过在 Kubernetes 集群中利用 Jenkins 和 Kubernetes-Jenkins-Plugin 实现动态按需扩展 jenkins-slave 实现 CI/CD。实验环境为阿里云 ACK Kubernetes 托管集群。

前提条件

已创建好的 Kubenretes 集群,包括自建集群和云服务提供商托管集群;

在已创建的 Kubenretes 集群中安装好的 Jenkins 服务;有关安装方式:

  1. 使用云服务提供商控制台安装:阿里云
  2. 使用 Helm Charts 进行安装,参见:Configuring CI/CD on Kubernetes with Jenkins – Medium, Jenkins Chart – Helm
  3. 使用 Docker Hub 镜像自行安装,参见:基于 jenkins 打造 kubernetes on aws 上的 CI/CD 管道 – AWS, jenkins/jenkins – Docker Hub

安装 Kubernetes 插件

以下进行的步骤依赖于 Jenkins 中 Kubernetes Plugin,需要在 Jenkins 设置内 Manage Plugins 页面自行安装。详情可参考 Jenkins Kubernetes Plugin

配置 Kubernetes 集群信息

在全局设置中配置好 Kubernetes 集群的信息,进行连通性测试,由于本示例中针对 Jenkins 创建 Service Account 并赋予了权限,否则可能需要对 Credentials 进行配置。有关证书的配置生成:

  1. 在 Jenkins 的左侧导航栏中,选择系统管理。
  2. 在右侧的 Manage Jenkins 页面,单击系统设置。
  3. 在 Cloud 区域中,单击 Credentials 右侧的 Add。
  4. 将相应集群的 Kube Config 的信息填入对应的输入框中。
Cloud – Kubernetes Configuration in Jenkins
Cloud – Kubernetes Configuration in Jenkins

配置 Pod Template

这一步配置 Dynamic Slaves 的 Pod Template, 构建过程中 master 节点就会按照这个 Pod Template 通过请求 Kubernetes 集群的 API Server 拉起一个容器并挂载为 Jenkins Slave 节点用于构建,而 Pod Template 具体应该包含哪些 container 很大程度上取决于你的应用构建方式和部署方式,下面的示例主要围绕 Docker 镜像打包构建流程和部署到 Kubernetes 集群的部署方式展开。

Jenkins Pod Template Global Configuration
Jenkins Pod Template Global Configuration

查询各类资料的过程中,发现很多教程都会先自定义一个 jnlp-slave 的 container 用于和 master 主节点进行通信,而在本人实测的过程中发现,构建过程中会自动创建一个使用 jenkins/jnlp-slave:3.35-5-alpine 作为镜像的 jnlp-slave 的容器,即 Pod Template 只定义所需容器即可。需要注意的是大部分 alpine 版本的镜像会缺少部分常用的工具,例如实测过程中发现该镜像缺少 curl,导致钉钉推送出现问题,最后使用 wget 暂时解决。

在 Docker 镜像的构建方面,由于 Kubernetes 集群中缺少 Docker Daemon, 且考虑到 Docker 占用的空间较大,并没有采用打包带有 docker-ce 镜像的方式,而是使用了来自 Google 的 Kaniko Project,不依赖 Docker Daemon,可以直接运行在 Kubernetes 集群中。相关配置如下图:

Kaniko Container Template Configuration
Kaniko Container Template Configuration

值得注意的是,本示例中使用的镜像为 gcr.io/kaniko-project/executor:debug,主要是因为在 tag latest 的镜像中缺少对 shell 脚本的支持,entrypoint 直接为 /kaniko/executor,故会对 pipeline 中执行一些脚本产生阻碍。至于国内的用户有个小技巧,可以使用 gcr.azk8s.cn/kaniko-project/executor:debug 避免被墙。

假设 Kaniko 编译镜像后需要推送到对应的镜像仓库,还需要配置对应仓库的权限,可以使用 Kubernetes 的 secret,对应的配置如下:

配置好 Kaniko 的容器模板后,接下来就需要配置部署容器了,实例中使用了自己打包的一个 Docker 镜像最为部署容器的镜像,以下是该镜像的 Dockerfile:

# 该镜像还可以用于在宿主机上配置 Docker in Docker 版本的 Jenkins
FROM jenkins/jenkins:centos
 
USER root
 
# https://get.helm.sh/helm-v3.1.2-linux-amd64.tar.gz
 
ENV ALICLI_VERSION aliyun-cli-linux-latest-amd64
ENV ALICLI_URL https://aliyuncli.alicdn.com/${ALICLI_VERSION}.tgz
ENV HELM_VERSION helm-v3.1.2
ENV HELM_PLATFORM linux-amd64
ENV HELM_URL https://get.helm.sh/${HELM_VERSION}-${HELM_PLATFORM}.tar.gz
ENV KUBECTL_VERSION v1.16.0
ENV KUBECTL_URL https://storage.googleapis.com/kubernetes-release/release/${KUBECTL_VERSION}/bin/linux/amd64/kubectl
 
WORKDIR /tmp
 
# 安装 Kubectl
RUN curl -LO ${KUBECTL_URL} && \
    chmod +x ./kubectl && \
    mv ./kubectl /usr/bin/kubectl
 
# 安装 Helm
RUN curl -O ${HELM_URL} && \
    tar -xzvf ${HELM_VERSION}-${HELM_PLATFORM}.tar.gz && \
    chmod +x ./${HELM_PLATFORM}/helm && \
    mv ./${HELM_PLATFORM}/helm /usr/bin/helm && \
    rm -rf ${HELM_VERSION}-${HELM_PLATFORM}.tar.gz && rm -rf ./${HELM_PLATFORM}
 
# 安装 Aliyun-Cli
RUN curl -O ${ALICLI_URL} && \
    tar -xzvf ${ALICLI_VERSION}.tgz && \
    chmod +x ./aliyun && mv ./aliyun /usr/bin/aliyun && \
    rm -rf ${ALICLI_VERSION}.tgz
 
COPY ./config /home/.kube/config
ENV KUBECONFIG /home/.kube/config
 
USER jenkins

Jenkins 中部署容器配置如下:

配置好相应容器的模板后,还有较多的自定义参数可供配置,例如容器的资源限制,构建 Pod 留存策略和留存时间等,可以按需进行配置:

流水线配置

流水线的基本配置可以参考相关资料,此处给出供参考的 Jenkinsfile:

#!/bin/env groovy
 
pipeline {
    agent {
        node {
            label 'devops-jenkins-build-agent' //
        }
    }
 
    stages {
        stage('初始化') {
            steps {
                echo '初始化'
                sh "git branch: 'test', credentialsId: '', url: 'https://github.com/xxx/jenkins-demo.git'"
            }
        }
        stage('构建') {
            environment {
                VERSION=sh(returnStdout: true, script:"sh scripts/get-version.sh").trim()
                IMAGE_REPOSITORY=sh(returnStdout: true, script:"sh scripts/get-backend-image-url.sh").trim()
            }
            steps {
                container(name: "kaniko", shell: '/busybox/sh') {
                    sh "/kaniko/executor -f ./Dockerfile -c `pwd` --destination=${IMAGE_REPOSITORY}:${VERSION} --skip-tls-verify"
                }
            }
        }
        stage('部署') {
            steps {
                container('kubectl') {
                    sh "kubectl apply -f ./test.yaml"
                }
            }
        }
    }
    post {
        always {
            echo '构建结束!!!!!'
        }
        success {
            echo '恭喜您,构建成功!!!'
            sh '''
            AUTHOR="`git log -1 --pretty=format:'%an'`"
            PROJECT="test"
            ENV="`sh scripts/get-env.sh`"
            MODULE="backend"
            VERSION="`bash scripts/get-version.sh`"
            RAW_BODY="`git log -1 --pretty=format:'%B'`"
            STATUS="恭喜您,发布成功!!!"
            sh scripts/notice-dinding.sh "$AUTHOR" "$PROJECT" "$ENV" "$MODULE" "$VERSION" "$RAW_BODY" "$STATUS"
            '''
        }
        failure {
            sh '''
            AUTHOR="`git log -1 --pretty=format:'%an'`"
            PROJECT="test"
            ENV="`sh scripts/get-env.sh`"
            MODULE="backend"
            VERSION="`bash scripts/get-version.sh`"
            RAW_BODY="`git log -1 --pretty=format:'%B'`"
            STATUS="抱歉,发布失败!!!"
            sh scripts/notice-dinding.sh "$AUTHOR" "$PROJECT" "$ENV" "$MODULE" "$VERSION" "$RAW_BODY" "$STATUS"
            '''
        }
        unstable {
            echo '该任务已经被标记为不稳定任务!!!'
        }
        cleanup {
            echo '清理环境!!!'
            sh "rm -rf ./*"
        }
    }
}