[TOC]

0x00 前言简述

看过我前面Jenkins学习之路的朋友肯定知道,在Kubenetes中使用Jenkins可以进行动态生成分布式jnlp-slave的agent节点,相比于传统的agent节点来说极大的节约资源。

但是某一天开发反馈某个项目CICD无法正常进行,作为运维人员(搬砖)立马进入K8S集群,排除安装在集群中的Jenkins的异常问题,排查可知由于K8S集群证书到期导致动态jenkins agent节点无法正常被创建,此时项目又比较紧急。

遂利用装有Docker的机器,立即使用自行构建的jenkins-jnlp-agent镜像,并在 jenkins 中创建一个固定节点,通过docker运行该镜像并连接到Jenkins,在流水线项目绑定到该节点执行,通过几分钟的时间就快速解决了jenkins agent问题,在项目完成持续集成和交互后,趁着间隙更新Jenkins上配置连接kubernetes apiserver的证书,恢复了动态生成agent节点。

所以,为了便于自己总结知识,以及有相同需求的看友,遂将jenkins外部agent节点接入以及自行构建jnlp-agent镜像流程进行实践,希望能帮助到大家。

如果此篇文章对你有用,请您也转发、点赞、在看、给周边的朋友吧!

温馨提示: 若需要企业 Jenkins Pipeline 流水线脚本的朋友,可以关注【WeiyiGeek】后回复【Jenkins流水线代码】即可下载 Jenkins-Pipeline.groovy 示例文件。

WeiyiGeek.Jenkins流水线代码图

WeiyiGeek.Jenkins流水线代码图


0x01 基础环境准备

1.环境说明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# 1.K8s 集群中安装的 Jenkins 
~$ kubectl get pod,svc -n devops -l app=jenkins
NAME READY STATUS RESTARTS AGE
pod/jenkins-7fc6f4fcf6-glqxj 1/1 Running 0 28h

NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jenkins NodePort 10.109.163.223 <none> 8080:30001/TCP,50000:30634/TCP 382d

# 2.Jenkins 安装版本
Jenkins 2.330

# 3.地址说明
Jenkins 面板: http://192.168.12.107:30001/
TCP port for inbound agents(Agent 端口): http://192.168.12.107:30634/ ( 内部为 50000,若是在集群中使用则需要将nodePort端口改成,可通过转发的形式 因为K8S默认nodePort范围在 30000-32767 之中)

温馨提示: 我们需要修改 Jenkins Agent 端口 可以访问 Dashboard 全局安全配置 -> 代理 (TCP port for inbound agents) 指定端口设置为 30634,注意相对应Pod暴露的端口也要一同修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ kubectl edit deployments.apps -n devops jenkins
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 30634 # 修改点
name: agent
protocol: TCP

$ kubectl edit svc -n devops jenkins
- name: agent
nodePort: 30634
port: 30634 # 修改点与 containerPort 要一致。
protocol: TCP:q
targetPort: agent

# 验证查看 Pod 与 服务是否正常
$ kubectl get svc,pod -n devops -l app=jenkins
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) AGE
service/jenkins NodePort 10.109.163.223 <none> 8080:30001/TCP,30634:30634/TCP 382d

NAME READY STATUS RESTARTS AGE
pod/jenkins-856bc9b47f-4t7k5 1/1 Running 0 2m9s
WeiyiGeek.Jenkins-inbound agents端口修改图

WeiyiGeek.Jenkins-inbound agents端口修改图

或者采用如下临时解决办法(下面实践1中采用的是此种方法,正式环境下还是建议使用上面的方法稳定,但是需要更改和注意的事项更多了)

1
2
3
4
5
6
7
$ kubectl -n devops port-forward --address 127.0.0.1,192.168.12.107 jenkins-7fc6f4fcf6-glqxj 50000:50000
# Forwarding from 127.0.0.1:50000 -> 50000
# Forwarding from 192.168.12.107:50000 -> 50000
# Handling connection for 50000
# Handling connection for 50000

# 或者在 jenkins 控制器资源清单中设置 hostNetwork: true

温馨提示: 若看友不了解Jenkins持续集成的,或者需要安装实践的,可以参考博主学习【Jenkins学习之路汇总】汇总,关注 WeiyiGeek 公众号回复【jenkins学习之路】即可获得学习资料:


2.添加新的Jenkins agent节点

操作步骤如下: Dashboard 系统管理 -> 节点管理 -> 新建节点 -> 按照提示输入如下信息 -> 最后点击保存

1
2
3
4
5
6
7
8
9
10
11
12
13
# 基础配置
节点名字: docker-jenkins-jnlp
Number of executors (最大执行数): 1~5
远程工作目录: /home/jenkins/agent
标签: docker-jnlp-1
启动方式: 通过 Java Web 启动代理
可用性: 尽量保持代理在线

# 节点属性
- 工具位置: 请按照实际情况填写。
# 例如, Docker 配置,名称 Docker ,目录 /var/run/docker.sock
- 环境变量:
# 例如,键值对列表,键 name 值 weiyigeek
WeiyiGeek.新建Docker-agent节点图

WeiyiGeek.新建Docker-agent节点图

温馨提示: 在点击保存后,我们可以点击此节点,看到其提示节点连接Jenkins的方式如下:

