# Kubernetes1.15.2使用jenkins 动态 slave

[上一篇文章](https://link.juejin.im/?target=https%3A%2F%2Fwww.ntpstat.com%2Fpost%2Fjenkins-install-on-k8s%2F)中已经安装好 jenkins，这次我们来实现动态 slave。

当我们使用动态 slave 时，每启动一个 job 都会创建一个 pod，其中这个 pod 的主容器自然就是 jenkins slave。有主容器自然就有其他容器，什么情况下会出现其他容器呢？

比如我得启动一个 git 容器拉代码，拉下来代码之后需要使用 maven 进行构建，这时就需要启动 maven 容器了。等你构建完了之后还得打成镜像吧，还得上传吧，所以还得需要 docker 容器。

所以这样一来一个 pod 中就有四个容器了，有意思的是，当你拉代码之后，之后的容器都会将代码目录挂载进去，并且默认就在这个目录下面工作。这样一来就十分方便了，多个容器可以如同一个容器般的操作。

但是你无法挂载目录进镜像中，也就是说你打好的 war 包带不出来，你只能打成镜像或者上传到 ftp 等。

说了这么多，相信你对其已经有了大致的了解，那让我们直接开整。

### 创建云

登录 jenkins 之后，首先需要安装 Kubernetes 这个插件，安装完成之后，我们点击 Manage Jenkins -> Configure System -> Cloud -> add -> Kubernetes。

开始新建一个云：

* `Name`：kubernetes，这个名字随意；
* `Kubernetes URL`：`https://kubernetes.default`；
* `Disable https certificate check`：勾选；
* `Credentials`：Add -> Jenkins
  * `Domain`：Global credentials；
  * `Kind`：Kubernetes Service Account；
  * `Scope`：System；
* `Jenkins URL`：如果你启动的容器能够解析你的 jenkins 域名，那么就写域名（可以是 http），否则就写 service。我这里使用 `http://jenkins`，感觉直接写 service 更好；
* `Jenkins tunnel`：和上面一样的道理，我这里使用 `jenkins:50000`；
* `Connection Timeout`：0；
* `Read Timeout`：0；
* `Concurrency Limit`：10，指定并发，应该是同时创建的 slave 的数量。

只需要配置这些，其他默认就好。

### 创建 job

接着创建第一个 job，一个名为 test 的 pipeline 任务。我们直接来到 Pipeline 这里，你这可以直接将 pipeline 脚本贴在 Script 中，也可以使用 `Pipeline script from SCM`，将 jenkinsfile 放在 git/svn 上。

我这里选择将 jenkinsfile 放在 git 上，这样的好处是每次你对 jenkinsfile 的修改都会记录下来。

* Definition：Pipeline script from SCM；
* SCM：Git；
* Repositories：
  * Repository URL：我觉得可以使用 http 地址，这样可以输入用户名和密码；
  * Credentials：创建一个 Username with password 类型的 credentials，然后输入你 gitlab 的用户名和密码即可；
  * Branches to build：应该都是 master 吧；
* Script Path：脚本的名称，你 jenkinsfile 名字是什么这里就写什么。

### jenkinsfile

以下是 jenkinsfile 的内容：

```
def label = "test-${UUID.randomUUID().toString()}"

podTemplate(label: label, yaml: """
apiVersion: v1
kind: Pod
metadata:
  labels:
    jenkins: slave
spec:
  imagePullSecrets:
  - name: nexus-pull
  containers:
  - name: jnlp
    image: registry.ntpstat.com:2222/jenkins/jnlp-slave:3.10-1-alpine
    args:
    - \$(JENKINS_SECRET)
    - \$(JENKINS_NAME)
"""
) {
    node(label) {
        stage('test') {
            echo "test"
        }
    }
}
复制代码
```

说明：

* label 就是 slave 的名字，使用一堆随机字符避免重复；
* podTemplate：关键字，用来定义 pod 中所有需要的镜像；
* yaml：通过 yaml 关键字可以直接使用 kubernetes 语法；
* node：通过它来选择 pod，表示下面的 stage 都在 pod 中执行。

由于是从私有镜像仓库中拉镜像，所以这里我定义了一个 `imagePullSecrets`，相信看了我前面文章的同学都懂。动态 slave 情况下，slave 节点必须要启动，哪怕你觉得它啥也没干，所以这里定义了 slave 这一个容器。

必须传递给 slave 容器两个参数，因为 slave 容器在启动之后必须连接 master，而这两个参数是由 kubernetes 插件自动注入的。事实上，插件会传递多个参数，只不过其他的我们用不上而已。

因为只是测试，所以这里就简单的输出 test，看看是否能够直接输出。

保存之后回到主界面之后，直接点击构建 test 任务，过会就能够看到在左侧出现一个新的 slave，一开始是离线状态。

![](https://user-gold-cdn.xitu.io/2019/3/27/169bf3095d01d3cb?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

等会它就消失了，因为构建结束。我们可以直接点击进入任务中查看。

![](https://user-gold-cdn.xitu.io/2019/3/27/169bf30d87118bc1?imageView2/0/w/1280/h/960/format/webp/ignore-error/1)

可以看出编译成功。

### 使用 maven

测试成功之后，我们直接实战。我会首先使用 git 镜像拉取项目代码，然后使用 maven 构建。由于 maven 镜像配置文件是默认的，所以我会使用 configMap 创建一个我线上实现的配置文件，挂载到 Maven 镜像中覆盖其配置文件。

maven 编译过程中会从 nexus 下载依赖包，虽然配置文件中指定了 nexus 的地址，但是如果每次构建下载的依赖包都放在容器中，那么此次构建完毕之后这些下载的依赖包就会被清掉，无法二次利用。所以我们将 nfs 的一个目录直接映射到 maven 镜像的 `/root/.m2` 目录下。

由于此时用来进行构建实战的项目有些复杂，它会生成五个包，因此这里会有循环的操作。循环生成五个镜像，并发布到私有仓库。

在之后的发布中，我会启动 docker 镜像，但是会将本地的 docker 域套接字映射进去，这样 docker 镜像仓库的操作和宿主机就没什么区别了。

```
def label = "maven-${UUID.randomUUID().toString()}"
def warDir = "warDir"
def dirPrefix = "hehe_"
def targetDir = "/target"

podTemplate(label: label, yaml: """
apiVersion: v1
kind: Pod
metadata:
  labels:
    jenkins: slave
spec:
  imagePullSecrets:
  - name: nexus-pull
  containers:
  - name: jnlp
    image: registry.ntpstat.com:2222/jenkins/jnlp-slave:3.10-1-alpine
    args:
    - \$(JENKINS_SECRET)
    - \$(JENKINS_NAME)
  - name: git
    image: registry.ntpstat.com:2222/alpine/git
    command:
    - cat
    tty: true
  - name: maven
    image: registry.ntpstat.com:2222/maven:3-alpine
    command:
    - cat
    tty: true
    volumeMounts:
    - name: maven-xml
      mountPath: /usr/share/maven/conf/settings.xml
      readOnly: true
      subPath: settings.xml
    - name: maven-data
      mountPath: /root/.m2
  - name: docker
    image: registry.ntpstat.com/library/docker:18.06
    command:
    - cat
    tty: true
    volumeMounts:
    - name: docker-socket
      mountPath: /var/run/docker.sock
      readOnly: true
  volumes:
  - name: maven-xml
    configMap:
      name: maven-conf
  - name: maven-data
    nfs:
      server: registry.ntpstat.com
      path: /opt/kubernetes/maven-data
  - name: docker-socket
    hostPath:
      path: /var/run/docker.sock
"""
) {
  wars = ['fuckGod-sync', 'fuckGod-job', 'fuckGod-mq', 'fuckGod-main', 'fuckGod-web']
  node(label) {
    stage('拉代码') {
      checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: '19f20e6e-910e-42cd-b395-e4f82c76fd89', url: 'http://git.ntpstat.com/hehe/hehe.git']]])
      container('git') {
        sh "cp Dockerfile /"
        version = sh(script: "git tag | tail -1", returnStdout: true).trim().split('-')[1]
        sh "git checkout version-${version}"
        sh "cp /Dockerfile ."
      }
    }
    stage('maven 编译') {
      container('maven') {
        sh 'mvn clean org.jacoco:jacoco-maven-plugin:prepare-agent package -Dmaven.test.failure.ignore=true'
        wars.each { item ->
          dir(dirPrefix + item + targetDir) {
            sh "mkdir ${warDir}"
            sh "unzip *.war -d ${warDir}"
          }
        }
      }
    }
    stage('打镜像') {
      container('docker') {
        withDockerRegistry(credentialsId: 'nexus-pull', url: 'https://registry.ntpstat.com') {
          wars.each { item ->
            dir(dirPrefix + item + targetDir) {
              imageName = "registry.ntpstat.com/tomcat/${item}:${version}"
              sh "cp ../../Dockerfile ."
              sh "docker build -t ${imageName} ."
              sh "docker push ${imageName}"
            }
          }
        }
      }
    }
  }
}
复制代码
```

从 gitlab/svn 上面拉代码的语法可以使用 Pipeline Syntax，选择 checkout 自动生成。

为了方便使用，我将 dockerfile 放在了项目的根目录。

我们这里的 git 在上线前会打个 tag，因此我在拉代码之后会切换到这个 tag 进行编译。
