JenkinsFile 工作总结

基础

声明式vs脚本式

  • 脚本式流水线更依赖groovy(特别是错误检查和异常处理),全写node{}块内。

  • 声明式更关注实现逻辑步骤,一般都是直接写声明式。

  • 需要理解,jenkinsfile写的是一种基于groovy开发而来的语法(DSL:领域特定语言Domain-Specific Language),100%兼容groovy,意味着:

    • 你可以直接写groovy,比如定义一个groovy函数

      1
      2
      3
      4
      5
      6
      def function(a){
      ...

      }
      // 并在Jenkinsfile的任何地方调用它,a是要传递给函数的参数
      function(a)
    • 声明式中script{}块用来处理groovy代码

概念

节点

一个概念,包含任何可以执行jenkins任务的系统,包括主节点和代理节点,也可以是一个容器。

  • 主节点

    是一个jenkins实例的主要控制系统,它完全可以访问所有的配置选项和任务(jpb)列表,负责调度构建任务,执行管理任务,处理用户请求,更新配置等,如果没有指定其他节点,它也是默认的任务执行节点.

  • 代理节点

    早期版本的jenkins中也称作slave(从节点),连接到主节点的一台或多台机器(物理,虚拟,容器,云服务器).由主节点控制,负责执行构建和测试任务.使用从节点可以创建一个多机器的分布式构建环境,以适配不同平台或配置的环境进行构建.

执行器(Excutor)

一个执行器是指可以执行任务的槽位或线程.一个节点配置了多少个执行器就可以并发运行多少个独立的构建任务.执行器的数量可以根据节点的资源(cpu,内存)来确定.数量的设置影响jenkins的负载均衡和任务调度能力.

Jenkins处理并发任务的机制基于Excutor模型,每个节点都可以配置一定数量的执行者,这些执行者决定了同时可以运行多少个作业.

项目(Item)

一个持续集成/持续部署的任务或作业,jenkins支持多种类型的项目,比如“Freestyle project”,“Pipeline”,“Multibranch Pipeline”等

声明式结构

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
pipeline {
agent any

triggers { // triggers用于定义触发器,比如配合cron实现定时触发
cron('H */4 * * 1-5')
}

environment {
// 定义环境变量
MY_ENV_VAR = 'some_value'
}

parameters { // 定义pipeline的用户输入参数,description支持一些简单的html标签(高版本).通过`params.<name>`调用参数
string(name: 'GREETING', defaultValue: 'Hello', description: 'The greeting you want to use')
booleanParam(name: 'INCLUDE_TIMESTAMP', defaultValue: true, description: 'Include a timestamp with the greeting?')
choice(name: 'ENVIRONMENT', choices: ['Staging', 'QA', 'Production'], description: 'The target environment')
password(name: 'DEPLOY_KEY', defaultValue: '', description: 'The deployment key')
}

stages { // 包含所有stage块的主块
stage('Prepare') { // stage定义流水线一个阶段
steps { // steps块则是定义这一阶段具体要执行的操作
// 执行准备工作
echo 'Preparing the environment...'
script {
println 'Hello World'
}
}
}

stage('Checkout') {
steps {
// 从版本控制系统检出代码
checkout scm
}
}

stage('Build') {
when {
expression{
anyOf { // anyOf表示或关系,allOf表示与关系,可以嵌套
not { // 加一层not块就表示非
params.GREETING == 'Hello'
params.INCLUDE_TIMESTAMP == 'false'
}
}
}
}
steps {
// 执行构建
echo 'Building...'
script {
// 执行脚本命令
env.BUILD_ID = sh(returnStdout: true, script: 'date +%F_%H-%M-%S').trim()
echo "Build ID: ${env.BUILD_ID}"
}
}
}

stage('Test') {
steps {
// 运行测试
echo 'Testing...'
script {
currentBuild.result = 'SUCCESS' // currentBuild是Pipeline插件提供的一个全局变量,这个变量指向一个RunWrapper类的实例,其中有result属性用于表示构建的状态(结果)
}
}
}

stage('Deploy') {
when { // 有条件地执行stage
expression {
currentBuild.result == 'SUCCESS'
}
}
steps {
// 部署到服务器
echo 'Deploying...'
}
}

stage('Parallel Stage') {
steps {
script {
parallel( // 并发执行,注意格式
stage1: { // 这里的stage1和下面的stage2都是标识符,用来区分,没有其他特别的含义,可以自定义其他名字
echo 'Running stage 1'
},
stage2: {
echo 'Running stage 2'
},
failFast: true // 可选参数,true表示任一并发阶段失败,则整个parallel都会失败
)
}
}
}
}

post {
// 构建后的操作
always {
echo 'This will always run'
}
success {
echo 'Build was successful!'
}
failure {
echo 'Build failed.'
}
unstable {
echo 'Build is unstable.'
}
cleanup {
// 清理后续操作
echo 'Performing cleanup steps'
}
}
}