1
2
3
4
5
6
7
8
9
# 方式1
java -jar agent.jar -jnlpUrl http://192.168.12.107:30001/computer/docker%2Djenkins%2Djnlp/jenkins-agent.jnlp -secret b97b9d1e0cf083f9da5721caa6ebc63f6fe648375bd90cb2c2f484681d887bb7 -workDir "/home/jenkins/agent"

java -Xms512m -Xmx1g -Xss1m -jar /usr/local/bin/agent.jar -jnlpUrl http://192.168.12.107:30001/computer/jenkinsAgentWork1/jenkins-agent.jnlp -secret 2a789bbbd0193ef576e7b62eb2d205d1d024d0ea7b14f6f79f1cccdd6fb1ed20 -workDir "/home/jenkins/agent"

# 方式2
# Run from agent command line, with the secret stored in a file:
echo b97b9d1e0cf083f9da5721caa6ebc63f6fe648375bd90cb2c2f484681d887bb7 > secret-file
java -jar agent.jar -jnlpUrl http://192.168.12.107:30001/computer/docker%2Djenkins%2Djnlp/jenkins-agent.jnlp -secret @secret-file -workDir "/home/jenkins/agent"

WeiyiGeek.节点连接Jenkins的方式图

WeiyiGeek.节点连接Jenkins的方式图


3.自行构建Jenkins Agent镜像(干货)

描述: 在WeiyiGeek微信公众号回复20230206关键字,即可获取最新Jenkins-jnlp镜像构建的Dockerfile及下述相关脚本文件,此处我们自定义的jnlp容器镜像主要实现功能如下:

  • 用户权限控制(sudo)
  • ssh 远程连接
  • git 代码版本控制
  • docker 容器管理
  • kubectl 集群管理
  • Java 运行环境
  • Maven 运行环境
  • NodeJS 环境
  • SonarQube 扫描环境
  • Gitlab-Release 上传环境
  • 中文环境支持
  • 时区更改配置
  • 自定义工作目录(/home/jenkins/agent)

温馨提示: 如下操作构建依赖于Docker环境,若你没有安装Docker环境或者不了解的Docker容器的朋友,可以参考博主学习【Docker的系列笔记】汇总,关注 WeiyiGeek 公众号回复【Docker容器学习之路汇总】即可获得学习资料:


构建目录

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~/k8s/jenkins/jnlp-slave$ tree .
.
├── agent.jar # Jenkins -> 2.330
├── agent.jar.bak
├── apache-maven-3.8.4-bin.tar.gz
├── build
│   └── Dockerfile
├── docker
├── glibc-2.32-r0.apk
├── glibc-bin-2.32-r0.apk
├── glibc-i18n-2.32-r0.apk
├── jdk-8u281-linux-x64.tar.gz
├── jenkins-agent.sh
├── kubectl
├── locale.md
├── release-cli-0.10.0-linux-amd64
├── remoting-4.11.2.jar
├── sgerrand.rsa.pub
└── sonar-scanner-cli-4.5.0.2216-linux.zip

1 directory, 16 files

