最近重新搭建了一次 Jenkins CI 服务,并尝试了使用 pipeline 配置任务,结合以前 Jenkins 的配置经验,稍微做了一下总结。本文将讲述如何从头开始搭建一个 Jenkins 服务器并进行相关配置,接着会介绍配置 Jenkins 任务的几种方式,最后会列举一个 rails + jenkins + bitbucket 的 CI 任务配置实例。想先看效果的话建议直接跳到最后一节
以下搭建步骤是在 ubuntu14.04 下进行,其他环境的搭建步骤参考官方的文档。
参照文档,在 ubuntu 下安装步骤比较简单:
$ wget -q -O - https://pkg.jenkins.io/debian/jenkins-ci.org.key | sudo apt-key add -
$ sudo sh -c 'echo deb http://pkg.jenkins.io/debian-stable binary/ > /etc/apt/sources.list.d/jenkins.list'
$ sudo apt-get update
$ sudo apt-get install jenkins
Jenkins 默认使用8080端口,若要使用其他端口可以在/etc/default/jenkins文件下修改这一行:
HTTP_PORT=8080
为了访问 Jenkins 服务器,我们要将 80 端口的请求代理到 Jenkins 使用的端口,参考官方提供的 Nginx 配置:
upstream app_server {
# 若jenkins使用其他端口,127.0.0.1:8080要作对应修改
server 127.0.0.1:8080 fail_timeout=0;
}
server {
listen 80;
listen [::]:80 default ipv6only=on;
server_name ci.yourcompany.com;
location / {
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_redirect off;
if (!-f $request_filename) {
proxy_pass http://app_server;
break;
}
}
}
假如你需要使用 ssl,参考这个配置
jenkins的用户,用户的根目录位于/var/lib/jenkins。jenkins用户执行。上一步 Jenkins 访问配置完成之后,访问 Jenkins 服务器的地址ci.yourcompany.com。首次访问需要进行初始化:

根据提示执行以下命令取得初始化密码。
$ cat /var/lib/jenkins/secrets/initialAdminPassword
输入密码后需要设置 Jenkins 的管理员账号,并且选择需要安装的插件,插件可以在进入系统之后管理,因此此处按默认配置即可。
至此 Jenkins 服务器已经可用,接下来介绍一些常用但非必要的配置。
Jenkins 在任务运行过程中可以执行 shell 脚本,但在使用前请先检查默认配置的 shell 是否是你想要的。进入 Manage Jenkins -> System Configuration,检查Shell选项。
在 CI 执行的过程中需要使用到的密钥可以在首页的Credentials中配置,在对应的 Domain 下选择Add credentials,进入如下页面:

我们需要添加 ssh 密钥的配置,类型选择SSH Username with private key。private key 的提供方式有三种,第一种Enter directly是直接填到 Jenkins 的配置中,后两种需要在服务器上生成 ssh 密钥。例如采用第三种From the Jenkins master ~/.ssh,需要像这样生成 ssh 密钥:
$ sudo su jenkins
$ ssh-keygen -t rsa
选择好配置之后点OK,以后在编写任务的时候就可以使用该密钥。
项目中的密钥配置不会放在代码库中,可以使用 Jenkins 的插件Config File Provider Plugin。安装插件后进入 Manage Jenkins -> Managed files 即可创建配置文件。
编写 Jenkins 任务主要有两种方式:
freestyle project 上手简单,基本上参考一个例子就能掌握,但需要按照给定的步骤执行 CI 流程,不够灵活。pipeline 使用 Groovy 编写,需要用户掌握一点 Groovy 语法,而且插件文档不太完善,某些插件可能没提供在 pipeline 中使用的文档,但 pipeline 最大的优势是灵活,用户可以自行控制整个 CI 的流程。下面将以配置一个 Rails 项目的 CI 任务来讲解。
新建一个freestyle project,进入配置。整个任务可以分为六个部分:
首先要说明的是,对于每个任务,Jenkins 都会生成一个 workspace,并会把项目代码 clone 到 workspace 中,在配置中的路径的当前路径均是对应 workspace 的目录 (即项目的根目录)。
项目的基础信息配置,在每个选项旁都有一个问号,点开后可以看到详细的文档介绍。
选择拉取代码的方式。我们这里选择 Git,使用 SSH 方式拉取,Repository URL填写项目的 SSH 地址,Credentials 处需要选择之前添加的 SSH 密钥 (PS: 不要忘了在代码托管服务中添加该 SSH 密钥的公钥)。配置如图:

