本次实验涉及以下多个代码仓库:
% tree -L 1 ├── 1-cd-platform # 实验环境相关代码 ├── 1-env-conf # 环境配置代码-实现配置独立 └── 1-springboot # Spring Boot 应用的代码及其部署代码 复制代码
1-springboot 的目录结构如下:
% cd 1-springboot % tree -L 1 ├── Jenkinsfile # 流水线代码 ├── README.md ├── deploy # 部署代码 ├── pom.xml └── src # 业务代码 复制代码
所有代码,均放在 GitHub: github.com/cd-in-pract…
笔者使用 Docker Compose + Vagrant 进行实验。环境包括以下几个系统:
使用 Vagrant 是为了启动虚拟机,用于部署 Spring Boot 应用。如果你的开发机器无法使用 Vagrant,使用 VirtualBox 也可以达到同样的效果。但是有一点需要注意,那就是网络。如果在虚拟机中要访问 Docker 容器内提供的服务,需要在 DNS 上或者 hosts 上做相应的调整。所有的虚拟机的镜像使用 Centos7。
另,接下来笔者的所有教程都将使用 Artifactory 作为制品库。 在此申明,笔者没有收 JFrog——研发 Artifactory 产品的公司——任何广告费。 笔者只是想试用商业产品,以便了解商业产品是如何应对制品管理问题的。
启动 Artifactory 后,需要添加 “Virtual Repository” 及 “Local Repository”。具体请查看 Artifactory 的官方文档。如果你当前使用的是 Nexus,参考本教程,做一些调整,问题也不大。
如果想使用已有制品库,可以修改 1-cd-platform 仓库中的 settings-docker.xml 文件,指向自己的制品库。
实验环境近期的总体结构图如下:
architecture.png
之所以说是“近期的”,是因为上图与本篇介绍的结构有小差异。本篇文章还没有介绍 Nginx 与 Springboot 配置共用,但是总体不影响读者理解。
Springboot 流水线有两个阶段:
流水线的所有逻辑都写在 Jenkinsfile 文件。接下来,分别介绍这两个阶段。
此阶段核心代码:
docker.image('jenkins-docker-maven:3.6.1-jdk8') .inside("--network 1-cd-platform_cd-in-practice -v $HOME/.m2:/root/.m2") { sh """ mvn versions:set -DnewVersion=${APP_VERSION} mvn clean test package mvn deploy """ } 复制代码
它首先启动一个装有 Maven 的容器,然后在容器内执行编译、单元测试、发布制品的操作。
而 mvn versions:set -DnewVersion=${APP_VERSION}
的作用是更改 pom.xml
文件中的版本。这样就可以实现每次提交对应一个版本的效果。
注意: 这部分需要一些 Ansible 的知识。
首先看部署脚本的入口 1-springboot/deploy/playbook.yaml :
--- - hosts: "springboot" become: yes roles: - {"role": "ansible-role-java", "java_home": "{{JAVA_HOME}}"} - springboot 复制代码
先安装 JDK,再安装 Spring Boot。JDK 的安装,使用了现成 Ansible role: github.com/geerlingguy… 。
重点在 Spring Boot 部署的核心逻辑。它主要包含以下几部分:
以上步骤实现在 1-springboot/deploy/roles/springboot 中。
流水线的部署阶段的核心代码如下:
docker.image('williamyeh/ansible:centos7').inside("--network 1-cd-platform_cd-in-practice") { checkout([$class: 'GitSCM', branches: [[name: "master"]], doGenerateSubmoduleConfigurations: false, extensions: [[$class: 'RelativeTargetDirectory', relativeTargetDir: "env-conf"]], submoduleCfg: [], userRemoteConfigs: [[url: "https://github.com/cd-in-practice/1-env-conf.git"]]]) sh "ls -al" sh """ ansible-playbook --syntax-check deploy/playbook.yaml -i env-conf/dev ansible-playbook deploy/playbook.yaml -i env-conf/dev --extra-vars '{"app_version": "${APP_VERSION}"}' """ } 复制代码
它首先将配置变量仓库的代码 clone 下来,然后对 playbook 进行语法上的检查,最后执行 ansible-playbook
命令进行部署。 --extra-vars
参数的 app_version
用于指定将要部署的应用的版本。
在 1-springboot/Jenkinsfile 中实现了简易的指定版本部署。核心代码如下:
parameters { string(name: 'SPECIFIC_APP_VERSION', defaultValue: '', description: '') } 复制代码
stage("build and upload"){ // 如果不指定部署版本,则执行构建 when { expression{ return params.SPECIFIC_APP_VERSION == "" } } // 构建并上传制品的逻辑 steps{...} } 复制代码
之所以说是“简易”,是因为部署时只指定了制品的版本,并没有指定的部署逻辑和配置的版本。这三者的版本要同步,部署才真正做到准确。
所有的配置项都放在 1-env-conf 仓库中。Ansible 执行部署时会读取此仓库的配置。
将配置放在 Git 仓库中有两个好处:
有好处并不代表没有成本。那就是开发人员必须开始关心软件的配置(笔者发现不少开发者忽视配置项管理的重要性。)。
本文重点不在配置管理,后面会有文章重点介绍。
事实上,整个实验,工作量大的地方有两处:一是 Spring Boot 流水线本身的设计;二是整个实验环境的自动化。读者朋友之所以能一两条简单的命令就能启动整个实验环境,是因为笔者做了很多自动化的工作。笔者认为有必要在本篇介绍这些工作。接下来的文章将不再详细介绍。
流水线中,我们需要将制品上传到 artifactory(settings.xml 配置的仓库地址是 http://artifactory:8081 ),但是发现无法解析 host。这是因为流水线中的 Docker 容器所在网络与 Docker compose 创建的网络不同。所以,解决办法就是让流水线中的 Docker 容器加入到 Docker compose 的网络。
具体解决办法就是在启动容器时,加入参数: --network 1-cd-platform_cd-in-practice
在没有做任何设置的情况启动 Jenkins,会出现一个配置向导。这个过程必须是手工的。笔者希望这一步也是自动化的。Jenkins 启动时会执行 init.groovy.d/
目录下的 Groovy 脚本。
http://artifactory 部署在 Docker 容器中。Spring Boot 应用的制品要部署到虚拟机中,需要从http://artifactory 中拉取制品,也就是要在虚拟机里访问容器里提供的服务。虚拟机与容器之间的网络是不通的。那怎么办呢?笔者的解决方案是使用宿主机的 IP 做中转。具体做法就是在虚拟机中加一条 host 记录:
machine.vm.provision "shell" do |s| s.inline = "echo '192.168.52.1 artifactory' >> /etc/hosts" end 复制代码
以上是使用了 Vagrant 的 provision
技术,在执行命令 vagrant up
启动虚拟机时,就自动执行那段内联 shell。 192.168.52.1
是虚拟宿主机的 IP。所以,虚拟机里访问 http://artifactory:8081 时,实际上访问的是 http://192.168.52.1:8081 。
网络结构可以总结为下图:
network.png