Backstage系列教程 - 05 集成 Jenkins

Backstage系列教程 - 05 集成 Jenkins

Zave Genshin Impact

注意:
本文档基于 Backstage 1.32.0 版本编写,并采用自 1.24.0 版本以来官方推荐使用的新后端系统(New BackEnd System)。

Backstage 仍处于不断更新发展中,本文下述步骤可能将不适用于后续更新版本。


一、开发前准备

环境要求

  • ✅ 确认 Jenkins 以及 GitLab 已经成功部署并能够运行
  • ✅ 确认 Jenkins 和 GitLab 能够相互 ping 通
  • ✅ 确认关闭 Jenkins 的 CSRF 机制(不关闭也可以,接下来的内容也会提到不关闭时的处理方案)
  • ✅ 确认 Jenkins 已经安装如下插件

必装插件:

插件 说明
Pipeline 流水线支持
GitLab Plugin GitLab 集成
Git Git 版本控制

可选插件:

插件 说明
Kubernetes CLI Plugin Kubernetes 支持
Docker Pipeline Docker 流水线支持

参考文档

  • 📖 Backstage 官方 | 编写自定义操作脚手架
  • 📖 Backstage 官方 | YAML 文件格式
  • 📖 Jenkins | REST API

二、在前端接入 Jenkins 插件

1. 安装支持插件

从项目的根目录开始:

1
yarn --cwd packages/app add @backstage-community/plugin-jenkins

2. 添加到合适的位置

编辑 packages/app/src/components/catalog/EntityPage.tsx:

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
// ... other import
import {
EntityJenkinsContent,
EntityLatestJenkinsRunCard,
} from '@backstage-community/plugin-jenkins';

// You can add the tab to any number of pages, the service page is shown as an
// example here
const serviceEntityPage = (
<EntityLayout>
<EntityLayout.Route path="/" title="Overview">
{/* ... */}
<EntitySwitch>
<EntitySwitch.Case>
<Grid item sm={6}>
<EntityLatestJenkinsRunCard
branch="main,master"
variant="gridItem"
/>
</Grid>
</EntitySwitch.Case>
{/* ... */}
</EntitySwitch>
</EntityLayout.Route>

{/* other tabs... */}
<EntityLayout.Route path="/ci-cd" title="Jenkins">
<EntitySwitch>
<EntitySwitch.Case>
<EntityJenkinsContent />
</EntitySwitch.Case>
</EntitySwitch>
</EntityLayout.Route>
{/* ... */}
</EntityLayout>
);

依照上面这个例子,我们将 Jenkins 前端插件添加到每一个 Service 类型的 Catalog 中。此时打开 Backstage 中的某一个 Service 类型的项目,则能够看到 Jenkins 插件的大致框架。

但是下方内容区域应该不是如图所示的,而是提示缺少配置标签。

接下来我们还需要安装后端支持来使得 Jenkins 能够显示对应的内容。


三、在后端接入 Jenkins 依赖

1. 安装依赖

从项目的根目录开始:

1
yarn --cwd packages/backend add @backstage-community/plugin-jenkins-backend

2. 添加依赖

编辑 packages/backend/src/index.ts:

1
2
// ... other feature additions
backend.add(import('@backstage-community/plugin-jenkins-backend'));

3. 配置 Jenkins 全局实例

在 app-config.yaml 中添加:

1
2
3
4
jenkins:
baseUrl: https://jenkins.xxxx.com.cn # 替换为你的 Jenkins 服务所对应的地址
username: admin
apiKey: xxxxxxxxxxxx # 替换为你的 Jenkins 服务中拥有最高Admin权限账号的apiKey

配置说明:

  • baseUrl:Jenkins 服务所对应的地址
  • username:最好填写 Admin 账号的名字(也就是 admin),个人开发使用可以填写自己的账号名
  • apiKey:username 填写的账号对应的 apiToken

4. 为单个项目添加 Jenkins 实例

编辑 examples/template/content/catalog-info.yaml:

1
2
3
4
5
6
7
8
9
apiVersion: backstage.io/v1alpha1
kind: Component
metadata:
name: ${{ values.name }}
annotations:
# ...others
jenkins.io/job-full-name: ${{ values.name }}
spec:
#...others

在 examples/template/content 文件夹下的 catalog-info.yaml 文件中的 annotations 下新增一行 jenkins.io/job-full-name: $。