该任务的触发方式,不作设置的话只能手动触发,但可以配置为在某个任务结束之后触发,或者是在代码托管收到 commit 时通知 Jenkins,可能需要另外安装插件。此外还能选择采用轮询的方式 (不推荐)。
这个步骤会为 build 准备环境,此处我们先安装RVM Plugin,之后我们就能看到Run the build in a RVM-managed environment的选项,作以下配置:

该部分配置 CI 过程执行什么的地方 (例如跑测试,生成报告等等)。点击add build step添加构建步骤。如果安装了Config File Provider Plugin,则可以选择Provide Configuration files,选择配置文件后填好路径,这样在任务执行时配置文件就会被复制到目标路径。配置如图:

接下来需要编写 shell 脚本,添加Execute shell,编写以下 shell script:
source ~/.bashrc
gem install bundler
bundle install
# 重新建立数据库
bundle exec rake db:drop RAILS_ENV=test
bundle exec rake db:create RAILS_ENV=test
bundle exec rake db:migrate RAILS_ENV=test
# 执行测试
bundle exec rake test
Build 结束后要执行的动作,一般可以用于向代码托管服务提交构建结果,发送 email,发布 HTML 页面等等,有一部分需要插件。这个例子就不配置了。
以上就是 Freestyle project 的简单配置流程,基本可以满足接收通知->构建->测试->结果反馈的流程,而且也有各种插件支持。
新建一个Pipeline项目,可以看到只有四个部分:
General 和 Build Triggers 跟 freestyle project 中的是一样的,Advanced Project Options 默认只有一个设置显示名。基本上全部的配置都集中在 Pipeline。pipeline 的脚本可以直接写在 Jenkins 任务的配置中,也可以放在项目根目录Jenkinsfile文件里。最佳实践是在项目中创建Jenkinsfile并加入版本控制中。
用户可以使用两种方式编写Pipeline script:声名式 (declarative) 和脚本式 (scripted)。声明式使用 DSL 来描述 pipeline,而脚本式则完全是使用 Groovy 来编写。个人认为声明式并没有怎样简化 pipeline 的编写,只是提高了可读性。总体看来脚本式和声明式差不多,下面编写流程将采用脚本式 pipeline,对声明式感兴趣的话可以浏览官方文档。
对于脚本式 pipeline,结构大体上是这样的:
node {
stage('Build') {
sh 'make'
}
stage('Test') {
sh 'make check'
}
stage('Deploy') {
sh 'make publish'
}
}
用户可以定义各种阶段 (stage) 进行不同的工作。这里先介绍一下怎样快速上手 pipeline。在项目的页面左侧能找到Pipeline Syntax的选项,可以进入一个生成 pipeline script 的页面,相当于查看文档,当安装了插件后,部分插件的方法也会被添加到这里。用的比较多的是 Jenkins 提供的sh方法,可以执行给定的 shell 脚本,并且可以设置把 stdout 作为返回值,具体参考文档。pipeline 生成页面如图:

