# devops-kata-jenkins-pipeline-as-code **Repository Path**: victoryw/devops-kata-jenkins-pipeline-as-code ## Basic Information - **Project Name**: devops-kata-jenkins-pipeline-as-code - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 11 - **Created**: 2020-11-08 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README = DevOps编程操练:用GitLab与Jenkins流水线建立代码质量预警机制 :toc: == 解决痛点 * 代码上线故障多 * 不知如何用docker搭建Jenkins操练环境 * 不知如何开始为Java代码编写自动化单元测试 * 不知如何将单元测试运行在Jenkins流水线上 * 不知如何将繁琐的手工Jenkins流水线配置,简化为编写一个Jenkinsfile脚本,并进行版本控制 * 当流水线出现故障后,不知如何revert导致故障的代码提交,来解决故障 == 使用docker搭建Jenkins操练环境 当然也可以不用docker,直接在本机安装Jenkins。但对于操练DevOps技能来说,Docker是一个必修项目。所以本操练使用docker来搭建操练环境 本操练是 https://www.jianshu.com/p/b790436f53b3[从“CI搭建兽”到“流水线即代码”] 的升级版,除了使用docker来运行Jenkins之外,还将 `Jenkinsfile` 的写法,从原来的脚本式(以 `node` 开头),升级为声明式(以 `pipeline` 开头) === 安装docker 参见 https://ocs.docker.com/engine/install/[Install Docker Engine] 安装Docker 下面以Ubuntu 20.04为例进行操练,其他操作系统操练步骤类同 === 安装Kitematic Kitematic是一个为了方便使用docker而精心设计的图形化工具。参见 https://github.com/docker/kitematic/releases[Kitematic发布页面] 安装Kitematic === 安装Jenkins 安装好docker后,在命令行运行以下命令,就能下载并启动 `jenkins` 容器。其中 `~/OOR` 文件夹是我的个人目录,需要替换成你的本机某个目录 ---- docker run --detach --hostname localhost --publish 8080:8080 --publish 50000:50000 --name jenkins -v ~/docker-volumes/jenkins:/var/jenkins_home jenkins/jenkins ---- 打开 `Kitematic` ,查看 `jenkins` 是否已经运行起来。如果已经运行起来,就可以在浏览器中访问 `http://localhost:8080/` ,进入Jenkins安装页面,安装Jenkins。安装第一步所需要的admin管理员密码,能在 `Home` 页签中的log内容中找到。安装Jenkins插件时,选择默认的即可。 == 用spring boot编写一个web应用程序并手工测试 === 从 http://start.spring.io[start.spring.io] 下载web空白应用 下载前的选项,参见下面的列表。其中Dependencies添加Web * Group: devops.katas * Artifact: adminprovider * Name: adminprovider * Description: Demo project for Jenkins pipeline as code * Dependencies: Web === 编写adminprovider的Web应用,可以按id号一次返回一位管理员 将刚才下载的adminprovider.zip解压,用IntelliJ IDEA打开该Maven项目,开始编写一个Web应用 为方便起见,本操练所创建的类,都写在AdminproviderAppication类中 首先创建 `AdminController` 类 .AdminproviderApplication.java [source,java] ---- @RestController class AdminController { @GetMapping("/admin/{id}") Admin admin(@PathVariable int id) { return new Admin("firstName [" + id + "]", "lastName [" + id + "]"); } } ---- 然后创建 `Admin` 类。其中的两个getter是必须的,否则在运行时会报 `HttpMessageNotWritableException` .AdminproviderApplication.java [source,java] ---- class Admin { private final String firstName; private final String lastName; public Admin(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } // The public getters are mandatory to fix the issue "org.springframework.http.converter. // HttpMessageNotWritableException: No converter found for return value of type: // class devops.katas.adminprovider.Admin" public String getFirstName() { return firstName; } public String getLastName() { return lastName; } } ---- 最后在 `application.properties` 文件中,添加该Web应用启动的端口号 `8765` .application.properties ---- server.port=8765 ---- 此时,在Intellij IDEA中运行 `AdminproviderApplication` 类。然后用浏览器或 https://httpie.org/[HTTPie]工具来访问地址 `localhost:8765/admin/1` 。应该能得到1号管理员的姓和名,参见下图 .用HTTPie工具访问 image::images/httpie-8765-admin-1.png[] == 编写AdminService的自动化单元测试 为了让Jenkins流水线起到质量预警的作用,必须在上面运行自动化测试,来检测每一次代码push是否有缺陷。让我们先从单元测试开始。 目前要测试的单元,是根据 `id` 号生成 `Admin` 对象。这段逻辑写在了 `AdminController` 类中,而这个设计是不好的。因为Controller类本来的用途,是起“传达室”的作用,即将用户的请求,分配给相应的服务来处理。所以良好的设计,应该是把这段逻辑交给 `AdminService` 来处理。而对这段逻辑的单元测试,也就是对 `AdminService` 的单元测试。 第一步,先把上述逻辑交给 `AdminService` 来处理 .AdminproviderApplication.java [source,java] ---- @Configuration class AdminConfiguration { @Bean AdminService adminService() { return new AdminService(); } } class AdminService { public Admin retrieveAdmin(int id) { return new Admin("firstName [" + id + "]", "lastName [" + id + "]"); } } @RestController class AdminController { @Autowired AdminService adminService; @GetMapping("/admin/{id}") Admin admin(@PathVariable int id) { return adminService.retrieveAdmin(id); } } ---- 第二步,为 `AdminService` 编写单元测试 .AdminServiceTest.java [source,java] ---- class AdminServiceTest { @Test public void should_retrieve_an_admin_with_correct_names() { AdminService adminService = new AdminService(); Admin admin = adminService.retrieveAdmin(4); BDDAssertions.then(admin.getFirstName()).isEqualTo("firstName [4]"); BDDAssertions.then(admin.getLastName()).isEqualTo("lastName [4]"); } } ---- 在IntelliJ IDEA中运行单元测试,应该运行通过 == 安装并配置GitLab 在命令行运行以下命令,就能下载并启动 `GitLab` 容器。其中 `~/OOR` 文件夹是我的个人目录,需要替换成你的本机某个目录 ---- docker run --detach --hostname localhost --publish 443:443 --publish 80:80 --publish 22:22 --name gitlab -v ~/docker-volumes/gitlab/etc:/etc/gitlab -v ~/docker-volumes/gitlab/var-log:/var/log/gitlab -v ~/docker-volumes/gitlab/var-opt:/var/opt/gitlab gitlab/gitlab-ce:latest ---- 安装完成后,在浏览器中访问 `http://localhost:80/` ,就能进入 `GitLab` 页面,设置 `root` 用户密码,注册一个用户,并登录 登录 `GitLab` 后,进入用户的 `Settings` 页面,创建 `Access Token` ,并记录下来,以便后面使用 现在可以把前面编写的代码push到 `GitLab` 中,以便后面操练中的Jenkins流水线读取代码来运行自动化测试 可以在 `GitLab` 自己的帐号中,创建一个名为 `devops-kata-jenkins-pipeline-as-code` 的空的代码库。然后在代码根目录中,使用下述命令push代码 [source,bash] ---- git init git add . git commit -m "AdminService with a test" git remote add origin git@localhost:wuzhenben/devops-kata-jenkins-pipeline-as-code.git git push -u origin master ---- 下面的任务,就是要把上述单元测试,运行在Jenkins流水线上 == 在Jenkins界面上编写流水线脚本并运行流水线 虽然本操练的最终目标,是要用Jenkinsfile脚本来定义流水线,但为了调试脚本方便,所以先在Jenkins界面上把脚本调试好,然后再把这些脚本写入Jenkinsfile === 创建文件夹 为方便管理操练内容,首先在Jenkins主页上创建 `jenkins-pipeline-as-code-kata` 文件夹,以后的操作都在该文件夹中 点击 `New Item` .点击 `New Item` image::images/jenkins-new-item.png[] 创建文件夹 .创建文件夹 image::images/jenkins-folder.png[] 不需要配置,直接点 `Save` .不需要配置,直接点 `Save` image::images/jenkins-folder-general.png[] 文件夹创建完毕 .文件夹创建完毕 image::images/jenkins-folder-done.png[] === 确认Maven与git都已经在Jenkins中配置好 因为运行流水线需要Maven和Git这两个工具,所以需要事先在Jenkins里配置好 进入 `Global Tool Configuration` 页面 .进入 `Global Tool Configuration` 页面 image::images/jenkins-tool.png[] 把Maven命名为M3 .把Maven命名为M3 image::images/jenkins-tool-maven.png[] 把git命令在Jenkins容器里的路径设置为 `/usr/bin/git` 。这一点可以通过执行命令 `docker container exec -it jenkins bash` 进入容器内部查看,查看有按 `Ctrl + PQ` 退出 .把git命令的路径设置为 `/usr/bin/git` image::images/jenkins-tool-git.png[] === 创建名为adminprovider的流水线 进入jenkins-pipeline-as-code-kata文件夹,点击 `New Item` ,创建名为 `adminprovider` 的流水线 .创建名为 `adminprovider` 的流水线 image::images/jenkins-pipeline-create.png[] === 修改流水线的脚本 在流水线配置页面的底部, `script` 输入框的右上角 `try sample Pipeline...` ,选择 `GitHub + Maven` 流水线样例脚本,作为修改的基础 .选择 `GitHub + Maven` 流水线样例脚本,作为修改的基础 image::images/jenkins-pipeline-sample.png[] 将第13行的git代码库的地址改为本操练的代码库的地址 `http://192.168.31.60/wuzhenben/devops-kata-jenkins-pipeline-as-code.git` 其中, `192.168.31.60` 是我的电脑ip地址,需要替换成你的电脑地址 将第16行的mvn命令,改为 `./mvnw clean package` 。mvnw命令能够在没有安装maven的情况下,运行maven命令。之后,点击 `Save` 按钮保存 点击 `Build Now` 手工触发流水线构建。点击左下角 `#1` 左侧的小圆点,能够跳转到控制台输出页面,观察运行结果。 .点击 `Build Now` 手工触发流水线构建 image::images/jenkins-pipeline-build-now.png[] .点击左下角 `#1` 左侧的小圆点,能够跳转到控制台输出页面 image::images/jenkins-pipeline-console-output.png[] 如果一切正常,那么构建应该成功。这表明在界面上编写的脚本没有问题。下面可以把这些脚本写到 `Jenkinsfile` 文件中,以便让Jenkins读取该文件中的流水线配置信息。从而实现用Jenkinsfile脚本文件来定义流水线,减轻配置的工作量。 == 根据脚本创建Jenkinsfile,并配置Jenkins,使其读取Jenkinsfile来运行流水线 因为流水线脚本要从git版本库中读取,需要重新配置,所以现在创建一个名为 `adminprovider-from-scm` 新的流水线 .创建名为adminprovider-from-scm的流水线 image::images/jenkins-pipeline-create-new.png[] === 准备好Jenkinsfile 在流水线配置页面的底部, `script` 输入框的右上角 `try sample Pipeline...` ,选择 `GitHub + Maven` 流水线样例脚本,将其内容复制粘贴到代码根目录下新创建的Jenkinsfile文件中,并把其中的git版本库地址和maven命令如上所示更改过来。为了验证Jenkins确实从Jenkinsfile读取了流水线配置,在 `steps` 第一句增加了 `echo 'hello from scm` 。修改完Jenkinsfile后,就可以点击流水线配置页面底部的 `Save` 按钮,保存配置。 .Jenkinsfile [source,groovy] ---- pipeline { agent any tools { // Install the Maven version configured as "M3" and add it to the path. maven "M3" } stages { stage('Build') { steps { echo 'hello from scm' // Get some code from a GitHub repository git 'http://username:TzVCYd7-zte3a3zB8swZ@192.168.31.60/wuzhenben/devops-kata-jenkins-pipeline-as-code.git' // Run Maven on a Unix agent. sh "./mvnw clean package" // To run Maven on a Windows agent, use // bat "mvn -Dmaven.test.failure.ignore=true clean package" } post { // If Maven was able to run the tests, even if some of the test // failed, record the test results and archive the jar file. success { junit '**/target/surefire-reports/TEST-*.xml' archiveArtifacts 'target/*.jar' } } } } } ---- 上面代码中的 `http://username:TzVCYd7-zte3a3zB8swZ@` 包含了前面配置的 `GitLab` 的 access token。如果没有这个token,那么在 `Jenkins` 里构建时会报 `Authentication failed` 错误 使用以下命令,将代码push到git版本库 [source,bash] ---- git add . git commit -m "add Jenkinsfile" git pull --rebase git push -u origin master ---- === 配置Jenkins使其读取代码库中的Jenkinsfile来配置流水线 进入刚刚创建的流水线 `adminprovider-from-scm` 配置页面,在页面底部的 `Pipeline` 配置区域,点击 `Definition` 下拉框,选择 `Pipeline script from SCM` .选择 `Pipeline script from SCM` image::images/jenkins-pipeline-script-from-SCM.png[] 在 `SCM` 下拉框中,选择 `Git`。在 `Repository URL` 中,填入Jenkinsfile所在的代码库的地址 `http://192.168.31.60/wuzhenben/devops-kata-jenkins-pipeline-as-code.git`。确保 `Branch Specifier` 中填写了 `*/master`, `Script Path` 中填写了 `Jenkinsfile` 。点击 `Save` 保存 .选择 `Git` ,填写代码库地址 image::images/jenkins-pipeline-script-from-SCM-Git.png[] 点击 `Build Now` 手工触发流水线构建,让Jenkins读取代码库中的Jenkinsfile。 .点击 `Build Now` 手工触发流水线构建 image::images/jenkins-pipeline-script-from-SCM-build-now.png[] 点击左下角 `#1` 左侧的小圆点,能够跳转到控制台输出页面,观察运行结果中包含了上面添加的那句 `hello from scm` 。说明Jenkins确实读取了Jenkinsfile .观察运行结果中包含了上面添加的那句 `hello from scm` image::images/jenkins-pipeline-script-from-SCM-hello-from-scm.png[] == 触发流水线 现在Jenkins能从代码库中读取Jenkinsfile了。这意味着流水线的配置,都可以用有版本控制的脚本来完成。但如何让流水线的构建自动进行,从而尽早频繁小批地发现代码集成的问题,以便修复呢? 一种方法是使用cron来每隔一段时间来轮询。但这样作无论代码是否更新,轮询总是会触发构建,比较耗费资源。另一种更有优势的方法是在GitLab里配置web hook。因为代码库一旦有代码push上来, GitLab 就能通过 web hook 通知Jenkins进行构建,从而把频繁小批构建做到极致。 ==== 在Jenkins里为配置web hook做准备 首先确认Jenkins安装了GitLab插件和Git插件 然后在Jenkins的System Credentials里,把GitLab的api access token粘贴进去 .把GitLab的api access token配置到Jenkins里 image::images/add-gitlab-credentials-to-jenkins.png[] 在Jenkins的System Configuration里,确保GitLab中的Credentials下拉菜单选择了上面配置的GitLab api access token .确保GitLab中的Credentials下拉菜单选择了上面配置的GitLab api access token image::images/configure-gitlab-credentials.png[] === 在GitLab里配置web hook === 在jenkinsfile中配置轮询 为了验证Jenkins对代码库的轮询,确实来自Jenkinsfile,可以先打开流水线配置页面中的build trigger配置,确认没有任何选项被勾选了 .打开流水线配置页面中的build trigger配置,确认没有任何选项被勾选了 image::images/jenkins-pipeline-script-from-SCM-build-trigger-none.png[] 在Jenkinsfile中的 `agent any` 下面,添加五个星号的 `cron` ,表示Jenkins每隔1分钟就轮询一次代码库,无论是否有新代码,都会执行构建 [source,groovy] ---- triggers { cron('* * * * *') } ---- 使用以下命令,将代码push到git版本库 [source,bash] ---- git commit -am "add triggers with 5 stars into Jenkinsfile" git pull --rebase git push -u origin master ---- 点击 `Build Now` 手工触发流水线构建,让Jenkins读取代码库中的Jenkinsfile 确认流水线配置页面中的Build Triggers配置区域中,Build periodically已经被勾选,且五个星出现在 `Schedule` 输入框中。这表明Jenkins确实读取了Jenkinsfile .确认流水线配置页面中的Build Triggers配置区域中,Build periodically已经被勾选,且五个星出现在 `Schedule` >输入框中。 image::images/jenkins-pipeline-script-from-SCM-with-triggers.png[] == 在流水线上引入一个编译错误,并revert来解决问题 现在操练一下当流水线遇到编译错误时,会报什么错 在测试代码中,加一句 `abc();` ,然后push代码到代码库 .AdminServiceTest.java [source,java] ---- class AdminServiceTest { @Test public void should_retrieve_an_admin_with_correct_names() { abc(); AdminService adminService = new AdminService(); Admin admin = adminService.retrieveAdmin(4); BDDAssertions.then(admin.getFirstName()).isEqualTo("firstName [4]"); BDDAssertions.then(admin.getLastName()).isEqualTo("lastName [4]"); } } ---- 等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信息。点击相应提交左边的小圆球,能看到具体的错误信息 .等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信息 image::images/jenkins-pipeline-script-from-SCM-compilation-error-committer.png[] .点击相应提交左边的小圆球,能看到具体的错误信息 image::images/jenkins-pipeline-script-from-SCM-compilation-error-logs.png[] 使用下述命令来查看上次提交的hash号,revert刚才引起流水线故障的提交 [source,bash] ---- git log git revert 131f54ebb5554aef43fc823d5d8d6fb7aaa8898c git push ---- revert并且push,1分钟后,流水线自动构建,故障消失 .revert并且push,1分钟后,流水线自动构建,故障消失 image::images/jenkins-pipeline-script-from-SCM-compilation-error-reverted.png[] == 在流水线上引入一个自动化单元测试失败,并revert来解决问题 现在操练一下当流水线遇到测试失败时,会报什么错 在测试代码中,将断言中的 `firstName [4]` 改为 `firstName [40]` ,然后push代码到代码库 .AdminServiceTest.java [source,java] ---- class AdminServiceTest { @Test public void should_retrieve_an_admin_with_correct_names() { AdminService adminService = new AdminService(); Admin admin = adminService.retrieveAdmin(4); BDDAssertions.then(admin.getFirstName()).isEqualTo("firstName [40]"); BDDAssertions.then(admin.getLastName()).isEqualTo("lastName [4]"); } } ---- 等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交信 息。点击相应提交左边的小圆球,能看到具体的错误信息 .等1分钟后,流水线被轮询程序自动触发。把鼠标放到有提交的出错构建处,能看到导致这次构建失败的提交人和提交>信息 image::images/jenkins-pipeline-script-from-SCM-failed-test-committer.png[] .点击相应提交左边的小圆球,能看到具体的错误信息 image::images/jenkins-pipeline-script-from-SCM-failed-test-logs.png[] 可以使用上面提到的命令来查看上次提交的hash号,revert刚才引起流水线故障的提交 == 将Jenkinsfile中的cron改为不那么频繁地构建 每分钟构建一次十分耗费资源,所以可以把轮询次数改为工作时间每2小时构建一次 .Jenkinsfile [source,groovy] ---- pipeline { agent any triggers { cron('H H(8-15)/2 * * 1-5') } ---- push代码,1分钟后自动构建,Jenkins会把修改后的轮询配置自动更新到配置页面 == 作业 操练到此结束。现在该轮到你操练了。可以换一个业务场景操练一下。比如可以将根据id号获取管理员的业务场景,换成根据id号获取学生,从头到尾操练一遍。愿你有所收获 == 反馈 为了让下次DevOps编程操练让你更有收获,不妨花2分钟 https://wj.qq.com/s2/7322298/7802/[填写4个问题]