Jenkins 插件将根据这里提供的 Job 命名,到整个 Jenkins 服务中查找对应的 Job,返回信息并展示到前端界面中。

当然,此处给出的示例是根据创建模板项目时填写的值,你也可以改为固定的值(若需要)。

具体含义可以参考这篇文章:如何为 Backstage 编写并添加自定义模板

此时,Jenkins 服务的接入算是圆满完成了。

接下来将要根据本内部开发者平台的需求,对 Jenkins 服务进行一些定制。


四、创建自定义操作脚手架用于为项目创建对应 Job

1. 新建自定义操作脚手架模板

从项目的根目录开始:

1
yarn new

使用上面这个命令,我们可以从中创建很多官方提供的自定义模板。

操作步骤:

  1. 输入命令后,按方向键翻动,找到用于新建一个 action 脚手架的选项
  2. Enter 键选中
  3. 为这个脚手架取一个名字(例如此处可以取名为:backstage-jenkins)
  4. Enter 键确认
  5. 等待脚手架及其依赖安装完成

2. 整理操作脚手架结构

将插件脚手架整理为如下的结构:

1
2
3
4
5
6
7
plugins/backstage-jenkins/
├── src/
│ ├── actions/
│ │ └── jenkins.ts # 整个操作脚手架的逻辑部分
│ ├── index.ts # 向外暴露
│ └── module.ts # 模块定义
└── ...

说明:

  • actions/jenkins.ts:整个操作脚手架的逻辑部分
  • index.ts 和 module.ts:负责将 jenkins.ts 中的内容向外暴露

3. 编写操作脚手架业务逻辑

编辑 src/actions/jenkins.ts:

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
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';

type credentialsItem = {
description: string;
displayName: string;
fingerprint: null,
fullName: string,
id: string,
typeName: string,
}

