使用 Jenkins 和 Ansible 实现 CI/CD
文章类型:翻译 译者:wangwenjuan 原文链接:https://jenkins-zh.cn/wechat/articles/2020/09/2020-09-09-ci-cd-with-jenkins-and-ansible/ 推荐语:以 Spring Boot + JS 项目为例,通过 Jenkins 以及 Ansible 实现 CI/CD
前言
当下,Kubernetes 在容器编排大战中取得了胜利。我们中的一些人怀着羡慕的心情阅读着硅谷创业公司的那些文章(是的,或许你所在的城市已经有了这些创业公司了!),然而读完之后还是回到自己手上运行得还可以的遗留的老系统上工作。
基于主干开发,容器部署至云上,这些虽然都在 DevOps 未来的规划中,但是短期内这些还基本无法落地。
向 DevOps 方向迈出的一步是要消除孤岛(dev,QA,ops),因此我们必须以一种每个角色都能轻松协作的方式来构建我们的代码。
我阅读了很多非常不错的文章,介绍如何使用一些单页面 Javascript 和 Spring Boot 后端构建应用,其中还涉及了配置管理、基础框架、持续集成和持续交付。现在我将结合以上所有内容,为你开展自己的工作提供一些支持和帮助。
准备
我准备了一个 Jenkins 实例,部署了 ssh, 以及一个可运行的 Spring Boot jar,还有一台 RedHat7 的虚拟机,和 Nexus 的制品仓库。所以我想我很高兴不用再部署 EARs 了。
现在我将使用以上的工具构建一个部署流水线,并对所有内容做版本控制,以便团队中的每个人都可以访问所有内容,并了解他们的代码从提交到部署的每个环节(本例中只是到测试环境)。
代码结构如下:
parent
+- backend
+- frontend
+- deployment
Jenkinsfile简单起见,backend——一个简单的 Spring Boot 应用——包含了前端 ReactJS 应用,deployment 中是持续交付相关工具,根目录下的 Jenkinsfile 是这个流水线的声明式描述
下面我们看一下每个模块!
后端
它继承自 Spring Boot parent:
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/>
</parent>
我们将 frontend 应用放在 dependencies:
...
<dependency>
<groupId>com.company.skeleton</groupId>
<artifactId>frontend</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
...
</dependencies>
我还使用了 Spotbugs,Checkstyle 和 Jacoco 来做静态代码检查和代码覆盖率检查,所以我们也将这些插件添加进来。需要注意的是安全插件 Spotbugs 是一个小的安全防护左移。
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<executable>true</executable>
</configuration>
</plugin>
<plugin>
<groupId>com.github.spotbugs</groupId>
<artifactId>spotbugs-maven-plugin</artifactId>
<version>3.1.3.1</version>
<configuration>
<effort>Max</effort>
<threshold>Low</threshold>
<failOnError>true</failOnError>
<plugins>
<plugin>
<groupId>com.h3xstream.findsecbugs</groupId>
<artifactId>findsecbugs-plugin</artifactId>
<version>LATEST</version>
</plugin>
</plugins>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
<version>3.0.0</version>
</plugin>
<plugin>
<groupId>org.jacoco</groupId>
<artifactId>jacoco-maven-plugin</artifactId>
<version>0.8.1</version>
<configuration>
<fileSets>
<fileSet>
<directory>${project.build.directory}</directory>
<includes>
<include>*.exec</include>
</includes>
</fileSet>
</fileSets>
</configuration>
<executions>
<execution>
<id>default-prepare-agent</id>
<phase>process-classes</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/jacoco.exec</destFile>
</configuration>
</execution>
<execution>
<id>pre-integration-test</id>
<phase>pre-integration-test</phase>
<goals>
<goal>prepare-agent</goal>
</goals>
<configuration>
<destFile>${project.build.directory}/jacoco-it.exec</destFile>
<propertyName>failsafeArgLine</propertyName>
</configuration>
</execution>
<execution>
<id>post-integration-test</id>
<phase>post-integration-test</phase>
<goals>
<goal>report</goal>
</goals>
<configuration>
<dataFile>${project.build.directory}/jacoco-it.exec</dataFile>
<outputDirectory>${project.reporting.outputDirectory}/jacoco-it</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>pl.project13.maven</groupId>
<artifactId>git-commit-id-plugin</artifactId>
</plugin>
</plugins>
</build>
前端
由于需要一个可以作为 Maven 依赖项的库,我们将构建资源复制到 jar 的 public 目录,作为 maven-resources-plugin。
但是首先我们需要构建和测试这个模块。我们会使用 frontend-maven-plugin 完成这两步。如果不喜欢 maven 方式,也可以使用脚本,或者直接使用 Jenkinsfile 完成构建和测试。
<build>
<plugins>
<plugin>
<artifactId>maven-resources-plugin</artifactId>
<version>3.0.2</version>
<executions>
<execution>
<id>prepare-package</id>
<phase>prepare-package</phase>
<goals>
<goal>copy-resources</goal>
</goals>
<configuration>
<outputDirectory>${basedir}/target/classes/public</outputDirectory>
<resources>
<resource>
<directory>${project.basedir}/build</directory>
</resource>
</resources>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>com.github.eirslett</groupId>
<artifactId>frontend-maven-plugin</artifactId>
<version>1.6</version>
<executions>
<execution>
<id>install node and yarn</id>
<goals>
<goal>install-node-and-yarn</goal>
</goals>
<configuration>
<nodeVersion>v9.9.0</nodeVersion>
<yarnVersion>v1.5.1</yarnVersion>
</configuration>
</execution>
<execution>
<id>yarn</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
</configuration>
</execution>
<execution>
<id>yarn build</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>prepare-package</phase>
<configuration>
<arguments>build</arguments>
</configuration>
</execution>
<execution>
<id>test</id>
<goals>
<goal>yarn</goal>
</goals>
<phase>test</phase>
<configuration>>
<arguments>test</arguments>
<environmentVariables>
<CI>true</CI>
</environmentVariables>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
Jenkinsfile
现在我们使用 Jenkins 来完成所有构建步骤!我们将创建如下流水线:
我们使用声明式流水线。在 Build stage,我们并行构建前端和后端。
当然我们需要注意后端是依赖于前端模块产生的制品的,因此在以上两个并行的构建完成后,我们必须用另一个步骤来创建可运行的 jar,本次我们先跳过测试环节。
pipeline {
agent { label 'RHEL' }
tools {
maven 'Maven 3.3.9'
jdk 'jdk1.8.0'
}
stages {
stage('Build') {
parallel {
stage('Build Backend'){
steps {
dir('backend'){
sh 'mvn clean test spotbugs:spotbugs checkstyle:checkstyle'
}
}
post {
always {
junit 'backend/target/surefire-reports/*.xml'
findbugs canComputeNew: false, defaultEncoding: '', excludePattern: '', healthy: '', includePattern: '', pattern: '**/spotbugsXml.xml', unHealthy: ''
checkstyle canComputeNew: false, defaultEncoding: '', healthy: '', pattern: '**/checkstyle-result.xml', unHealthy: ''
jacoco()
}
}
}
stage('Build Frontend'){
steps {
dir('frontend'){
sh 'mvn clean install'
}
}
}
}
}
stage('Create runnable jar'){
steps {
dir('backend'){
sh 'mvn deploy -DskipTests'
}
}
}
}
}
你或许已经注意到了,我用的是 mvn deploy,而不是 mvn install,这是因为我们使用了 Nexus 制品仓库。Nexus 是我们唯一存储构建制品的仓库,也是我们所有环境拉取制品的地方。
制品仓库需要定义在后端的 pom.xml 文件中。
<distributionManagement>
<repository>
<uniqueVersion>true</uniqueVersion>
<id>Releases</id>
<layout>default</layout>
<url>http://nexus.edudoo.com/</url>
</repository>
<snapshotRepository>
<uniqueVersion>false</uniqueVersion>
<id>Snapshots</id>
<layout>default</layout>
<url>http://nexus.edudoo.com/</url>
</snapshotRepository>
</distributionManagement>
部署
前面提到过我们有一台 RedHat7 虚拟机且可以通过 ssh 连接。我们使用的 Ansible 工具需要 ssh 连接,所以需要安装到 Jenkins 节点上。
另一个需要决定的是如何运行我们的应用程序。我们可以通过编写 shell 脚本来启停 java jar,但更为优雅的一种方式是使用进程/服务管理器。
我们可以选择使用 Supervisor 或者其它的一些工具,但是这些工具在 RedHat Linux 上不能开箱即用,所以我们选择使用 systemd。
每次执行的步骤如下:
- 准备环境,安装所需要的包
- 准备以及推送应用的配置
- 从 Nexus 拉取 jar
- 创建(或者更新)和启动(或者重启)systemd 服务
我们所说的搭建环境是指包已更新,且安装了 java。这些定义在 common 角色中:
- name: Ensure kernel is at the latest version
yum: name=kernel state=latest
- name: Install latest Java 8
yum: name=java-1.8.0-openjdk.x86_64 state=latest
其它步骤定义在 deploy 角色中。首先,将 jar 下载到 /opt 下的目录中:
- name: Create skeleton directory
file: path=/opt/skeleton state=directory
- name: Download skeleton runnable jar
get_url:
url: http://nexus.edudoo.com/artifact/maven/content?g=com.edudoo.skeleton&a=backend&v=0.0.1-SNAPSHOT&r=snapshots
dest: /opt/skeleton/skeleton.jar
backup: yes
force: yes
配置管理部分如下所示:
- name: Ensure app is configured
template:
src: application.properties.j2
dest: /opt/skeleton/application.properties
- name: Ensure logging is configured
template:
src: logback-spring.xml.j2
dest: /opt/skeleton/logback-spring.xml
Spring boot 应用的配置在 application.properties 文件中,且和可执行 jar 放在同一目录中。修改上面 template 的内容,可以适用于不同的环境。
下面我们看一下 template 本身:
server.port={{skeleton_port}}
logging.config=/opt/skeleton/logback-spring.xml
logging.file=/opt/skeleton/skeleton.log
当运行 ansible 脚本时,skeleton_port 将被替换成指定的值。我们稍后再讲这部分。(日志配置类似于此。)
最后是 service 部分:
- name: Install skeleton systemd unit file
template: src=skeleton.service.j2 dest=/etc/systemd/system/skeleton.service
- name: Start skeleton
systemd: state=restarted name=skeleton daemon_reload=yes
Template 中现在还没有任何变量(但是可以使用例如 java 参数来动态控制内存消耗):
[Unit]
Description=Skeleton Service
[Service]
User=root
WorkingDirectory=/opt/skeleton/
ExecStart=/usr/bin/java -Xmx256m -jar skeleton.jar
SuccessExitStatus=143
TimeoutStopSec=10
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
最后用 inventory 文件(如 dev-servers)来定义环境:
[test]
11.22.33.44
[prod]
11.22.33.45
11.22.33.46
以及一个 playbook(site.yml)来定义所有的步骤:
---
- hosts: test
remote_user: clouduser
roles:
- common
- deploy
vars:
- skeleton_port: 80
我们在这里给变量 skeleton_port 指定了值,这个值将替换到 application.properties 文件的 template 中。
现在我们将 Ansible 的相关步骤加到 Jenkinsfile 中:
...
stage('Deploy to test'){
steps {
dir('deployment'){ //do this in the deployment directory!
echo 'Deploying to test'
sh 'ansible-playbook -i dev-servers site.yml'
}
}
}
...
现在我们需要把所有东西提交到一个 git 仓库,以及让 Jenkins 知道从哪儿可以获取 Jenkinsfile。
配置 Jenkins
在 Jenkins 上创建一个新的 Multibranch 类型流水线。在配置页面唯一需要配置的就是 source:
保存配置、运行,并开始享受吧!
相关代码位于 https://github.com/balazsmaria/skeleton
- 在ASP.NET Core应用中如何设置和获取与执行环境相关的信息?
- 在ASP.NET MVC中如何应用多个相同类型的ValidationAttribute?
- [ASP.NET MVC]如何定制Numeric属性/字段验证消息
- 为.NET Core项目定义Item Template
- 晚绑定场景下对象属性赋值和取值可以不需要PropertyInfo
- 一个关于反序列化的小问题
- 两个简单的扩展方法:TrimPrefix和TrimSuffix
- 谈谈Nullable<T>的类型转换问题
- ASP.NET MVC是如何运行的(3): Controller的激活
- ASP.NET MVC是如何运行的[2]: URL路由
- 一个简单的小程序演示Unity的三种依赖注入方式
- 在Entity Framework中使用存储过程(三):逻辑删除的实现与自增长列值返回
- 在Entity Framework中使用存储过程(四):如何为Delete存储过程参数赋上Current值?
- ASP.NET MVC是如何运行的(4): Action的执行
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 小知识:OGG的TRANLOGOPTIONS MINEFROMACTIVEDG参数
- oracle转postgreSQL修改点
- 重学数据结构(三、队列)
- Jmeter系列(68)- BeanShell 内置变量 prev
- 聊聊java中的哪些Map:(六)ConcurrentHashMap源码分析
- 任意文件包含漏洞的绕过方式
- XXE实体注入漏洞详解
- ent orm笔记2---schema使用(下)
- 零基础Python教程043期 列表的增删改查,彻底学通序列基本操作
- MySQL与JDBC精简笔记
- Ajax的使用
- 时间复杂度与空间复杂度
- 零基础Python教程042期 求最值?求存在性?非常实用!
- 选择排序
- LeetCode48|三数之和