部署

使用docker部署个一主一从Jenkins

  1. 创建master镜像

    这是官方文档给的例子,包含blue-ocean插件

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    FROM jenkins/jenkins:2.426.3-jdk17
    USER root
    RUN apt-get update && apt-get install -y lsb-release
    RUN curl -fsSLo /usr/share/keyrings/docker-archive-keyring.asc \
    https://download.docker.com/linux/debian/gpg
    RUN echo "deb [arch=$(dpkg --print-architecture) \
    signed-by=/usr/share/keyrings/docker-archive-keyring.asc] \
    https://download.docker.com/linux/debian \
    $(lsb_release -cs) stable" > /etc/apt/sources.list.d/docker.list
    RUN apt-get update && apt-get install -y docker-ce-cli
    USER jenkins
    RUN jenkins-plugin-cli --plugins "blueocean docker-workflow"
  2. 启动master

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    docker build . -t myjenkins-blueocean:2.426.3-1
    docker network create jenkins
    docker run --name jenkins-blueocean --restart=on-failure --detach \
    --network jenkins --env DOCKER_HOST=tcp://docker:2376 \
    --env DOCKER_CERT_PATH=/certs/client --env DOCKER_TLS_VERIFY=1 \
    --publish 8080:8080 --publish 50000:50000 \
    --volume jenkins-data:/var/jenkins_home \
    --volume jenkins-docker-certs:/certs/client:ro \
    myjenkins-blueocean:2.426.3-1
    # 获取token用于首次登录
    docker logs -f jenkins-blueocean
  3. 添加slave节点

    image-20240326203656002

image-20240326203717691

image-20240326203741886

image-20240326203840196

启动方式选择java代理

创建节点后,点解节点的配置,就能看到启动jenkins-agent的命令,记下里面的secret参数

  1. 创建slave镜像

    实际上任何一个可以运行jenkins.jar的环境都可以是jenkins的节点,如果是裸机,先通过主节点获取jenkins.jar,然后直接执行上一步展示出来的命令即可,我这里懒得再开一台虚拟机,所以直接docker完事.

    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
    # 使用带有 Java 的官方基础镜像
    FROM openjdk:17-jdk

    # 设置 Jenkins 主节点地址,代理节点名称和代理秘密的环境变量
    # 这些环境变量应该在运行容
    ENV JENKINS_URL=http://172.18.0.2:8080 # 由于是同一台机器上的两个docker镜像,所以我写的是docker网络的地址
    ENV JENKINS_NODE_NAME=test-slave # 与创建slave的时候名字一致
    ENV JENKINS_SECRET=1735b637d6de619d0396e3198fdc418da5e5cfd35c5cbff70f222821ce85dfdc # 使用上面步骤保存下来的secret

    # 设置 Jenkins 从节点的工作目录环境变量
    ENV JENKINS_AGENT_WORKDIR=/home/jenkins/agent

    # 添加 Jenkins 用户
    RUN useradd -m -d /home/jenkins -s /bin/bash jenkins && \
    mkdir -p /home/jenkins/agent && \
    chown -R jenkins:jenkins /home/jenkins

    # 作为用户 jenkins 操作
    USER jenkins

    # 将工作目录设置为 Jenkins 用户的家目录
    WORKDIR /home/jenkins

    # 下载 Jenkins 代理 JAR,确保 Jenkins 主节点 URL 是可访问的
    # 此命令需在主节点 URL 可用的情况下执行,或者手动复制 agent.jar 到工作目录下
    COPY ./agent.jar /home/jenkins/agent.jar

    # 容器启动时默认执行的命令
    CMD java -jar /home/jenkins/agent.jar \
    -jnlpUrl ${JENKINS_URL}/computer/${JENKINS_NODE_NAME}/slave-agent.jnlp \
    -secret ${JENKINS_SECRET} \
    -workDir ${JENKINS_AGENT_WORKDIR}
  2. 运行slave

    1
    2
    3
    4
    5
    6
    docker build . -t jenkins-slave:2.0.0
    docker run --name jenkins-agent --detach \
    --network jenkins --env JENKINS_MASTER=http://172.18.0.2:8080 \
    --env JENKINS_AGENT_NAME=test-slave --env JENKINS_SECRET=1735b637d6de619d0396e3198fdc418da5e5cfd35c5cbff70f222821ce85dfdc \
    --env JENKINS_AGENT_WORKDIR=/home/jenkins/agent \
    jenkins-slave:2.0.0

    稍等一会,刷新下主节点,显示slave已经在线