export function createExampleAction() {
// For more information on how to define custom actions, see
// https://backstage.io/docs/features/software-templates/writing-custom-actions
return createTemplateAction<{
// 在此处,你需要声明本文件要用到的参数名称以及对应的类型
name: string;
repoUrl: string;
GitLab_Username: string;
GitLab_Personal_Access_Token: string;
}>({
id: 'acme:jenkins:create', // 此处也需要创建一个唯一值,用于在编写软件模板时指定是使用什么action
description: 'create a job in jenkins', // 一些必要的说明
schema: {
input: {
type: 'object',
required: ['name', 'repoUrl'], // 参照上面声明的参数进行填充并在下面进行说明
properties: {
name: {
title: 'An example parameter',
description: "This is an example parameter, don't set it to foo",
type: 'string',
},
repoUrl: {
title: 'A target project url',
describe: 'a gitlab repoUrl',
type: 'string',
},
GitLab_Username: {
title: 'GitLab_Username',
describe: 'your GitLab_Username',
type: 'string',
},
GitLab_Personal_Access_Token: {
title: 'GitLab_Personal_Access_Token',
describe: 'your GitLab_Personal_Access_Token',
type: 'string',
},
},
},
},
async handler(ctx) {
ctx.logger.info(
`Running example template with parameters: ${ctx.input.name}`,
);

// 解析 repoUrl
const url = new URL('https://' + ctx.input.repoUrl)
const params = new URLSearchParams(url.search)
const owner = params.get('owner')?.toLocaleLowerCase() // gitlab 项目的 owner
const repo = params.get('repo') // gitlab 项目的名称

const JENKINS_BASE_URL = 'https://jenkins.xxxxx.com.cn' // 替换为你的 Jenkins 服务所对应的地址
const GITLAB_BASE_URL = 'https://gitlabcode.xxxx.com.cn' // 替换为你的 GitLab 服务所对应的地址
const GITLAB_TOKEN = ctx.input.GitLab_Personal_Access_Token
const username = 'admin'
const apiToken = 'xxxxxxxxxxxx' // 替换为你的 Jenkins 服务中拥有最高Admin权限账号的apiKey
const tempName = ctx.input.GitLab_Username
let jenkins_gitlab_credentialsId = ''
let xmlRequestBody = ""

// let crumb = "" // 这是用于访问 Jenkins 时的 crumb 验证
// let setCookie = ""
// 用于保存会话信息,在开启 CSRF 的情况下,Jenkins 将同时验证会话/id/crumb 是否一致

// 首先需要获取 crumb,以便于请求能够通过 Jenkins 的 CSRF 验证
// 当 CSRF 关闭时,不需要获取 crumb,否则将出现 404 错误
// try {
// // 带上用户的 API Token 或者 关闭 CSRF 防护
// const response = await fetch(`${JENKINS_BASE_URL}/crumbIssuer/api/json`, {
// method: 'GET',
// });
// if (!response.ok) {
// throw new Error(`HTTP error! status: ${response.status}`);
// }
// const data = await response.json();
// crumb = data.crumb
// setCookie = response.headers.getSetCookie()[0]
// } catch (error) {
// ctx.logger.error('Error fetching crumb:', error);
// throw error;
// }

// 查询 credentialsID
try {
const response = await fetch(
`${JENKINS_BASE_URL}/credentials/store/system/domain/_/api/json?depth=1`,
{
method: 'GET',
headers: {
// 'Cookie': setCookie,
// 'Jenkins-Crumb': crumb,
'Content-Type': 'application/xml',
'Accept': '*/*',
'Authorization': 'Basic XXXXXXX'
},
}
)
if (!response.ok) {
throw new Error(`Failed to create item. Status: ${response.status}.`);
}
const data = await response.json();
const credentialsList = data.credentials;
credentialsList.map((item: credentialsItem) => {
if (item.displayName.includes(tempName)) {
jenkins_gitlab_credentialsId = item.id
}
})
console.log('jenkins_gitlab_credentialsId:', jenkins_gitlab_credentialsId);

xmlRequestBody = `<?xml version='1.1' encoding='UTF-8'?>
<flow-definition plugin="workflow-job@1472.ve4d5eca_143c4">
<actions />
<description></description>
<keepDependencies>false</keepDependencies>
<definition class="org.jenkinsci.plugins.workflow.cps.CpsScmFlowDefinition" plugin="workflow-cps@4000.v5198556e9cea_">
<scm class="hudson.plugins.git.GitSCM" plugin="git@5.6.0">
<configVersion>2</configVersion>
<userRemoteConfigs>
<hudson.plugins.git.UserRemoteConfig>
<url>${GITLAB_BASE_URL}/${owner}/${repo}</url>
<credentialsId>${jenkins_gitlab_credentialsId}</credentialsId>
</hudson.plugins.git.UserRemoteConfig>
</userRemoteConfigs>
<branches>
<hudson.plugins.git.BranchSpec>
<name>*/master</name>
</hudson.plugins.git.BranchSpec>
</branches>
<doGenerateSubmoduleConfigurations>false</doGenerateSubmoduleConfigurations>
<submoduleCfg class="empty-list" />
<extensions />
</scm>
<scriptPath>Jenkinsfile</scriptPath>
<lightweight>true</lightweight>
</definition>
<triggers />
<disabled>false</disabled>
</flow-definition>`
// 这是创建 Job 使用的配置文件,此处示例为 pipeline job
} catch (error) {
ctx.logger.error('Error creating item:', error);
throw error;
}

// 获取到 crumb 后去创建一个 job
try {
const response = await fetch(
`${JENKINS_BASE_URL}/createItem?name=${ctx.input.name}`,
{
method: 'POST',
headers: {
// 'Cookie': setCookie,
// 'Jenkins-Crumb': crumb,
'Content-Type': 'application/xml',
'Accept': '*/*',
'Authorization': 'Basic ' + btoa(`${username}:${apiToken}`)
},
body: xmlRequestBody
}
);
if (!response.ok) {
// const responseBody = await response.text();
throw new Error(`Failed to create item. Status: ${response.status}.`);
}
} catch (error) {
ctx.logger.error('Error creating item:', error);
throw error;
}

// 获取项目 ID
// let projectID = ""
// try {
// const response = await fetch(
// `${GITLAB_BASE_URL}/api/v4/search?scope=projects&search=${repo}`,
// {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// 'Accept': '*/*',
// 'PRIVATE-TOKEN': GITLAB_TOKEN
// },
// }
// )
// if(!response.ok) {
// throw new Error(`Can't get GitLab ProjectID, Status: ${response.status}`)
// }
// const data = await response.json()
// projectID = data[0]['id']
// } catch(error) {
// ctx.logger.error('Error get GitLab ProjectID:', error);
// throw error;
// }

// 创建 webhook
// try {
// const data = {
// url: `http://10.131.114.10:8080/project/${ctx.input.name}`,
// // Jenkins 的链接,此处需要为 IP 地址+端口形式,这是由于 https://jenkins.xxxxx.com.cn 并不能被 gitlab 识别,
// // 除非能够使得 gitlab 和 Jenkins 所在环境能顺利打通
// push_events: true,
// enable_ssl_verification: false
// }
// const response = await fetch(
// `${GITLAB_BASE_URL}/api/v4/projects/${projectID}/hooks`,
// {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// 'Accept': '*/*',
// 'PRIVATE-TOKEN': GITLAB_TOKEN
// },
// body: JSON.stringify(data)
// })
// if(!response.ok) {
// throw new Error(`Can't set GitLab webhook, Status: ${response.status}`)
// }
// } catch(error) {
// ctx.logger.error('Error set GitLab webhook:', error);
// throw error;
// }
},
});
}