温馨提示: 在Jenkins 2.330版本中添加一个新的节点, 即可获取匹配当前版本的 agent.jar (http://youjenkins-domainname-or-ip/jnlpJars/agent.jar), 或者是在 jenkins 官网 https://repo.jenkins-ci.org/public/org/jenkins-ci/main/remoting 进行下载;


自定义jnlp镜像的 Dockerfile
博主自定义jnlp镜像hub地址: https://hub.docker.com/r/weiyigeek/alpine-jenkins-jnlp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
$ cat build/Dockerfile
#----------------------------------------------------------------------#
# Title: Base in Alpine Images Create Custom Jenkins Kubernetes jnlp Images
# Author: WeiyiGeek
# WebSite: https://weiyigeek.top
# Email: mastr@weiyigeek.top
# Version: v1.12
# Image Version: alpine-3.13.1
# MainFunction:
# Install ssh-server docker git openssh tzdata curl tar sudo git ca-certificates wget unzip docker zlib nodejs npm jq
# Install JDK8 Version: 1.8.0_281
# - https://www.oracle.com/java/technologies/javase/javase-jdk8-downloads.html
# - https://github.com/sgerrand/alpine-pkg-glibc/releases/
# - https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
# Install jnlp Version: 4.11.2 (两种方式都可以下载agent)
# - https://repo.jenkins-ci.org/public/org/jenkins-ci/main/remoting/
# - http://youjenkins-domainname/jnlpJars/agent.jar
# Install Maven Version: 3.8.4
# - https://apache.osuosl.org/maven/maven-3/${MAVEN_VERSION}/binaries
# Install SonarqubeScan Version: 4.5.0
# - https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-4.5.0.2216-linux.zip
# Install Gitlab Release Version: 0.10.0
# - https://gitlab.com/gitlab-org/release-cli/-/releases
# Install kubernetes cli
# - kubectl Version: 1.23.1
# Install docker cli
# - kubectl Version: 20.10.3
# ChangeLog:
# v1.8 - 增加 docker
# v1.9 - 增加 中文环境
# v1.10 - 增加 node.js 环境支持
# v1.11 - 更新依赖软件版本及其agent.jar版本
#-------------------------------------------------#

FROM alpine:3.13.1

MAINTAINER Jenkins Custom Work Node Jnlp Container - <master@weiyigeek.top> - WeiyiGeek

ARG USERNAME=jenkins \
AGENT_WORKDIR=/home/jenkins \
BASE_DIR=/usr/local \
BASE_BIN=/usr/local/bin \
BASE_URL=http://192.168.12.107:8080 \
LOCALE=locale.md \
JDK_NAME=jdk-8u281-linux-x64 \
JDK_DIR=/usr/local/jdk1.8.0_281 \
GLIBC_NAME=glibc-2.32-r0.apk \
GLIBC_BIN_NAME=glibc-bin-2.32-r0.apk \
GLIBC_I18N_NAME=glibc-i18n-2.32-r0.apk \
MAVEN_NAME=apache-maven-3.8.4-bin \
MAVEN_DIR=/usr/local/apache-maven-3.8.4 \
SONAR_SCANNER_NAME=sonar-scanner-cli-4.5.0.2216-linux \
SONAR_SCANNER_DIR=/usr/local/sonar-scanner-4.5.0.2216-linux \
GITLAB_CLI=release-cli-0.10.0-linux-amd64

ENV LANG=en_US.UTF-8 \
LC_ALL=en_US.UTF-8 \
JAVA_HOME=/usr/local/jdk8 \
JRE_HOME=/usr/local/jdk8/jre \
MAVEN_HOME=/usr/local/maven \
MAVEN_RPEO=/home/jenkins/.m2 \
SONAR_SCANNER_HOME=/usr/local/sonar-scanner \
NODEJS_MODULES=/usr/lib/node_modules

# 用户ROOT切换
USER root

# Shell 命令 - 此种方式极大减少了构建的镜像大小;
RUN sed -i 's/dl-cdn.alpinelinux.org/mirror.tuna.tsinghua.edu.cn/g' /etc/apk/repositories \
&& apk update \
&& apk add --no-cache openssh tzdata curl tar sudo git ca-certificates wget unzip docker zlib nodejs npm jq \
&& cp /usr/share/zoneinfo/Asia/Shanghai /etc/localtime \
&& chmod 4755 /bin/busybox \
&& addgroup -g 1000 -S ${USERNAME} \
&& adduser ${USERNAME} -D -g ${USERNAME} -G root -u 1000 -s /bin/sh \
&& echo "jenkins ALL=(root) NOPASSWD:ALL" >> /etc/sudoers \
&& mkdir -p ${AGENT_WORKDIR}/.ssh ${AGENT_WORKDIR}/.m2 ${AGENT_WORKDIR}/agent\
&& wget -q -O /tmp/${GLIBC_NAME} ${BASE_URL}/${GLIBC_NAME} \
&& wget -q -O /tmp/${GLIBC_BIN_NAME} ${BASE_URL}/${GLIBC_BIN_NAME} \
&& wget -q -O /tmp/${GLIBC_I18N_NAME} ${BASE_URL}/${GLIBC_I18N_NAME} \
&& wget -q -O /etc/apk/keys/sgerrand.rsa.pub ${BASE_URL}/sgerrand.rsa.pub \
&& wget -q -O /tmp/${LOCALE} ${BASE_URL}/${LOCALE} \
&& wget -q -O /tmp/${JDK_NAME}.tar.gz ${BASE_URL}/${JDK_NAME}.tar.gz \
&& wget -q -O ${BASE_BIN}/agent.jar ${BASE_URL}/agent.jar \
&& curl -fsSL -o ${BASE_BIN}/jenkins-agent.sh ${BASE_URL}/jenkins-agent.sh \
&& curl -fsSL -o /tmp/${MAVEN_NAME}.tar.gz ${BASE_URL}/${MAVEN_NAME}.tar.gz \
&& curl -fsSL -o /tmp/${SONAR_SCANNER_NAME}.zip ${BASE_URL}/${SONAR_SCANNER_NAME}.zip \
&& curl -fsSL -o /usr/local/bin/release-cli ${BASE_URL}/${GITLAB_CLI} \
&& curl -fsSL -o /usr/local/bin/kubectl ${BASE_URL}/kubectl \
&& curl -fsSL -o /usr/local/bin/docker ${BASE_URL}/docker \
&& sed -i "s/#PermitRootLogin.*/PermitRootLogin yes/g" /etc/ssh/sshd_config \
&& sed -i "s/^#\s*StrictHostKeyChecking ask/StrictHostKeyChecking no/g" /etc/ssh/ssh_config \
&& ssh-keygen -t dsa -P "" -f /etc/ssh/ssh_host_dsa_key \
&& ssh-keygen -t rsa -P "" -f /etc/ssh/ssh_host_rsa_key \
&& ssh-keygen -t ecdsa -P "" -f /etc/ssh/ssh_host_ecdsa_key \
&& ssh-keygen -t ed25519 -P "" -f /etc/ssh/ssh_host_ed25519_key \
&& ssh-keygen -t ed25519 -P "" -C "master@weiyigeek.top" -f /home/jenkins/.ssh/id_ed25519 \
&& apk add /tmp/${GLIBC_NAME} /tmp/${GLIBC_BIN_NAME} /tmp/${GLIBC_I18N_NAME} \
&& tar -zxf /tmp/${JDK_NAME}.tar.gz -C ${BASE_DIR} \
&& mv ${JDK_DIR} ${JAVA_HOME} \
&& tar -zxf /tmp/${MAVEN_NAME}.tar.gz -C ${BASE_DIR} \
&& mv ${MAVEN_DIR} ${MAVEN_HOME} \
&& unzip /tmp/${SONAR_SCANNER_NAME}.zip -d ${BASE_DIR} \
&& mv ${SONAR_SCANNER_DIR} ${SONAR_SCANNER_HOME} \
&& npm config set registry https://registry.npm.taobao.org \
&& chmod a+x ${BASE_BIN}/* \
&& chown -R jenkins:jenkins ${BASE_DIR}/ ${AGENT_WORKDIR}/ ${NODEJS_MODULES}/ \
&& echo "root:WeiyiGeek" | chpasswd \
&& echo "jenkins:WeiyiGeek" | chpasswd \
&& cat /tmp/${LOCALE} | xargs -i /usr/glibc-compat/bin/localedef -i {} -f UTF-8 {}.UTF-8 \
&& sed -i "s#use_embedded_jre=true#use_embedded_jre=false#g" ${SONAR_SCANNER_HOME}/bin/sonar-scanner \
&& rm -rf /var/cache/apk/* /tmp/* ${SONAR_SCANNER_HOME}/jre/* \
&& cd ${JAVA_HOME} \
&& rm -rf COPYRIGHT LICENSE README release THIRDPARTYLICENSEREADME-JAVAFX.txt THIRDPARTYLICENSEREADME.txt Welcome.html javafx-src.zip src.zip \
lib/plugin.jar \
lib/ext/jfxrt.jar \
bin/javaws \
lib/javaws.jar \
lib/desktop \
plugin \
lib/deploy* \
lib/*javafx* \
lib/*jfx* \
lib/amd64/libdecora_sse.so \
lib/amd64/libprism_*.so \
lib/amd64/libfxplugins.so \
lib/amd64/libglass.so \
lib/amd64/libgstreamer-lite.so \
lib/amd64/libjavafx*.so \
lib/amd64/libjfx*.so \
&& echo "export LANG=zh_CN.UTF-8" > /etc/profile.d/locale.sh

USER jenkins

WORKDIR ${AGENT_WORKDIR}

ENV CLASSPATH=.:${JAVA_HOME}/lib/dt.jar:${JAVA_HOME}/lib/tools.jar \
PATH=${JAVA_HOME}/bin:${MAVEN_HOME}/bin:${SONAR_SCANNER_HOME}/bin:$PATH

ENTRYPOINT ["/usr/local/bin/jenkins-agent.sh"]


镜像构建操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# (0) 启动一个临时的web服务器(`存放上面的镜像构建所需软件`-非常重要-否则将会导致构建失败)
~/k8s/jenkins/jnlp-slave$ python3 -m http.server 8080
Serving HTTP on 0.0.0.0 port 8080 (http://0.0.0.0:8080/) ...
# 172.17.0.4 - - [30/Mar/2021 17:32:00] "GET /glibc-2.32-r0.apk HTTP/1.1" 200 -
...
# 172.17.0.4 - - [30/Mar/2021 17:32:00] "GET /kubectl HTTP/1.1" 200 -

# (1) 镜像构建 (建议一定要带上构建平台)
docker build -t weiyigeek/alpine-jenkins-jnlp --platform linux/amd64 .
# Sending build context to Docker daemon 25.09kB
# Step 1/10 : FROM alpine:3.13.1
# ---> e50c909a8df2
# Step 2/10 : MAINTAINER Jenkins Custom Work Node Jnlp Container - <master@weiyigeek.top> - WeiyiGeek
# ....................................................................................................
# Step 10/10 : ENTRYPOINT ["/usr/local/bin/jenkins-agent.sh"]
# ---> Running in 1869aaba6a4e
# Removing intermediate container 1869aaba6a4e
# ---> 0dfabb8ae0ee
# Successfully built 0dfabb8ae0ee
# Successfully tagged weiyigeek/alpine-jenkins-jnlp:latest


# (2) 查看构建信息
docker images weiyigeek/alpine-jenkins-jnlp --all
# REPOSITORY TAG IMAGE ID CREATED SIZE
# harbor.weiyigeek.top/devops/jenkins-jnlp 3.13.1-alpine b47b6581b712 About an hour ago 715MB

# (3) 推送镜像到Docker HUB中
docker push weiyigeek/alpine-jenkins-jnlp
# The push refers to repository [harbor.weiyigeek.top/devops/jenkins-jnlp]
# 059ea3fbd3b3: Pushed
# 1119ff37d4a9: Layer already exists
# 3.13.1-alpine: digest: sha256:1f869c553340c9399c7db9072169600a17ddb0ec41d41d3a4365b8c1571fc201 size: 741

# (4) 完毕后便可采用Ansible各节点拉取镜像(若没有安装ansible的朋友请自行ssh登录)
ansible node -m shell -a "docker pull weiyigeek/alpine-jenkins-jnlp"

至此,Jenkins Agent 镜像构建完毕!


0x02 项目实践

1.使用在K8S部署的Jenkins连接Docker容器中运行的Agent分布式节点 (缺省端口:50000)

描述: 从前面环境可知,我将Jenkins安装在Kubernetes集群之中了,并且将Pod中的8080端口映射到30001端口,而agent缺省端口为50000,在没有更改的SVC情况下,我们可以使用两种方式一种就是使用kubectl port-forward转发,另外一种是在Jenkins资源清单中配置 hostNetwork: true, 此时运行jenkins Pod的节点IP+50000端口便可直接访问。

步骤 01.针对 Jenkins 进行 inbound agents pod 50000 端口转发到192.168.12.107节点的50000端口

1
$ kubectl -n devops port-forward --address 127.0.0.1,192.168.12.107 jenkins-7fc6f4fcf6-glqxj 50000:50000


步骤 02.然后在其他有安装Docker的机器节点上运行如下命令进行jenkins jnlp容器的创建,注意此处为了多个agent构建时maven包的数据共享,使用nfs共享磁盘.

1
2
3
4
5
6
7
8
9
10
docker run --user jenkins -d --name jnlp \
-e "JAVA_OPTS=-Xms512m -Xmx1g -Xss1m" \
-e "JENKINS_NAME=docker-jenkins-jnlp" \
-e "JENKINS_AGENT_NAME=docker-jenkins-jnlp" \
-e "JENKINS_SECRET=b97b9d1e0cf083f9da5721caa6ebc63f6fe648375bd90cb2c2f484681d887bb7" \
-e "JENKINS_AGENT_WORKDIR=/home/jenkins" \
-e "JENKINS_PURL=http://192.168.12.107:30001" \
-w /home/jenkins \
-v /nfsdisk-31/appstorage/mavenRepo:/home/jenkins/.m2 \
-v /var/run/docker.sock:/var/run/docker.sock weiyigeek/alpine-jenkins-jnlp:latest

命令执行若出现INFO: Connected结果,则表示连接成功。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
Java version: 1.8.0_281, vendor: Oracle Corporation, runtime: /usr/local/jdk8/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "5.4.0-137-generic", arch: "amd64", family: "unix"
INFO: Scanner configuration file: /usr/local/sonar-scanner/conf/sonar-scanner.properties
INFO: Project root configuration file: NONE
INFO: SonarScanner 4.5.0.2216
INFO: Java 1.8.0_281 Oracle Corporation (64-bit)
INFO: Linux 5.4.0-137-generic amd64
Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main createEngine
INFO: Setting up agent: docker-jenkins-jnlp
Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener <init>
INFO: Jenkins agent is running in headless mode.
Feb 04, 2023 10:51:19 PM hudson.remoting.Engine startEngine
INFO: Using Remoting version: 4.11.2
Feb 04, 2023 10:51:19 PM org.jenkinsci.remoting.engine.WorkDirManager initializeWorkDir
INFO: Using /home/jenkins/remoting as a remoting work directory
Feb 04, 2023 10:51:19 PM org.jenkinsci.remoting.engine.WorkDirManager setupLogging
INFO: Both error and output logs will be printed to /home/jenkins/remoting
Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Locating server among []
Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener status
INFO: Agent discovery successful
Agent address: 192.168.12.107
Agent port: 50000
Identity: 8e:c7:1e:e1:39:ee:f4:2a:43:f6:aa:d9:0e:b7:b6:62
.....
INFO: Connected

WeiyiGeek.在Docker中运行jenkins-jnlp结果图

WeiyiGeek.在Docker中运行jenkins-jnlp结果图


步骤 03.当然你也可以通过点击 Jenkins Dashboard -> 节点列表 -> docker-jenkins-jnlp 查看创建的节点已经是运行状态了。

WeiyiGeek.jenkins中jnlp节点agent状态图

WeiyiGeek.jenkins中jnlp节点agent状态图

温馨提示:此处只是涉及jenkins agent节点的配置连接,若需要进行调用执行某一个项目的CICD,请继续往下看。


2.使用在K8S部署的Jenkins连接Docker容器中运行的自定义Agent端口节点 (直连方式)

描述: 由于我们更改Jenkins中的agent TCP 端口以及在K8S集群中也作出了相应更改,但是连接时需要指定更多的参数,例如(JENKINS_DIRECT_CONNECTION与JENKINS_INSTANCE_IDENTITY),这里面的坑还是很大的,为啥说坑大请看友继续看。

JENKINS_DIRECT_CONNECTION : Connect directly to this TCP agent port, skipping the HTTP(S) connection parameter download. Value: “:
JENKINS_INSTANCE_IDENTITY : The base64 encoded InstanceIdentity byte array of the Jenkins master. When this is set,the agent skips connecting to an HTTP(S) port for connection info.

步骤 00.前置准备,通过前面所需参数可知,我们需要指定要直连的jenkins agent服务的IP以及端口 (例如: 192.168.12.107:30634),和 jenkins Instance Identity值(此处值得注意)。

最开始,JENKINS_INSTANCE_IDENTITY 其值我认为是 Jenkins 家目录中 secrets 下的 master.key 的 base64 编码,但是怎么连接都是不正确的,随后没得办法只能查询官方文档,功夫不负有心人,在 jenkinsci 的 remoting 项目下找到了一个issue,然后顺腾摸瓜找到了该值。

Issue: https://github.com/jenkinsci/remoting/pull/338/files/07d7fd668b17123afd9ef7ac7a859bdda151f5cf#diff-8f973eec7a7bc04d532ba6b42015c903d7100b09f8ed655367d0fe893a9b6d0a
Document: https://wiki.jenkins.io/display/JENKINS/Instance+Identity

WeiyiGeek.JENKINS-53461 Direct inbound TCP agent connection 图

WeiyiGeek.JENKINS-53461 Direct inbound TCP agent connection 图

1
2
3
4
5
6
7
Jenkins : Instance Identity
Created by Unknown User (kohsuke), last modified by Unknown User (songy) on Mar 26, 2015
Each Jenkins instance maintains an RSA private/public key pair that can be used to uniquely identify Jenkins. This information is called "instance identity".

From outside, the public key can be obtained by sending the GET request to the top page of Jenkins, and look for the "X-Instance-Identity" header in the response. This header is always available, even if the response is 401 access denied (which can happen if Jenkins is protected via security.) The value represents a base64-encoded ASN.1 DER serialization of X.509 SubjectPublicKeyInfo record.

Plugins that run inside Jenkins can access this key pair programmatically through the org.jenkinsci.main.modules.instance_identity.InstanceIdentity class (add a provided scope dependency to this module into your plugin)

上述大致意思是,每个Jenkins实例都维护一个RSA私钥/公钥对,可用于唯一标识Jenkins,这就是实例标识(instance identity),该值(公钥)可以通过将GET请求发送到Jenkins的首页来获得,并在响应中查找“X-Instance-Identity”标头。

1
X-Instance-Identity: MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB******************1KQ5CLlISSjBaYrGqwsDThdDRbM6CjmP3UJ0NVhsMRJuQyAA2x4XktjEAYO3X0PWQIDAQAB
WeiyiGeek.获取X-Instance-Identity图

WeiyiGeek.获取X-Instance-Identity图


步骤 01.获取到了X-Instance-Identity值后,一条Docker命令创建运行 jenkins agent 容器连接到Jenkins之中

1
2
3
4
5
6
7
8
9
10
11
docker run --user jenkins -d --name jnlp \
-e "JAVA_OPTS=-Xms512m -Xmx1g -Xss1m" \
-e "JENKINS_NAME=docker-jenkins-jnlp" \
-e "JENKINS_AGENT_NAME=docker-jenkins-jnlp" \
-e "JENKINS_SECRET=b97b9d1e0cf083f9da5721caa6ebc63f6fe648375bd90cb2c2f484681d887bb7" \
-e "JENKINS_AGENT_WORKDIR=/home/jenkins" \
-e "JENKINS_DIRECT_CONNECTION=192.168.12.107:30634" \
-e "JENKINS_INSTANCE_IDENTITY=MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsFqtbTp5/xRS85YdURKk6Zc+qfl+RxrtnJ0zUXXLqXIa8S0MVk5U+xhu0Xo6Kz9MN/i7znfKpljk4/6+GphGFJbgGVw/1M2xbZjg7XN8QiXU64rlHC1NaGbKsa6R0PbWZEjbExR+sgTreiKwh08FcQemEDEajN5WihvvC0LmopYoSfHXW0fYMYIvVPO0Kow80MKoXighfhpK9msLWV4ay4ttP9zjBWml2gEeCXNjPQAt+r1l/kDT3c7vaIybzrxgRg2K9IVYmao/wemwDdpEP1KQ5CLlISSjBaYrGqwsDThdDRbM6CjmP3UJ0NVhsMRJuQyAA2x4XktjEAYO3X0PWQIDAQAB" \
-w /home/jenkins \
-v /nfsdisk-31/appstorage/mavenRepo:/home/jenkins/.m2 \
-v /var/run/docker.sock:/var/run/docker.sock weiyigeek/alpine-jenkins-jnlp:latest


步骤 02.查看运行的容器及其结果,我们也可以在jenkins 面板上看到连接的节点,如需删除节点直接选择左边,菜单栏中删除节点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
~/k8s/jenkins/jnlp-slave$ docker ps
# CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
# efb27bd9cb8e weiyigeek/alpine-jenkins-jnlp:latest "/usr/local/bin/jenk…" 3 seconds ago Up 1 second jnlp

~/k8s/jenkins/jnlp-slave$ docker logs -f efb27bd9cb8e
# INFO: Agent discovery successful
# Agent address: 192.168.12.107
# Agent port: 30634
# Identity: 8e:c7:1e:e1:39:ee:f4:2a:43:f6:aa:d9:0e:b7:b6:62
# Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener status
# INFO: Handshaking
# Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener status
# INFO: Connecting to 192.168.12.107:30634
# Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener status
# INFO: Trying protocol: JNLP4-connect
# Feb 04, 2023 10:51:19 PM org.jenkinsci.remoting.protocol.impl.BIONetworkLayer$Reader run
# INFO: Waiting for ProtocolStack to start.
# Feb 04, 2023 10:51:19 PM hudson.remoting.jnlp.Main$CuiListener status
# INFO: Remote identity confirmed: 8e:c7:1e:e1:39:ee:f4:2a:43:f6:aa:d9:0e:b7:b6:62
# Feb 04, 2023 10:51:21 PM hudson.remoting.jnlp.Main$CuiListener status
# INFO: Connected


步骤 03.验证连接上Jenkins的agent节点,可以通过左侧的脚本命令行进行测, 你可以访问 http://你jenkinsIP地址/computer/agent节点名称/script 即可到 脚本命令行页面。

1
2
3
4
5
6
7
# Groovy script
println "uname -a".execute().text
println System.getenv("PATH")

# 执行结果:
Linux 2736a6240d91 5.4.0-137-generic #154-Ubuntu SMP Thu Jan 5 17:03:22 UTC 2023 x86_64 Linux
/usr/local/jdk8/bin:/usr/local/maven/bin:/usr/local/sonar-scanner/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin

0x03 实践调用Jenkins agent节点

1.牛刀小试

描述: 此处不再累述 Jenkins 项目的创建说明,不会的朋友请参照我的【Jenkins学习之路汇总】( https://blog.weiyigeek.top/2018/1-1-1.html#Jenkins学习之路汇总 ),帮助大家快速入门。

agent 代理语法及使用介绍
我们可以通过下面流水线中可知,其中最主要的就是 agent 代码块包含的,我们设定的 jenkins agent 标签值的机器。
描述: 指定整个Pipeline或特定阶段将在Jenkins环境中执行的位置,具体取决于该agent 部分的放置位置;

语法参数:

1
2
3
4
5
6
7
8
9
10
必须: YES
参数:any / none / label / node / docker / dockerfile / kubernetes
- 1.在任何可用的 agent 上执行Pipeline或stage
- 2.在pipeline块的顶层应用时,不会为整个 Pipeline运行分配全局代理,并且每个stage部分都需要包含自己的agent部分。
- 3.使用提供的标签在Jenkins环境中可用的代理上执行 Pipeline或阶段, 注意标签条件也可以使用。
- 4.node使用与lable类似
- 5.执行Pipeline或stage时会动态供应一个docker节点去接受Docker-based的Pipelines。
- 6.使用从Dockerfile源存储库中包含的容器构建的容器执行 Pipeline或阶段,Jenkinsfile 必须从多分支 Pipeline或 SCM Pipeline加载。
- 7.在Kubernetes集群上部署的Pod内执行 Pipeline或阶段,同样Jenkinsfile 必须从多分支 Pipeline或 SCM Pipeline加载,Pod模板在kubernetes {} 块内定义。
允许:在顶层pipeline块和每个stage块中。

语法示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
pipeline {
agent any

agent none

agent {
label 'my-label1 && my-label2'
}

agent { node { label 'labelName' } } // 等同于 agent { label 'labelName' }

// docker 还可以接受一个args直接传递给`docker run`调用以及一个 alwaysPull 选项
// registryUrl和registryCredentialsId参数 有助于指定要使用的Docker注册表及其凭据
agent {
docker {
image 'maven:3-alpine'
label 'my-defined-label'
args '-v /tmp:/tmp'
registryUrl 'https://myregistry.com/'
registryCredentialsId 'myPredefinedCredentialsInJenkins'
}
}

// dockerfile
agent {
// 等同于 to "docker build -f Dockerfile.build --build-arg version=1.0.2 ./build/
dockerfile {
filename 'Dockerfile.build'
// 如果要Dockerfile在另一个目录中构建,请使用以下dir选项
dir 'build'
label 'my-defined-label'
additionalBuildArgs '--build-arg version=1.0.2'
args '-v /tmp:/tmp'
// 同样也接受registryUrl和registryCredentialsId参数
registryUrl 'https://myregistry.com/'
registryCredentialsId 'myPredefinedCredentialsInJenkins'
}
}

// kubernetes: 例如如果要在其中装有Kaniko容器的容器
agent {
kubernetes {
label podlabel
yaml """
kind: Pod
metadata:
name: jenkins-agent
spec:
containers:
- name: kaniko
image: gcr.io/kaniko-project/executor:debug
imagePullPolicy: Always
command:
- /busybox/cat
tty: true
volumeMounts:
- name: aws-secret
mountPath: /root/.aws/
- name: docker-registry-config
mountPath: /kaniko/.docker
restartPolicy: Never
volumes:
- name: aws-secret
secret:
secretName: aws-secret
- name: docker-registry-config
configMap:
name: docker-registry-config
"""
}
}

常用选项:
描述: 下面可以应用于两个或者多个agent实现的选项即label、customWorkspace、reuseNode;

  • 1.label (参数:字符串): 运行 Pipeline或单个 Pipeline的标签或标签条件stage。 【此选项对node,docker和有效对dockerfile必需 node。】
  • 2.customWorkspace (参数: 字符串) : 运行 Pipeline或个人 stage 这 agent 是这个自定义的工作空间内的应用,而不是默认的, 它可以是相对路径(在这种情况下自定义工作空间将位于节点上的工作空间根目录下),也可以是绝对路径。【此选项是有效的node,docker和dockerfile。】
  • 3.reuseNode(参数: 布尔值-false): 如果为true在同一工作空间中在 Pipeline顶级指定的节点上运行容器,而不是在整个新节点上运行
  • 4.args (参数: 字符串): 要传递给的运行时参数docker run,此选项对docker和有效dockerfile。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
// 示例1. Docker代理,声明性 Pipeline
pipeline {
// V.在具有给定名称和标签(maven:3-alpine)的新创建容器中执行此 Pipeline中定义的所有步骤。
agent { docker 'maven:3-alpine' }
stages {
stage('Example Build') {
steps {
sh 'mvn -B clean verify'
}
}
}
}

// 例子2.阶段级代理部分
pipeline {
/* agent none在 Pipeline的顶层进行定义可确保 不会不必要地分配执行程序。使用agent none还会强制每个stage部分包含其自己的agent部分。 */
agent none
stages {
stage('Example Build') {
/* 使用此映像在新创建的容器中执行此阶段中的步骤。*/
agent { docker 'maven:3-alpine' }
steps {
echo 'Hello, Maven'
sh 'mvn --version'
}
}
stage('Example Test') {
/* 使用与上一阶段不同的图像在新创建的容器中执行此阶段中的步骤。 */
agent { docker 'openjdk:8-jre' }
steps {
echo 'Hello, JDK'
sh 'java -version'
}
}
}
}