凭证

Jenkins的凭证都存放在JENKINS_HOME底下,里面有几个子目录:

  • secrets: 加密用的密钥

  • credentials.xml: 系统范围的凭证

  • users//credentials.xml: 每个用户的私有凭证

安全地管理这些目录和文件的权限

凭证的范围

  • 全局: Jenkins 实例中的所有项目可用,除非在域中做了特定的限制
  • 系统: 只允许jenkins自身以及它的节点使用
  • 用户: 顾名思义,只能该用户使用
  • folder: 只能在folder中的pipeline使用,要先创建folder再创建凭证

凭证域

对凭证进行逻辑分组的机制.将凭证划分到不同的环境或上下文,你就可以根据凭证将要使用的地方来限制凭证的可见性和可用性.

例如,创建不同的域来区分生产和测试环境的凭证

每个域包含一套规则,定义凭证的可用性.jenkins总是有一个默认的全局域,该域没有任何规范,意味着可以被jenkins的任何东西去使用

例如可以为域配置一组说明符,只有满足这些条件,相应域的凭证才会被提供.

image-20240328154342393

比如上图就表示,只有jenkins的任务需要访问*.prod.example.com的时候才能用这个域的凭证

授权策略

Jenkins对于用户的授权提供了多种策略:

路径: dashboard –> 系统管理 –> 全局系统配置

  • 任何人可以做任何事: 顾名思义

  • 登录用户可以做任何事: 一旦用户登录就可以做任何事

  • 安全矩阵(Matrix-based security):

    image-20240326222025486

  • 项目矩阵: 与安全矩阵类似,但是它可以针对项目授权

    image-20240326222849457

工作总结

凭证的处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# withCredentials()获取凭证,指定环境变量名赋值
stage{
withCredentials([usernamPassword(credentialsId: 'XXX', passwordVariable: 'PASSWORD', usernamVariable: 'USER')]){
git_account = env.USER
git_password = env.PASSWORD
}
}


# credentials()获取凭证,通过固定的环境变量名使用,可以在environment{}中获取
pipeline{
agent any
environment {
BITBUCKET_COMMON_CREDS = credentials('peter-cred')
}
stages {
stage ('test') {
steps {
echo $BITBUCKET_COMMON_CREDS_USR # 固定格式<env_name>_[USR|PSW]
}
}
}
}

groovy变量/环境变量

groovy定义的变量不可以直接在其他脚本(例如sh)中使用,可以把它转为环境变量

1
2
3
4
5
6
7
8
9
10
steps {
script {
def curTime = new Data().format('yyyyMMddHHmmss')
def model_version = 'abc' + curTime[0..7]
def model_file = model_version + '.zip'
//使用脚本式将变量转为环境变量
env.model_file = model_file
sh "echo ${model_file}"
}
}

html description

example:

1
2
3
4
5
6
7
8
<table style="border:1px solid black;border-collapse: collapse;cellpadding=25">
<tr>
<th style="border:1px solid black;padding:5px;">aaa</th>
</tr>
<tr>
<th style="border:1px solid black;padding:5px;">bbb</th>
</tr>
</table>

工作坑点

  1. 假如通过jenkins跑云资源的terraform,要注意你要操作的是不是就是你正在跑的jenkins本身,因为有些人会把操作jenkins节点资源的pepeline跑在那个jenkins上

JenkinsFile 工作总结
http://example.com/2023/10/01/jenkinsfile/
作者
Peter Pan
发布于
2023年10月1日
许可协议