接下来编写一个跟上面的 freestyle project 相同的 pipeline 任务。首先必须确保 CI 运行环境,要先安装 rvm。可以在 pipeline 中进行安装,我选择的是在服务器上直接装好:
$ sudo su jenkins
$ cd
$ gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
$ \curl -sSL https://get.rvm.io | bash
pipeline 的结构如下:
node{
def rails_env = 'test'
stage('Preparation'){
//...
}
stage('Test'){
//...
}
}
先看 Preparation 阶段。首先要做的工作是从代码库中 clone 代码,从 generator 生成如下的方法调用:
git branch: 'master', credentialsId: 'b33c1c57-737d-4195-a6f5-446a9d000f2a', url: 'git@jenkins/example.git'
接下来是准备测试环境,为了让 pipeline 更加灵活,可以通过项目里的.ruby-version 文件安装和使用指定版本的 ruby。假如想要使用特定版本的 bundler 进行bundle install,可以通过检测Gemfile.lock中的信息来获取 bundler 的版本 (适用于大于 1.10 版本的 bundler 生成的Gemfile.lock)。上述步骤完成后需要先删除上次任务执行时的数据库并重新创建。编写如下 pipeline:
node{
stage('Preparation'){
// 拉取代码
git branch: 'master', credentialsId: 'b33c1c57-737d-4195-a6f5-446a9d000f2a', url: 'git@jenkins/example.git'
// 获取项目ruby版本
def rubyVersion = sh(script: 'cat .ruby-version', returnStdout: true).trim()
sh 'source $HOME/.bashrc'
// 假如没有安装所需版本ruby,用rvm进行安装
// "${foo}"为groovy的字符串插值方式
// """也是groovy中表示字符串的方式
sh """if ! rvm ${rubyVersion} do ruby -v &> /dev/null; then
rvm get stable
rvm install ${rubyVersion}
fi"""
// 获取bundler版本并进行安装
def bundlerVersion = sh(script: '''rvm all do ruby -e 'puts $<.read[/BUNDLED WITH\\n (\\S+)$/, 1] || "<1.10"' Gemfile.lock''', returnStdout: true).trim()
sh "rvm ${rubyVersion} do gem install bundler --conservative --no-document -v ${bundlerVersion}"
// 在CI环境下适合使用--deployment选项,具体看:http://bundler.io/v1.14/man/bundle-install.1.html#DEPLOYMENT-MODE
sh "rvm ${rubyVersion} do bundle install --deployment --retry=3"
// 使用Config File Provider Plugin提供的方法复制配置文件
configFileProvider([configFile(fileId: 'cd003fbc-d1ae-496b-b68c-b08f0640a286', targetLocation: 'config/secrets.yml', variable: 'SECRET_FILE'), configFile(fileId: 'fff7f49d-254b-478e-ab7c-f4587927cdbb', targetLocation: 'config/database.yml', variable: 'DATABASE_FILE')]){}
// 重新建立测试用数据库
sh "rvm ${rubyVersion} do bundle exec rake db:drop db:create db:migrate RAILS_ENV=${rails_env}"
}
}
下面是测试步骤的 pipeline
node {
def rails_env = 'test'
stage('Preparation') {
// ...
}
stage('Test') {
def rubyVersion = sh(script: 'cat .ruby-version', returnStdout: true).trim()
sh "rvm ${rubyVersion} do bundle exec rake test RAILS_ENV=${rails_env}"
}
}
本例子测试步骤比较简单,这点因项目测试使用的工具不同而不同。至此跟上述 freestyle project 相同功能的 pipeline script 编写完成。
这是官方推荐的使用方式,适合为一个项目的每个分支配置不同的 pipeline,例如测试分支在 commit 后进行测试并部署到测试服务器,发布分支在 commit 后直接部署到生产服务器。编写方式跟 Pipeline script 大体相同,唯一不同的只有拉取代码的配置。当选择了Pipeline script from SCM后,可以配置 SCM 和脚本的文件名,默认为Jenkinsfile。

然后脚本要作如下修改:
node {
// 从设置好的SCM拉取代码到workspace
checkout scm
def rails_env = 'test'
def rubyVersion = sh(script: 'cat .ruby-version', returnStdout: true).trim()
stage('Preparation'){
// 此处删去拉取代码的方法
sh 'source $HOME/.bashrc'
//...
}
stage('Test'){
//...
}
}
首先介绍这个 CI 服务的工作流,先上图:

当 bitbucket 上有新的 commit 或者 pull request 时,会通知 Jenkins 服务器,CI 工作流开始。首先是Preparation阶段,拉取代码后执行 bundle install 等命令,为 rails 项目准备测试环境。接着是Test阶段,运行项目对应的测试,输出测试结果。接着是Report阶段,可以使用brakeman,rubocop和simplecov等项目检测工具,生成 HTML 页面报告。最后检测当前分支,若是 staging 用的分支则部署到 staging 环境,其他分支则跳过。最后将 CI 结果返回给 bitbucket,要注意的是,只要其中某个 stage 出错,就会直接返回 CI 结果。
在 Jenkins 后台可以查看生成的 HTML 报告:

在 bitbucket 每个 commit 可以查看 build 结果:

接下来介绍配置步骤。使用Multibranch Pipeline类型的项目,这种项目能跟踪整个 repo 的提交。创建成功后,必须配置的只有Branch Sources,使用 bitbucket 的话需要安装Bitbucket Plugin。配置如图:

需要注意的是,bitbucket 的Scan Credentials仅支持使用 bitbucket 账号,建议使用一个只有读权限的账号。相应填入owner和Repoitory Name信息,Property strategy可以指定哪些 branch 不执行 ci。最后说一下Auto-register webhook这个 checkbox,Jenkins 配置完成后,用户还需要在 bitbucket 配置 webhook 通知 Jenkins,这个配置需要项目管理员权限,因此建议不要勾选这个 checkbox 而是手动去配置。文档给出的配置要点为:
该配置在项目首页 -> Settings -> Webhooks。创建一个 webhook,url 按上面指示填写,Triggers 选择Choose from a full list of triggers然后按指示勾选即可。
完成以上步骤后项目配置就完成了,对于其他 SCM,请根据文档配置。
直接贴出 Jenkinsfile,具体解释在脚本的注释部分
// plugins:
// SSH Agent Plugin
// Config File Provider Plugin
// HTML Publisher plugin
node {
checkout scm
def rails_env = 'test'
// 获取项目的ruby版本
def rubyVersion = sh(script: 'cat .ruby-version', returnStdout: true).trim()
stage('Preparation'){
// 若提示command not found等错误,检查shell配置:https://issues.jenkins-ci.org/browse/JENKINS-29877
sh 'source $HOME/.bashrc'
// 安装对应版本的ruby
sh """if ! rvm ${rubyVersion} do ruby -v &> /dev/null; then
rvm get stable
rvm install ${rubyVersion}
fi"""
// 安装对应bundler
def bundlerVersion = sh(script: '''rvm all do ruby -e 'puts $<.read[/BUNDLED WITH\\n (\\S+)$/, 1] || "<1.10"' Gemfile.lock''', returnStdout: true).trim()
sh "rvm ${rubyVersion} do gem install bundler --conservative --no-document -v ${bundlerVersion}"
// --deployment http://bundler.io/deploying.html#deploying-your-application
sh "rvm ${rubyVersion} do bundle install --deployment --retry=3"
// 复制配置文件,使用Config File Provider Plugin
configFileProvider([configFile(fileId: 'cd003fbc-d1ae-496b-b68c-b08f0640a286', targetLocation: 'config/secrets.yml', variable: 'SECRET_FILE'), configFile(fileId: 'fff7f49d-254b-478e-ab7c-f4587927cdbb', targetLocation: 'config/database.yml', variable: 'DATABASE_FILE')]){
}
// 重建数据库
sh "rvm ${rubyVersion} do bundle exec rake db:drop db:create db:migrate RAILS_ENV=${rails_env}"
}
stage('Test'){
sh "rvm ${rubyVersion} do bundle exec rake test RAILS_ENV=${rails_env}"
}
stage('Report'){
// 生成brakeman和rubocop报告,以HTML输出
sh "rvm ${rubyVersion} do bundle exec brakeman --summary -o brakeman.html"
sh "rvm ${rubyVersion} do bundle exec rubocop --out rubocop.html || true"
// 使用HTML Publisher plugin,在项目页面生成HTML展示页面链接。
publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: false, reportDir: './', reportFiles: 'brakeman.html', reportName: 'Brake Report'])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: false, reportDir: 'coverage/', reportFiles: 'index.html', reportName: 'SimpleCov Report'])
publishHTML([allowMissing: true, alwaysLinkToLastBuild: false, keepAll: false, reportDir: './', reportFiles: 'rubocop.html', reportName: 'Rubocop Report'])
}
if(env.BRANCH_NAME=='master'){
stage('Deploy'){
// 使用SSH Agent Plugin提供ssh key,用capistrano进行staging环境的部署
sshagent(['b33c1c57-737d-4195-a6f5-446a9d000f2a']) {
sh "rvm ${rubyVersion} do bundle exec cap staging deploy"
}
}
}else{
println 'not on master, skip deploy'
}
}
Preparation和Test部分在之前的 pipeline 介绍中已经解释过。Report部分可以使用一些项目检测工具如Rubocop,Simplecov和Brakeman等输出 html 页面。在 pipeline 中没有去生成 simplecov 的报告,因为在测试的阶段就会生成。然后使用HTML Publisher plugin发布页面。
使用 HTML Publisher plugin 需要修改 Jenkins 默认的CSP配置 (会产生安全问题,在不了解的情况下不建议修改),否则发布出来的页面的 css 和 js 都会被禁用。修改的方法查看官方文档,具体如何配置要根据发布的页面对应设置。设置可以在 Manage Jenkins -> Script Console 中执行脚本来设置,但在 Jenkins 重启后将会重新从配置文件加载,所以想保留修改的话要在/etc/default/jenkins中作为参数传递。像这样修改 JAVA_ARGS 参数:
JAVA_ARGS="-Djava.awt.headless=true -Dhudson.model.DirectoryBrowserSupport.CSP=\"default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-eval'; img-src 'self'\""
看到这里,我想你应该已经掌握了 Jenkins 的使用方法。使用下来个人感觉 Jenkins 是一个很完善的自动化工具,你可以用 Jenkins 来实现 CI,CD 等工作流。Jenkins 还有非常多的插件,因此可以快速实现你的工作流,对于 Github,Bitbucket 都有比较完善的支持。Pipeline 的引入又提供了极大的自由度,折腾起来还是非常有意思的。但 Jenkins 有一部分插件的文档不完善,而官方的文档也是略为简略,所以坑还是有不少的。对于 Jenkins,我也只有配置简单工作流的经验,欢迎指出文章中的不足之处,也欢迎一起交流下使用心得。