代码逻辑说明:

在这里的例子中,按照非注释内容走通的逻辑为:

在关闭 Jenkins 服务的 CSRF 防御机制的情况下,通过创建项目模板时填写的数据:

  1. 先解析为一个正确的 repoUrl
  2. 再通过拥有者的用户名到 Jenkins Credentials 中查找该用户对应的 GitLab Credentials ID
  3. 将信息填充至准备好的一个 pipeline job 模板中
  4. 通过该模板,并使用 admin 账户的 token,完成对 job 的创建

注释的业务逻辑内容:

  • ① 如果在开启 CSRF 机制的情况下,使用任何一个 Jenkins 的 REST API 都需要先获取一个会话 crumb,然后在 header 中带上这个 crumb 才能够去调用各个 API
  • ② 使用 GitLab Webhook 实现提交时自动触发构建

4. 修改其他两个文件

module.ts

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { createBackendModule } from "@backstage/backend-plugin-api";
import { scaffolderActionsExtensionPoint } from '@backstage/plugin-scaffolder-node/alpha';
import { createExampleAction } from "./actions/jenkins";

/**
* A backend module that registers the action into the scaffolder
*/
export const JenkinsScaffolderModule = createBackendModule({
moduleId: 'jenkins-create-job',
pluginId: 'scaffolder',
register({ registerInit }) {
registerInit({
deps: {
scaffolderActions: scaffolderActionsExtensionPoint
},
async init({ scaffolderActions}) {
scaffolderActions.addActions(createExampleAction());
}
});
},
})

注意事项:

  • 主要是修改了一下函数的命名,此处修改为 JenkinsScaffolderModule
  • 其次就是 moduleId 的命名,务必要确保是唯一的,独一无二的,并且最好是能够表示其作用的。此处是 jenkins-create-job

index.ts

1
export { JenkinsScaffolderModule as default } from './module';

将 index.ts 中向外暴露的函数命名改为和 module.ts 中的函数命名一致。

此时,整个 Jenkins 服务逻辑的定制也算是完成了。


五、如何调用自定义操作脚手架

在一个创建项目用的模板中的 spec: steps 下面新增如下内容:

1
2
3
4
5
6
7
8
- id: jenkins-create-job
name: Create Jenkins Job
action: acme:jenkins:create
input:
name: ${{ parameters.name }}
repoUrl: ${{ parameters.repoUrl }}
GitLab_Username: ${{ parameters.GitLab_Username }}
GitLab_Personal_Access_Token: ${{ parameters.GitLab_Personal_Access_Token }}

通过上述内容,在创建项目时,将会在 Jenkins 服务器中创建与项目相同命名的 pipeline job,并会在配置中使用项目创建者的 Credentials 链接到 GitLab 对应项目。


总结:Backstage 与 Jenkins 的集成涵盖了前端展示、后端对接和自定义脚手架三个层面。通过官方插件可以快速实现基本的 CI/CD 信息展示,而通过开发自定义脚手架,我们可以将项目创建与 Jenkins Job 创建流程打通,实现真正的”一键创建项目”,大幅提升开发效率。


下一篇预告:[Backstage系列教程 06|Gitlab 和 Jenkins 的联动]

  • 标题: Backstage系列教程 - 05 集成 Jenkins
  • 作者: Zave
  • 创建于 : 2026-08-09 16:30:00
  • 更新于 : 2026-08-09 16:30:00
  • 链接: https://zavex.cn/post/27310.html
  • 版权声明: 版权所有 © Zave,禁止转载。
评论