目录

    1. Jenkins 的工作模式

    Jenkins 是一个单 Master,多 Slave 架构。Master 负责分配任务、管理服务。 Slave 负责执行具体任务。

    即使部署了多个 Master,这些 Master 之间依然相互独立,无法协同调度。在高可用的 Jenkins 方案中,需要借助外部的任务分发框架,协调多 Master 之间的调度,比如,gearman。

    在每个 Master 节点上,安装 gearman 插件,连接到 gearman server。在 gearman server 的统一分发下,各个 Master 才能构成高可用的 Jenkins 应用。

    那么,Master 和 Slave 之间是如何通信的呢?主要有两种方式:ssh,jnlp。

    • ssh 模式

    将 Master 的 SSH 公钥配置到所有的 Slave 上。当有任务调度时,由 Master 使用 ssh 客户端进行远程通信,启动 Agent 执行任务。

    在 【系统管理】->【节点管理】->【新建节点】页面中,启动方式选择 【Launch agent agents via SSH】,填入主机相关信息,保存即可。

    • jnlp 模式

    在 Slave 上需要常驻一个守护进程 jnlp ,使用 HTTP 协议与 Master 进行通信。每个 jnlp 都需要配置一个单独的 secret。

    在 【系统管理】->【节点管理】->【新建节点】页面中,启动方式选择 【通过Jave Web启动代理】。保存之后,点击查看代理,可以看到启动节点命令:

    java -jar agent.jar -jnlpUrl http://dev.chenshaowen.com:8080/computer/jnlp%E6%A8%A1%E5%BC%8F/slave-agent.jnlp -secret 8c7f2a83ea2f29ab9e5427d137c324b8102dfab18f8750229e36e34839a9e9c8 -workDir "/data"
    

    2. 安装 Kuernetes 和 Jenkins

    安装不是本篇主要内容,这里主要提供相关文档或脚本,以保证内容连贯。

    2.1 Kubernetes

    提供两种安装方式:

    • KubeSpray

    多个 IP 使用逗号分隔,使用 root 权限执行即可。

    bash <(curl -s -L https://raw.githubusercontent.com/shaowenchen/scripts/master/kubesphere/kubespray.sh) helm_enabled=true metrics_server_enabled=true local_path_provisioner_enabled=true IP=192.168.12.5 K8S_VERSION=1.15.5 PASSWORD=Qcloud@123
    
    • Kubeadm

    参考文档:使用 Kubeadm 安装 Kubernetes 集群

    2.2 Jenkins

    Jenkins 非常友好地提供了一个完整 war 包,有三种方式部署 Jenkins。

    • 直接在 Java 环境,运行 war 包
    • 使用 Docker Compose 运行,参考链接
    • 在 Kubernetes 中部署 Jenkins,使用 yaml 文件或者 helm 包

    这里,我使用的是 Docker Compose 的方式运行 Jenkins。其他方式类似,注意开放相关访问端口即可。

    3. 在 Kubernetes 动态创建 Slave

    3.1 创建 ServiceAccount

    为了使 Jenkins 有权限访问 Kubernetes 集群,这里我们需要创建一个 ServiceAccount。

    在 Kubernetes 集群主机上,创建文件 jenkins-rbac.yml。为了省略创建命名空间的步骤,这里 namespace 使用 default。

    apiVersion: v1
    kind: ServiceAccount
    metadata:
      name: jenkins
      namespace: default
    
    ---
    apiVersion: rbac.authorization.k8s.io/v1beta1
    kind: RoleBinding
    metadata:
      name: jenkins-rolebinding
      namespace: default
    roleRef:
      apiGroup: rbac.authorization.k8s.io
      kind: ClusterRole
      name: admin
    subjects:
      - kind: ServiceAccount
        name: jenkins
        namespace: default
    

    创建 ServiceAccount

    kubectl apply -f jenkins-rbac.yml
    

    获取访问 token

    kubectl describe  secret
    

    忽略 default-token-xxx,获取 jenkins-token-xxx 中的 token 值:

    Name:         jenkins-token-6vn2v
    Namespace:    default
    Labels:       <none>
    Annotations:  kubernetes.io/service-account.name: jenkins
                  kubernetes.io/service-account.uid: e1c181ae-ac1a-4ca8-b696-c06087b8c87c
    
    Type:  kubernetes.io/service-account-token
    
    Data
    ====
    token:[这里的值,需要配置在 Jenkins 中]
    ca.crt:     1025 bytes
    namespace:  7 bytes
    

    3.2 安装插件 Kubernetes Plugin

    在【系统管理】->【插件管理】->【可选插件】中,搜索 Kubernetes ,找到插件 Kubernetes plugin,安装并重启 Jenkins 即可。

    下图中已经安装插件,如果待安装,请选择【可选插件】Tab。

    3.3 插件配置

    在【系统管理】->【系统配置】中,滚动到页面底部,找到 cloud,新增加一个云。

    Kubernetes 地址指的是 Apiserver 地址,Jenkins 地址指的是 Jenkins 页面的访问地址,Jenkins 通道指的是 jnlp 与 Jenkins 通信的地址,默认是 Master 的 50000 端口地址。

    在【凭据】中添加 Secret text 类型的凭据,内容填写上面获取的 token 值。

    凭证选择【jenkins-token】后,点击【连接测试】,提示 Connection test successful 则表示配置成功。最终配置参数如下图:

    3.4 创建任务

    创建流水线类型的任务,将以下脚本粘贴在流水线的编辑框内,保存执行。

    • 使用 podTemplate 语法,进行构建
    podTemplate(containers: [
        containerTemplate(name: 'maven', image: 'maven:3.3.9-jdk-8-alpine', ttyEnabled: true, command: 'cat'),
        containerTemplate(name: 'golang', image: 'golang:1.8.0', ttyEnabled: true, command: 'cat')
      ]) {
    
        node(POD_LABEL) {
            stage('Maven project') {
                container('maven') {
                    stage('Maven test') {
                        sh 'mvn -version'
                    }
                }
            }
            stage('Golang project') {
                container('golang') {
                    stage('Go test') {
                        sh 'go version'
                    }
                }
            }
    
        }
    }
    

    执行日志:

    • 使用声明式语法,直接提供 Yaml 内容。也可以提供文件路径,指向 Yaml 文件。
    pipeline {
      agent {
        kubernetes {
          yaml """
    apiVersion: v1
    kind: Pod
    metadata:
      labels:
        mylabel: myvalue
    spec:
      containers:
      - name: maven
        image: maven:3.3.9-jdk-8-alpine
        command:
        - cat
        tty: true
      - name: go
        image: golang:1.8.0
        command:
        - cat
        tty: true
    """
        }
      }
      stages {
        stage('Run maven') {
          steps {
            container('maven') {
              sh 'mvn -version'
            }
            container('go') {
              sh 'go version'
            }
          }
        }
      }
    }
    

    执行日志:

    4. 使用模板创建 Slave

    Kubernetes Plugin 提供的 podTemplate 十分的灵活,能够为 Jenkins 提供各种各样的工作节点。

    但是,如果每个流水线都需要这样配置模板,反而会变得十分繁琐。在 Kubernetes Plugin 中提供了内置的 podTemplate,当流水线配置的 agent label 与之匹配时,就可以直接用于 Pod 创建。

    4.1 配置 PodTemplate

    在【系统管理】->【系统配置】中,滚动到页面底部,找到 Pod Template,增加一个模板,填入相关信息。

    这里需要特别注意的是标签列表值。在流水线中,可以通过标签列表选中当前 Pod Template。

    如上图,如果仅增加 Python 容器,在流水线执行时,会一直等待调度。

    Started by user admin
    Running in Durability level: MAX_SURVIVABILITY
    [Pipeline] Start of Pipeline
    [Pipeline] node
    Still waiting to schedule task
    ‘test-nqppr’ is offline
    

    查看 Jenkins 的系统全局日志,可以发现:

    Failed to send back a reply to the request hudson.remoting.Request$2@693aae9b: hudson.remoting.ChannelClosedException: Channel "hudson.remoting.Channel@376b1008:JNLP4-connect connection from 172.17.0.1/172.17.0.1:56788": channel is already closed
    

    意思是 Pod 中没有 jnlp 与之通信。因此,除了运行时容器,还需要添加一个名为 jnlp 的容器用于与 Jenkins 通信。如下图:

    最终的效果是,一个 Pod 模板,包含一个或多个指定的运行时容器,加一个 jnlp 容器。

    如果没有填写 jnlp 容器,会自动创建 jnlp 容器,但有时会报错无法连接(可能是 Jenkins 的 Bug),所以建议显示指定一个 jnlp 容器。同时,jnlp 是默认容器,也就是没有被 container 包裹的运行环境都是 jnlp。为了方便,另外一种方案,就是定制 jnlp 。

    【非必须】如果需要 Pod 中的容器能够共享宿主机上的 Docker,也就是 docker in docker,可以在【卷】中添加一个 【Host Path Volume】,将所主机上的 /var/run/docker.sock 挂载到容器的 /var/run/docker.sock 上,如下图。

    4.2 创建模板任务

    • 创建一个流水线任务
    pipeline {
      agent {
        node {
          label 'python'
        }
      }
      stages {
        stage('Run Test') {
          steps {
            container('python') {
              sh 'python --version'
            }
          }
        }
      }
    }
    

    执行日志:

    • 创建一个自由风格任务

    构建中,选择【执行 shell】,填入如下内容:

    uname -a
    python  --version
    

    执行日志:

    可以看到 python --version 执行失败了。这是因为默认容器为 jnlp,而 jenkins/jnlp-slave:3.35-5-alpine 镜像中没有打包 python。这也意味着,这里仅能执行 jnlp 提供的命令。

    5. 参考