运维 为 Rails 项目搭建 Jenkins CI 服务器

ACzero · 2017年03月22日 · 最后由 huacnlee 回复于 2017年03月23日 · 10487 次阅读

概述

最近重新搭建了一次 Jenkins CI 服务,并尝试了使用 pipeline 配置任务,结合以前 Jenkins 的配置经验,稍微做了一下总结。本文将讲述如何从头开始搭建一个 Jenkins 服务器并进行相关配置,接着会介绍配置 Jenkins 任务的几种方式,最后会列举一个 rails + jenkins + bitbucket 的 CI 任务配置实例。想先看效果的话建议直接跳到最后一节😂

搭建 jenkins 服务器

以下搭建步骤是在 ubuntu14.04 下进行,其他环境的搭建步骤参考官方的文档

安装 jenkins

参照文档,在 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 访问配置

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 安装完成后会自动创建一个名为jenkins的用户,用户的根目录位于/var/lib/jenkins
  • Jenkins 在执行任务时会以这个jenkins用户执行。

Jenkins 初始化与配置

初始化

上一步 Jenkins 访问配置完成之后,访问 Jenkins 服务器的地址ci.yourcompany.com。首次访问需要进行初始化:

unlock

根据提示执行以下命令取得初始化密码。

$ cat /var/lib/jenkins/secrets/initialAdminPassword

输入密码后需要设置 Jenkins 的管理员账号,并且选择需要安装的插件,插件可以在进入系统之后管理,因此此处按默认配置即可。

至此 Jenkins 服务器已经可用,接下来介绍一些常用但非必要的配置。

shell 配置

Jenkins 在任务运行过程中可以执行 shell 脚本,但在使用前请先检查默认配置的 shell 是否是你想要的。进入 Manage Jenkins -> System Configuration,检查Shell选项。

ssh 密钥配置

在 CI 执行的过程中需要使用到的密钥可以在首页的Credentials中配置,在对应的 Domain 下选择Add credentials,进入如下页面:

ssh_cert

我们需要添加 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 任务

编写 Jenkins 任务主要有两种方式:

  1. 使用 Jenkins 提供的 freestyle project
  2. 用户编写 pipeline

freestyle project 上手简单,基本上参考一个例子就能掌握,但需要按照给定的步骤执行 CI 流程,不够灵活。pipeline 使用 Groovy 编写,需要用户掌握一点 Groovy 语法,而且插件文档不太完善,某些插件可能没提供在 pipeline 中使用的文档,但 pipeline 最大的优势是灵活,用户可以自行控制整个 CI 的流程。下面将以配置一个 Rails 项目的 CI 任务来讲解。

Freestyle project

新建一个freestyle project,进入配置。整个任务可以分为六个部分:

  • General
  • Source Code Management
  • Build Triggers
  • Build Enviroments
  • Build
  • Post-build Actions

首先要说明的是,对于每个任务,Jenkins 都会生成一个 workspace,并会把项目代码 clone 到 workspace 中,在配置中的路径的当前路径均是对应 workspace 的目录 (即项目的根目录)。

General

项目的基础信息配置,在每个选项旁都有一个问号,点开后可以看到详细的文档介绍。

Source Code Management

选择拉取代码的方式。我们这里选择 Git,使用 SSH 方式拉取,Repository URL填写项目的 SSH 地址,Credentials 处需要选择之前添加的 SSH 密钥 (PS: 不要忘了在代码托管服务中添加该 SSH 密钥的公钥)。配置如图:

source_code_management

Build Triggers

该任务的触发方式,不作设置的话只能手动触发,但可以配置为在某个任务结束之后触发,或者是在代码托管收到 commit 时通知 Jenkins,可能需要另外安装插件。此外还能选择采用轮询的方式 (不推荐)。

Build Enviroments

这个步骤会为 build 准备环境,此处我们先安装RVM Plugin,之后我们就能看到Run the build in a RVM-managed environment的选项,作以下配置:

rvm