好了,agent 介绍完毕,我们该回归正题了。


实践流程

步骤 01.此处我创建了一个 Test 项目用于测试调用我们在Docker中运行的jenkins Agent节点。

WeiyiGeek.图

WeiyiGeek.图


步骤 02.此处是我准备的 jenkins 流水线代码, 在应用保存后即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
def JOB_WORKSPACE = "${env.WORKSPACE}"
def JOB_NAME = "${env.JOB_NAME}-${env.BUILD_NUMBER}"

pipeline {
agent {
label 'docker-jnlp-1'
}
environment {
SONARQUBE_TIMEOUT = '10'
}

stages {
stage ('Start Test') {
steps {
echo "[Test Jenkins JNLP Agent with docker]!"
echo "流水线环境变量输出: ${env.JOB_NAME}-${env.JOB_WORKSPACE}-${env.SONARQUBE_TIMEOUT}"
script {
// 命令执行
sh " echo -e '<name>weiyigeek</name>\n<addr>https://weiyigeek.top</addr>' > demo.xml && cat demo.xml"
// 返回命令执行结果
USERNAME=sh label: 'name',returnStdout: true, script: """
awk '/<\\/*name\\/*>/{gsub(/[[:space:]]*<\\/*name\\/*>/,"");print \$0}' demo.xml | head -n 1
"""
ADDRESS=sh label: 'name',returnStdout: true, script: """
awk '/<\\/*addr\\/*>/{gsub(/[[:space:]]*<\\/*addr\\/*>/,"");print \$0}' demo.xml | head -n 1
"""
sh "mkdir -vp test/demo && ls && pwd"
}
echo "脚本执行返回结果输出 => ${USERNAME} => ${ADDRESS}"
}
}
stage ('END Test') {
steps {
echo "跨阶段变量传递 => ${USERNAME}"
sh "ls && pwd"
sh "java -version;mvn -version;release-cli -v;sonar-scanner --version"
echo "[Test End Stage]!"
}
}
}
}


