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

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

概述

最近重新搭建了一次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,我也只有配置简单工作流的经验,欢迎指出文章中的不足之处,也欢迎一起交流下使用心得。

参考链接

共收到 4 条回复

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

alucardpj 回复

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

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

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

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