Build

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

provide_config_file

接下来需要编写 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

Post-build Actions

Build 结束后要执行的动作,一般可以用于向代码托管服务提交构建结果,发送 email,发布 HTML 页面等等,有一部分需要插件。这个例子就不配置了。

以上就是 Freestyle project 的简单配置流程,基本可以满足接收通知->构建->测试->结果反馈的流程,而且也有各种插件支持。

Pipeline

新建一个Pipeline项目,可以看到只有四个部分:

  • General
  • Build Triggers
  • Advanced Project Options
  • Pipeline

General 和 Build Triggers 跟 freestyle project 中的是一样的,Advanced Project Options 默认只有一个设置显示名。基本上全部的配置都集中在 Pipeline。pipeline 的脚本可以直接写在 Jenkins 任务的配置中,也可以放在项目根目录Jenkinsfile文件里。最佳实践是在项目中创建Jenkinsfile并加入版本控制中。

Pipeline script

用户可以使用两种方式编写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 生成页面如图:

pipeline_generator

接下来编写一个跟上面的 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 script from SCM

这是官方推荐的使用方式,适合为一个项目的每个分支配置不同的 pipeline,例如测试分支在 commit 后进行测试并部署到测试服务器,发布分支在 commit 后直接部署到生产服务器。编写方式跟 Pipeline script 大体相同,唯一不同的只有拉取代码的配置。当选择了Pipeline script from SCM后,可以配置 SCM 和脚本的文件名,默认为Jenkinsfile

pipeline_from_scm

然后脚本要作如下修改:

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'){
        //...
    }
}

Bitbucket+Pipeline 实现 rails 项目 CI 服务

首先介绍这个 CI 服务的工作流,先上图:

ci_workflow

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

在 Jenkins 后台可以查看生成的 HTML 报告:

html_reports

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

bitbucket_commit

项目具体配置

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

ci_workflow

需要注意的是,bitbucket 的Scan Credentials仅支持使用 bitbucket 账号,建议使用一个只有读权限的账号。相应填入ownerRepoitory Name信息,Property strategy可以指定哪些 branch 不执行 ci。最后说一下Auto-register webhook这个 checkbox,Jenkins 配置完成后,用户还需要在 bitbucket 配置 webhook 通知 Jenkins,这个配置需要项目管理员权限,因此建议不要勾选这个 checkbox 而是手动去配置。文档给出的配置要点为:

  • URL: [JENKINS_ROOT_URL]/bitbucket-scmsource-hook/notify
  • Check "Push", "Pull Request Created" and "Pull Request Updated" in the triggers section.

该配置在项目首页 -> Settings -> Webhooks。创建一个 webhook,url 按上面指示填写,Triggers 选择Choose from a full list of triggers然后按指示勾选即可。

完成以上步骤后项目配置就完成了,对于其他 SCM,请根据文档配置。

pipeline 脚本

直接贴出 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'
  }
}

PreparationTest部分在之前的 pipeline 介绍中已经解释过。Report部分可以使用一些项目检测工具如RubocopSimplecovBrakeman等输出 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,我也只有配置简单工作流的经验,欢迎指出文章中的不足之处,也欢迎一起交流下使用心得。

参考链接

如果代码用 gitlab 管理,直接使用 gitlab-ci 我觉得更方便一些,使用 docker 作为 ci 容器,代码 push 以后就直接进行 ci,相比 jenkins 省去了很多配置上的麻烦。当然 jenkins 更强大,生态更丰富。

alucardpj 回复

gitlab-ci 还没尝试过。Jenkins 也可以用 docker 简化环境配置,但假如把任务迁到另一个 Jenkins 上,确实是有不少东西要重新配置😓

不闲付费的话可以试用我司 flow.ci 一键 ci 无需提供机器 push 自动跑测试

用 Docker 部署一个 GitLab 和 GitLab CI 或许更简单、也更好用一些

需要 登录 后方可回复, 如果你还没有账号请 注册新账号