步骤 03.返回到Test项目首页之中,点击立即构建,然后可以在阶段视图中查看到执行结果,我们可以通过Blue Ocean进行更加直观的展示。

WeiyiGeek.Jenkins agent连接运行测试图

WeiyiGeek.Jenkins agent连接运行测试图


2.完整的pipeline流水线企业项目

描述: 上一章节只是简单使用该镜像创建的容器,是否能被Jenkins正常调度,此章节为作者内部实践项目抽取的pipeline流水线,实现了’代码拉取’, ‘代码检测’, ‘项目构建’,’镜像构建’,’部署测试’,’成品归档’等阶段,供大家参考使用。

步骤 01.在登录Jenkins后,创建一个测试任务weiyigeek-oa,此处根据一个已经存在的任务内部OA系统进行复制创建,修改对应的流水线agent代码块为标签选择。

WeiyiGeek.创建weiyigeek-oa任务流水线图

WeiyiGeek.创建weiyigeek-oa任务流水线图

温馨提示:由于 Jenkins Pipeline 流水线代码太长,需要的朋友可以关注【WeiyiGeek】公众号后回复【Jenkins流水线代码】即可下载 Jenkins-Pipeline.groovy 示例文件。

WeiyiGeek.Jenkins流水线代码图

WeiyiGeek.Jenkins流水线代码图


步骤 03.回到项目首页,点击 【build with Parameters】选择构建项目指定参数,点击【开始构建】便可进行构建任务。

1
2
3
4
5
# 从控制台输出看到当前 weiyigeek-oa 运行在我们使用docker部署的agent节点之上(docker-jenkins-jnlp)
Started by user Jenkins 管理员
[Pipeline] Start of Pipeline
[Pipeline] node
Running on docker-jenkins-jnlp in /home/jenkins/agent/workspace/weiyigeek-oa
WeiyiGeek.流水线任务构建参数设定与运行图

WeiyiGeek.流水线任务构建参数设定与运行图


步骤 04.任务运行完毕后可以通过 Blue Ocean 进行查看各阶段的执行情况,以及查看Jenkins发送在企业微信中构建信息。

1
2
3
4
5
6
7
8
9
10
11
12
Jenkins-消息通知【weiyigeek-oa-1】任务开始 
项目信息: overtime-leave-1.5.5.jar
提交信息: 6f335ad 统计查询添加筛选条件
构建版本: master - origin/master
构建操作: None
镜像构建: True
部署环境: Prod
成品归档: False
质量测试: False
镜像仓库1: harbor.weiyigeek.top/weiyigeek/oa:master
镜像仓库2: harbor.weiyigeek.top/weiyigeek/oa:latest
查看当前任务流水线
WeiyiGeek.流水线任务阶段执行情况查看说明图

WeiyiGeek.流水线任务阶段执行情况查看说明图

至此,在 Docker 中运行 Jenkins Agent 实践完毕!