first commit

This commit is contained in:
张乾
2024-10-16 06:37:41 +08:00
parent 633f45ea20
commit 206fad82a2
3590 changed files with 680090 additions and 0 deletions

View File

@@ -0,0 +1,335 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
01.Jenkins的安装配置与日常维护
Jenkins 安装与系统管理
本章节作为<<使用Jenkins持续交付与部署>>系列文章的第一篇主要介绍一下持续集成工具Jenkins的安装配置以及日常维护Jenkins的一些系统管理说明。本次持续交付/持续部署教程主要是针对Jenkins以及与之集成的一些开源工具进行扩展说明的下面开始本章的内容介绍。
本小节课程主要涉及到的内容如下:
安装Jenkins
Jenkins 系统管理与基础配置
介绍
在开始正式安装Jenkins之前首先需要了解一下Jenkins是什么。
[Jenkins](https://jenkins.io/zh)是一款由Java编写的开源的可扩展持续集成工具。允许持续集成和持续交付项目并通过各种扩展插件可以处理任何类型的集成构建或部署操作。
Jenkins 提供了软件开发的持续集成服务。当前持续集成(CI)已成为许多开发团队在整个软件生命周期内保证代码质量的必要流程。它是一种实践旨在缓和和稳固软件的构建过程而Jenkins使持续集成变成可能。它的主要目标是监控软件开发流程快速定位并报告问题。丰富而众多的扩展插件使得Jenkins能够帮助研发、运维、QA团队在软件测试接口测试、单元测试等、软件构建maven、ant、gradle、软件部署shell、docker、ansible等等等CI/CD整个环节进行良好的协作提高工作效率。
安装
简单了解了jenkins是什么接下来安装一下Jenkins。Jenkins的安装相对简单可以通过war包、rpm包、Docker等方式安装这里简单介绍两种安装方式虽然安装方式不一样但访问方法和配置都是一样的所以凭个人喜好及实际情况选择安装方式。
使用rpm方式安装
使用rpm安装首先要确保已经安装好jdk1.8+使用最新版本时如果jdk的版本低于1.8对于jenkins的使用没有问题但是在后续使用某个插件的时候可能会出现问题。
配置jdk
去 jdk官网 下载jdk1.8
$ tar xf jdk-8u231-linux-x64.tar.gz -C /usr/local/
# /etc/profile 添加如下内容
export JAVA_HOME=/usr/local/jdk1.8.0_231
export JRE_HOME=${JAVA_HOME}/jre
export CLASSPATH=.:${JAVA_HOME}/lib:${JRE_HOME}/lib
export PATH=${JAVA_HOME}/bin:$PATH
$ source /etc/profile
$ java -version
java version "1.8.0_231"
Java(TM) SE Runtime Environment (build 1.8.0_231-b11)
Java HotSpot(TM) 64-Bit Server VM (build 25.231-b11, mixed mode)
配置好jdk后安装Jenkins。
#去官网下载最新的jenkins rpm包
$ wget https://pkg.jenkins.io/redhat-stable/jenkins-2.204.2-1.1.noarch.rpm
# 下载完成后安装
$ yum localinstall jenkins-2.204.2-1.1.noarch.rpm
# 启动
$ systemctl start jenkins
使用docker启动
$ docker run -u root -it -d --name jenkins -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home jenkins/jenkins
获取镜像可以从docker hub搜索关键字获取。
第一次启动时会拉取镜像。
$ docker run -u root -it -d --name jenkins -p 8080:8080 -p 50000:50000 -v jenkins-data:/var/jenkins_home jenkins/jenkins
Unable to find image 'jenkins/jenkins:latest' locally
latest: Pulling from jenkins/jenkins
3192219afd04: Pull complete
17c160265e75: Pull complete
cc4fe40d0e61: Pull complete
9d647f502a07: Pull complete
d108b8c498aa: Pull complete
1bfe918b8aa5: Pull complete
dafa1a7c0751: Pull complete
78d3391dc013: Pull complete
c1c87cf7f0bf: Pull complete
4bfacf44cce2: Pull complete
9eaefa421a64: Pull complete
043144b011c9: Pull complete
3d9c8a4a5cb8: Pull complete
05fca0659bbd: Pull complete
14241c82960f: Pull complete
41a13492424a: Pull complete
d7aa64814e63: Pull complete
19049899ad6a: Pull complete
3b23f8338127: Pull complete
Digest: sha256:676448a326f96a991d2d32ffbe52f239c0d2c40de3538af2ae6f18d88bf3cb56
Status: Downloaded newer image for jenkins/jenkins:latest
11f86c1472fc4d89e1af5c9340f68e8318b6b7322d46abe30bff23f092a62fe5
# 查看启动的容器
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
11f86c1472fc jenkins/jenkins "/sbin/tini -- /usr/…" 6 minutes ago Up 6 minutes 0.0.0.0:8080->8080/tcp, 0.0.0.0:50000->50000/tcp jenkins
容器启动时映射了8080和50000端口其中
8080端口是web访问的端口。
50000端口是基于JNLP的Jenkins代理slave通过TCP与Jenkins master 进行通信的端口。
-v jenkins-data:/var/jenkins_home 为挂载的volume卷将容器内的/var/jenkins_home目录挂载到jenkins-data卷这个卷如果不存在启动容器的时候会自动创建也可以通过docker volume create jenkins-data命令手动创建或者映射到宿主机指定的目录。
容器启动成功后通过访问IP:8080如下Jenkins界面
提示管理员秘密从/var/jenkins_home/secrets/initialAdminPassword路径获取(对于使用war包或者rpm包安装的Jenkins路径可能会有所差异)如果使用rpm方式或者war包安装直接在jenkins所在服务器上cat一下该文件即可如果是docker方式安装可根据上面获取的Container_ID或者Container_name执行docker命令
$ docker exec -it jenkins cat /var/jenkins_home/secrets/initialAdminPassword
1dc17f65d83c4c56b13cbae230b5e7c6
获取到密码,密码输入成功后:
选择安装所需插件gradlemavenant……同样也可忽略这一步后面有需要再安装所需插件由于网络原因安装时间可能有点长需要耐心等待或者参考下面的”修改插件更新源”。然后设置好管理员用户名和密码登录即可下面就是安装好后的jenkins界面。
到这里 jenkins 就安装完成。
Jenkins基础配置
系统管理
安装好Jenkins后首先来看一下Jenkins中的系统管理Manage Jenkins面板。系统管理面板是整个Jenkins管理、配置、维护的入口。该面板下包含多个菜单项默认包括如下
Configure System系统配置该菜单用来设置Jenkins的全局配置以及各种工具插件的全局配置包括jenkins的工作目录、可同时构建的job的数量、jdk环境变量等等。
configure Global Security全局工安全配置Jenkins的全局安全配置入口可以配置用户权限、API Token、agent代理等信息。
Configure Credentials(凭据配置):该菜单项用来创建各种类型的凭据,用于对系统或者服务进行认证,比如登录服务器,登录源码仓库的认证等。
Global Tool Configuration(全局工具配置)该菜单项用来对Jenkins项目里使用到的全局工具进行配置比如Jdk的配置代码编译工具 maven/ant/gradle工具的配置代码拉取命令git的配置等等。
Manage Plugins插件管理Jenkins各种插件得入口用来对插件进行增删更新等操作jenkins强大的功能和扩展离不开各种插件的支持所以在初期这是一个使用比价频繁的菜单。
System Information系统信息该菜鸟用来显示Jenkins系统的各种属性信息包括jenkins所在系统的配置信息jenkins环境变量信息jenkins所安装的插件信息等。
System Log系统日志用来展示jenkins的系统日志。
Jenkins CliJenkins命令行该菜单项介绍了如何使用jenkins-cli.jar在命令行操作jenkins这个菜单提供了大量可以在命令行操作jenkins的命令以及使用说明比如重启jenkins可以使用如下方式。
java -jar jenkins-cli.jar -s ${jenkins_url} restart
Manage Nodes(管理节点)该菜单项用于管理jenkins的节点信息比如增加或减少jenkins slave节点修改master或者slave节点的配置等如何配置将在后面章节介绍。
Manage Users用户管理该菜单项用于管理jenkins的系统用户信息包括用户信息的增删改查等操作。
上面列出了jenkins的一些常用的配置菜单在使用jenkins的时候对于上面的菜单或多或少的都会接触到并且在以后的学习中会对某个菜单里的使用进行详细的配置说明这里先不多做介绍当然如果你有些迫不及待了也可以自己试一下。
配置文件
了解了Jenkins的面板配置再来看一下jenkins的配置文件。Jenkins安装好以后使用的默认端口为8080通过ip加端口访问jenkins面板点击”Manage Jenkins”>”Configure System“进入系统配置界面可以看到jenkins的默认配置比如下图所示
通过该图可以看到jenkins的主目录为/var/lib/jenkins该目录为Jenkins的实际工作目录对于jenkins的配置修改以及存放jenkins项目配置的文件都会放到此目录下 并不是运行Jenkins程序的目录。由上图发现该目录并不能在jenkins ui界面修改对于要想修改Jenkins的工作目录路径的需求应该怎么实现呢或者如果想要修改jenkins的启动端口jvm内存等又要如何修改呢本小节就简单介绍一下如何修改这些配置。
对于使用Tomcat方式启动的Jenkins服务直接修改tomcat的端口和jvm内存设置即可。有关工作目录的修改大体就与使用rpm安装的jenkins服务相同了建议保持默认即可对于使用容器镜像方式启动的jenkins服务如何配置可参考docker hub仓库下jenkins镜像的Dockerfile 。
所以有关使用Tomcat方式或者容器化安装的Jenkins服务配置文件的说明这里不多说主要介绍一下使用rpm方式安装的jenkins的配置文件的管理。
我这里使用rpm方式安装的jenkins服务默认启动命令为systemctl start jenkins以centos7为例但是在系统默认存放启动文件的目录有没有找到与jenkins有关的service文件该如何下手呢通过systemctl status jenkins命令可以获取jenkins启动的脚本文件比如下面示例
找到了该启动文件,首先看一下该文件的内容。
$ cat /etc/rc.d/init.d/jenkins |grep -v "^#\|^$"|grep "^JENKINS\|^PARAMS"
JENKINS_WAR="/usr/lib/jenkins/jenkins.war"
JENKINS_CONFIG=/etc/sysconfig/jenkins
JENKINS_PID_FILE="/var/run/jenkins.pid"
JENKINS_LOCKFILE="/var/lock/subsys/jenkins"
PARAMS="--logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war --daemon"
说明
我这里通过grep将所需要的内容过滤了出来如果想要看全部内容可以将过滤条件去掉。
然后通过ps命令查看一下Jenkins的进程信息。
$ ps -ef|grep jenkins
root 15655 1 2 Apr15 ? 00:31:11 /usr/local/jdk1.8.0_231/bin/java -Djava.awt.headless=true -DJENKINS_HOME=/var/lib/jenkins -jar /usr/lib/jenkins/jenkins.war --logfile=/var/log/jenkins/jenkins.log --webroot=/var/cache/jenkins/war --httpPort=8080 --debug=5 --handlerCountMax=100 --handlerCountMaxIdle=20
由上面信息可以看到jenkins服务主要配置文件为/etc/sysconfig/jenkins使用的 java为自定义的/usr/local/jdk1.8.0_231/bin/java启动方式也是通过war包启动日志文件存放在/var/log/jenkins/jenkins.log文件里jenkins服务运行所需要的程序文件放到了/var/cache/jenkins/war目录。当然这些配置的路径都是可以修改的。
了解完基本配置信息下面看一下jenkins的主配置文件。
$ cat /etc/sysconfig/jenkins|grep -v "^#\|^$"
JENKINS_HOME="/var/lib/jenkins"
JENKINS_JAVA_CMD=""
JENKINS_USER="root"
JENKINS_JAVA_OPTIONS="-Djava.awt.headless=true"
JENKINS_PORT="8080"
JENKINS_LISTEN_ADDRESS=""
JENKINS_HTTPS_PORT=""
JENKINS_HTTPS_KEYSTORE=""
JENKINS_HTTPS_KEYSTORE_PASSWORD=""
JENKINS_HTTPS_LISTEN_ADDRESS=""
JENKINS_HTTP2_PORT=""
JENKINS_HTTP2_LISTEN_ADDRESS=""
JENKINS_DEBUG_LEVEL="5"
JENKINS_ENABLE_ACCESS_LOG="no"
JENKINS_HANDLER_MAX="100"
JENKINS_HANDLER_IDLE="20"
JENKINS_EXTRA_LIB_FOLDER=""
JENKINS_ARGS=""
在这里就可以看到jenkins的详细配置了包括Jenkins的主要工作目录Jenkins的端口运行Jenkins的用户默认为jenkins以及jenkins启动时的java参数等。如果要修改jenkins的jvm参数只需要将配置放到JENKINS_JAVA_OPTIONS参数里即可多个参数使用空格隔开修改完配置文件不要忘了重启Jenkins服务。
Jenkins汉化
大部分版本的Jenkins安装好以后默认的语言为英文有些同学可能不太习惯英文版的jenkins这时就需要对jenkins进行汉化汉化比较简单下面进行配置。
”Manage Jenkins”—> “Manage Plugins“在可选插件中找到Locale plugin插件点击安装即可。安装完以后在”Config System“菜单中找到Locale选项配置设置”Default Language“为zh_CN并勾选Ignore browser preference and force this language to all users
如下图所示:
配置好后重启Jenkins从新登录界面就发现英文菜单变成了中文菜单。
Jenkins针对不同的安装方式或者不同的版本汉化方法可能不一样大多数版本使用上面的方法就能汉化成功。但是有的版本安装该插件后重启jenkins后发现面板还是英文的汉化不成功。那么接下来就需要在安装Localization: Chinese (Simplified插件了安装好后重启Jenkins即可。如果汉化完发现面板有一部分是中文一部分是英文的话可能与jenkins的版本或者安装方式有关了。
修改插件更新源
Jenkins强大的功能和扩展离不开插件的支持。在安装jenkins时或者安装完jenkins以后需要安装很多必须的插件使用官方默认的jenkins插件源安装插件时要么速度很慢要么安装失败使得无论是工作效率还是学习效率都大大降低为了解决此问题需要修改一下jenkins下载插件的镜像源。
进入”Manage-Jenkins”>”Manage Plugins” —> “高级“页面网上大多数方法都是修改该页面下的”升级站点“下URL输入框的值改为清华源或者Jenkins插件中心国内源。如下图所示-
然而修改完成后下载镜像还是很慢这是因为Jenkins通过解析update-center.json 文件的方式来获取插件版本以及下载插件但是jenkins使用私钥来给update-center.json文件做了签名只有通过了公钥验证的update-center.json文件才会被使用。所以还需要替换Jenkins中使用的秘钥和私钥文件。
替换证书文件
到https://github.com/jenkins-zh/mirror-adapter/rootCA下载mirror-adapter.crt文件放到/var/cache/jenkins/war/WEB-INF/update-center-rootCAs目录下即可。
对于该配置流程Jenkins中文社区帮大家把秘钥和地址的问题解决了可以使用如下方式
在jenkins面板的右下角点击Jenkins中文社区在跳转的界面会有”更新中心镜像设置“。如下所示-
说明:
先点击使用,然后在点击”设置更新中心地址“,在跳转的页面输入上面给出的地址即可。
如果没有上面的Jenkins中文社区按钮可能是你没有汉化导致的。
密码修改
在使用Jenkins的时候常常遇到了忘记Jenkins管理员登录密码的情况如果忘记了密码怎么办可以修改Jenkins的配置文件${JENKINS_HOME}/config.xml找到
<useSecurity>true</useSecurity>
将true 改为false然后重启jenkins。
Jenkins重启后访问Jenkins 就可以直接跳过验证直接跳转到Jenkins面板界面了。然后点击”Manage Jenkins”>”Config Global Security“勾选“启用安全”在”访问控制“选项的”安全域“属性选中”Jenkins own user database“授权策略属性选中”Logged-in users can do anything“。如下如所示-
保存后到”Manage Users“界面重置管理员用户密码即可。
备份与恢复
使用jenkins另一个必不可少的配置就是对jenkins的备份。Jenkins的所有的数据都是以文件的形式存放在${JENKINS_HOME}目录中。所以不管是迁移还是备份,只需要将${JENKINS_HOME}打包后在拷贝,然后将打包的文件解压到新的${JENKINS_HOME}目录就行了。备份jenkins的方法有很多种本文就简单介绍一下常用的备份方法使用ThinBackup插件。
在插件管理页面找到ThinBackup插件安装即可如果你不会安装看一下第二篇的内容吧。安装完以后在manage jenkins界面会出现ThinBackup菜单项点击进去后进入settings设置界面进行备份设置。如下所示设置
说明:
Backup directory备份目录用于存储备份的文件如果指定的目录不存在默认在进行备份之前会自动创建。
Backup schedule for full backups进行备份的计划任务与linux下的crontab一样其中上面的H代表哈希为了允许定期调度的任务在系统上产生均匀负载应尽可能使用符号H(用于“散列”)。例如如果多个job都设置使用0 0 * * *将导致负载大幅飙升。相比之下使用H H *仍然会每天执行一次每个job但不会同时执行更好地使用有限的资源。
Backup schedule for differential backups进行差异化备份的计划任务同上。
Max number of backup sets备份的最大数量。
Files excluded from backup (regular expression):不需要进行备份的文件的正则表达式。
Wait until Jenkins/Hudson is idle to perform a backup等待jenkins空闲多长时间后进行备份。
Backup build results如果启用此选项还将备份构建结果一般不建议勾选此选项。
Backup userContent folder备份${jenkins_home}/userContent目录下的文件。
Backup next build number file备份jenkins构建的build id文件。
Backup plugins archives备份插件。
Clean up differential backups完成备份以后清除所有的差异备份。
Move old backups to ZIP files如果选中此选项则无论何时执行新的完整备份所有旧备份集都将移至ZIP文件。每个ZIP文件将包含一个备份集即一个完整备份和任何引用该备份文件的差异备份。文件名将标识包含备份的时间范围即完整备份的时间戳和最新差异备份的时间戳
备份策略根据自己实际情况定义即可。
保存后回到ThinBackup界面除了可以自动进行备份外也可以通过手动备份点击Backup Now按钮就会立即进行备份。如下按钮
备份完后会在上面设定的备份目录下生成一个以FULL-开头以当前时间精确到分为结尾的目录该目录下就是默认所有的Jenkins配置。
当要使用备份文件进行恢复的时候点击restore按钮在跳转的界面中就会显示出该备份文件的备份时间点击Restore即可。
总体来说,使用该插件备份相对简单,这里就不在多说,有兴趣的可以自己试一下。

View File

@@ -0,0 +1,522 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
02.Jenkins强大的插件功能
在上一节的章节中简单介绍了一下Jenkins的安装以及一些与jenkins系统管理有关的配置说明。其中有提到过jenkins强大的功能离不开各种插件的支持所以本节就先来介绍一下本系列教程要用的到一些公共插件以及相关的配置说明。
安装好Jenkins后开始进行配置首先安装插件本次教程用到如下公共插件
Maven Integration plugin用于创建maven项目的插件
Pipeline用于创建pipeline项目的插件
Ansible Plugin用于使用ansible插件
Git/Git Parameter用于拉取git代码的插件
Docker Pipeline用于在Pipeline中使用与Docker相关的语法插件
SonarQube Scanner用于执行Sonarqube scan的插件
Publish Over SSH通过ssh协议发送文件到远程服务器
Email Extension Template用于发送邮件给特定的人或组
Role-based Authorization Strategy用户权限管理插件
回到首页点击”manage Jenkins管理jenkins -> Manage Plugins插件管理” 菜单找到插件选中install即可。
比如安装ansible插件。
搜索框输入关键字就会列出包含该关键字的插件由于我已经安装了ansible的插件所以这里就不会列出已安装的。
配置全局工具
JDK
回到首页点击”manage Jenkins -> Global Tool Configuration全局工具配置” 菜单,
首先看到Jdk工具配置在上一节的文章中已经配置好了jdk在这里直接配置引入即可如下所示。
Maven
继续往下浏览会发现有maven工具的配置由于还没有安装这里先配置一下。
到apache官网下载根据个人需要选择指定的版本下载
$ wget https://downloads.apache.org/maven/maven-3/3.6.3/source/apache-maven-3.5.4-src.tar.gz
解压后直接放到指定目录,然后在/etc/profile 添加环境变量配置,比如我的配置如下。
$ tar xf apache-maven-3.5.4-src.tar.gz
$ vi /etc/profile
export M2_HOME=/opt/apache-maven-3.5.4
export PATH=$PATH:$M2_HOME/bin
$ source /etc/profile
验证。
$ mvn -version
Apache Maven 3.5.4 (1edded0938998edf8bf061f1ceb3cfdeccf443fe; 2018-06-18T02:33:14+08:00)
Maven home: /opt/apache-maven-3.5.4
Java version: 1.8.0_231, vendor: Oracle Corporation, runtime: /usr/local/jdk1.8.0_231/jre
Default locale: en_US, platform encoding: UTF-8
OS name: "linux", version: "3.10.0-1062.4.1.el7.x86_64", arch: "amd64", family: "unix"
然后在jenkins中添加maven工具的配置信息。
需要注意的是:
maven编译代码时默认${Maven_HOME}/conf/路径下的settings.xml文件配置了读取存储公共依赖jar包的仓库地址默认是阿里云的仓库以及用户名和密码所以如果有用其它地址的仓库比如私有nexus需要修改该配置文件。
关于Jenkins配置maven集成的内容到此就完成了还是比较简单的。
Email Extension Template 插件
Email Extension Template插件用来在执行job的时候发送email给要通知的用户或者组。
配置Jenkins Location和Extended E-mail Notification其中系统管理员邮件地址一定要和User Name值一致。如下所示。
-
配置默认的邮件主题以及邮件内容
该插件下的”Default Subject“和”Default Content“参数分别用来设置发送邮件的主题和内容默认的配置如下所示。
这两个参数的内容均使用了jenkins内置的全局变量其中$PROJECT_NAME表示项目的名称$BUILD_NUMBER表示项目构建的ID$BUILD_STATUS表示项目构建状态。如果想要自定义邮件的主题和内容直接在这里修改即可。
配置好以后点击”Test configuration“测试一下。-
用户权限控制
在使用Jenkins的时候也经常会遇到给不同的用户分配不同的权限的问题Jenkins的权限管理通过”Role-based Authorization Strategy“插件进行管理。如何安装该插件不在演示下面看一下如何开启该插件。
点击”Manage Jenkins”>”Config Global Security“在”访问控制“选项的”授权策略“中选择”Role-Based Strategy“保存即可。保存退出后在”manage Jenkins“中会出现Manager and Assign Roles选项该选项用于管理并配置用户权限。
”Manage and Assign Roles“选项下有三个菜单如下所示。
该插件的使用流程大概如下。
通过Mnaage Roles选项创建角色可以是全局角色也可以是项目角色。如果是项目角色则可以通过正则语法给相应的项目角色创建匹配规则使该角色对某些项目有管理权限。
通过Assign Roles选项分配角色分配角色的主体为Jenkins系统里的用户可以通过“Manage Users”菜单查看到。
创建角色
点击Manage Roles点击进去以后会发现有一个默认的admin角色该角色为具有管理员权限的用户角色。
创建一个全局角色,一个项目角色,如下所示。
在Role to add输入框中输入角色名称自定义然后在Pattern中输入要管理的项目名称或者要匹配的多个jeknkin项目job的正则语法比如我这里写的是test-.*表示匹配以test-开头的所有项目。添加以后点击”Pattern“框中匹配项就可以显示该正则匹配到的项目名称了。然后在相应的权限配置栏中勾选该role的权限比如对于凭据有什么权限对于job有什么权限等这里就需要根据自己的实际情况勾选了。
分配角色
创建好角色以后就可以分配角色了分配角色给谁呢这就需要看jenkins里存在的用户了。点击”Manage-Jenkins”>”Manage Users“可以看到jenkins系统里所有的用户。然后回到”Manage and Assign Roles“菜单点击”Assign Roles“在跳转的界面中就可以看到刚刚创建的角色。同样的既可以给用户分配全局角色也可以给用户分配项目角色。
如下图所示。
然后就可以使用test-li用户登录了。需要注意的是该用户需要在Jenkins中存在。
此时用“test-li”账户登录发现没有权限提示如下错误。
这是因为在创建角色的时候没有给用户创建一个对Jenkins面板具有查看权限的全局角色所以此时还需要去“Manage Roles”菜单里添加一个具有全局权限的角色如下所示
说明:
创建一个名称为test-all的全局角色该角色的权限为Overall/read对于Jenkins面板有读取权限。该角色的凭据栏和agent栏都没有勾选表示对Jenkins系统管理的凭据node节点等都没有权限。
test-all 角色对Jenkins面板具有读取权限保存后再去从新分配权限如下所示
给test-li用户分配一个具有可以读取jenkins面板的权限这样就可以使用该用户进行正常工作了。如下所示
有关用户权限的分配说明就这里,所有的角色创建与权限分配大致就是这样一个流程。
Sonar
配置完公共插件下面来介绍一下常用的持续交付过程中的使用代码质量检测插件Sonar
介绍
Sonar是一个用于代码质量管理的开源平台可以从多个维度管理和检测源代码的质量。通过插件形式可以支持包括java,C#,C/C++,PL/SQL,Cobol,JavaScrip,Groovy等二十几种编程语言的代码质量管理与检测。sonar也是当下众多公司使用的一款代码质量分析工具。
sonar从如下几个维度检测代码质量
sonar通过PMDCheckStyleFindbugs等代码规则检测工具规范代码编写
sonar也可以通过PMDCheckStyleFindbugs等代码规则检测工具检测出潜在的缺陷
糟糕的复杂度分布文件、类、方法等,如果复杂度过高将难以改变,这会使得开发人员难以理解它们,且没有自动化的单元测试,对于程序中的任何组件的改变都将可能导致需要全面的回归测试;
sonar可以展示源码中重复严重的地方
注释不足或者过多没有注释将使代码可读性变差,特别是当不可避免出现人员变动时,程序的可读性大幅度下降,而过多的注释又会使得开发人员将精力过多的花费在阅读注释上,亦违背初衷;-
sonar可以很方便地统计并展示单元测试覆盖率-
通过sonar可以找出循环展示包与包、类与类之间相互依赖关系可以检测自定义的架构规则 可以管理第三方的jar包可以利用LCOM4检测单个任务规则的应用情况检测耦合。
sonar执行流程
开发人员将他们的代码提交到代码管理平台中SVNGIT等
持续集成工具自动触发构建调用SonarScanner对项目代码进行扫描分析。
分析报告发送到SonarQube Server中进行加工。
SonarQube Server 加工并且保存分析报告到SonarQube Database中通过UI显示分析报告。
sonar环境需求
SonarQube服务实例需要至少2GB的RAM才能有效运行并且1GB的可用RAM用于操作系统。需要的磁盘空间量取决于所使用SonarQube分析的代码量。SonarQube必须安装在具有良好读写性能的硬盘上。sonar 的data文件夹包含Elasticsearch索引当服务器启动并运行时将在其上完成大量I/O操作. 因此良好的读写硬盘性能将对整个SonarQube服务性能产生很大影响。
安装
sonar安装的方式也比较多当然最简便的就是使用docker了所以本次安装使用docker方式下面看详细过程。
安装sonar需要安装数据库这里使用mysql5.7。
下载mysql:5.7镜像。
docker pull mysql:5.7
启动容器
$ docker run --name mysql5.7 -v /data/mysql5.7-data:/var/lib/mysql -p 3306:3306 -e MYSQL_ROOT_PASSWORD=123456 -d mysql:5.7
说明:
/data/mysql5.7-data为挂载的宿主机目录
进入容器中创建数据库与用户。
$ docker exec -it mysql5.7 bash
root@60253d27a5f5:/# mysql -uroot -p123456
......
mysql>
# 创建数据库db_sonar
mysql> create database db_sonar character set utf8 collate utf8_general_ci;
Query OK, 1 row affected (0.00 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
#创建数据库用户用户名密码都为sonar
mysql> grant all privileges on db_sonar.* to 'sonar'@'%'identified by 'sonar' with grant option;
Query OK, 0 rows affected, 1 warning (0.00 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
......
有关sonar使用mysql数据库的配置就完成了。
启动sonar容器
下载 sonarqube:6.7.5 镜像。
docker pull sonarqube:6.7.5
启动sonarqube容器。
$ docker run -d --name sonar --link mysql5.7 -p 9000:9000 -p 9092:9092 -v /data/sonar/conf:/opt/sonarqube/conf -v /data/sonar/data:/opt/sonarqube/data -v /data/sonar/logs:/opt/sonarqube/logs -v /data/sonar/extensions:/opt/sonarqube/extensions -e "SONARQUBE_JDBC_USERNAME=sonar" -e "SONARQUBE_JDBC_PASSWORD=sonar" -e "SONARQUBE_JDBC_URL=jdbc:mysql://mysql5.7:3306/db_sonar?useUnicode=true&characterEncoding=utf8&rewriteBatchedStatements=true&useConfigs=maxPerformance&useSSL=false" sonarqube:6.7.5
说明:
--link 表示sonar容器要连接到名称为mysql5.7容器,在配置数据库连接的时候,数据库地址可以直接写容器的名称
-v 挂载相关配置到 /data/sonar/ 目录下
-e 指定sonar启动时连接数据库的相关参数配置
待容器全部启动成功后查看
$ docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
120cb90c2248 sonarqube:6.7.5 "./bin/run.sh" 5 weeks ago Up 2 seconds 0.0.0.0:9000->9000/tcp, 0.0.0.0:9092->9092/tcp sonar
60253d27a5f5 mysql:5.7 "docker-entrypoint..." 5 weeks ago Up 8 minutes 0.0.0.0:3306->3306/tcp, 33060/tcp mysql5.7
根据 http://ip:9000 访问web界面
到此sonar安装完成。
配置
打开web界面点击login in
默认账号密码都是admin首次登录后会有一个使用教程根据提示创建token。
随便输入一个比如我输入的adminlanguage为javabuild technolog为maven如下所示。
点击finish即可。
点击finish以后跳转到首页就会看到一个英文版的主页了如果不习惯也可以汉化。
汉化
点击顶层菜单”Administration””marketplace“在下面箭头处搜索“chinese”出现如下插件点击“install”
稍等一会,如果安装不成功,报如下错误:
可能的原因是插件版本与sonar版本不兼容可以去sonar官方github下载合适的汉化插件比如我的版本是6.7.5在github上发现sonar-l10n-zh-plugin-1.19该版本支持,下载放到宿主机/data/sonar/extensions/plugins 目录后重启sonar容器即可。
$ pwd
/data/sonar/extensions/plugins
$ wget https://github.com/SonarQubeCommunity/sonar-l10n-zh/releases/download/sonar-l10n-zh-plugin-1.19/sonar-l10n-zh-plugin-1.19.jar
$ docker restart sonar
说明:
在部署sonar时挂载了宿主机目录所以将插件放到宿主机目录即可。
启动完成后,从新打开页面,到应用市场发现新安装的插件。-
到此sonar的配置基本就告一段落接下来找一段代码测试一下sonar是否好用。
测试
这里以扫描maven项目为例进入到项目工程根目录下执行扫描命令为
$ mvn sonar:sonar -Dsonar.host.url=http://sonar_ip:9000 -Dsonar.login=7e73499a981bf49e2f724d11479623a00cf97f1a
说明:
sonar.login指定的值为刚登陆时生成的token
token如果忘了怎么办从新生成一个即可如下所示先回收在重新生成
执行成功后,部分日志如下
66230 [INFO] Analysis report generated in 305ms, dir size=790 KB
66816 [INFO] Analysis reports compressed in 584ms, zip size=408 KB
67682 [INFO] Analysis report uploaded in 865ms
67686 [INFO] ANALYSIS SUCCESSFUL, you can browse http://192.168.176.160:9000/dashboard/index/com.hivescm:fw-base-nop
67687 [INFO] Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report
67687 [INFO] More about the report processing at http://192.168.176.160:9000/api/ce/task?id=AXBJZsjiw7C6SYqgng-6
67816 [INFO] Task total time: 44.679 s
68175 [INFO] ------------------------------------------------------------------------
68176 [INFO] BUILD SUCCESS
界面如下。
到此步骤sonar基本可以用了。最基本的扫描完成了还需要对代码进行分析这就需要其他插件进行了用的比较多的当然是SonarQube Scanner 了,下面进行安装配置。
SonarQube Scanner 代码分析
去官网下载所需要的安装包解压放到指定的目录即可需要说明的是建议将scanner放到与Jenkins同主机因为需要在jenkins中配置即成scanner然后添加环境变量比如我的如下
$ cat /etc/profile
PATH=$PATH:/opt/sonar-scanner-4.2.0/bin
export PATH
$ source /etc/profile
然后修改全局配置
$ cat sonar-scanner-4.2.0/conf/sonar-scanner.properties
#Configure here general information about the environment, such as SonarQube server connection details for example
#No information about specific project should appear here
#----- Default SonarQube server
sonar.host.url=http://192.168.176.160:9000
#----- Default source code encoding
#sonar.sourceEncoding=UTF-8
环境变量生效后,检查配置是否成功
$ sonar-scanner -h
INFO:
INFO: usage: sonar-scanner [options]
INFO:
INFO: Options:
INFO: -D,--define <arg> Define property
INFO: -h,--help Display help information
INFO: -v,--version Display version information
INFO: -X,--debug Produce execution debug output
配置成功后,找一个项目测试一下。
测试
测试方式有两种,一种是直接在命令行加参数执行,一种是使用在项目目录创建配置文件的方式执行,本次测试使用第二种
在项目根目录下创建配置文件sonar-project.properties部分参数未列出
$ cat sonar-project.properties
# must be unique in a given SonarQube instance
sonar.projectKey=fw-base-nop
# this is the name and version displayed in the SonarQube UI. Was mandatory prior to SonarQube 6.1.
sonar.projectName=fw-base-nop
sonar.projectVersion=1.0
# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows.
# This property is optional if sonar.modules is set.
sonar.sources=.
sonar.java.binaries=./target/classes
# Encoding of the source code. Default is default system encoding
sonar.sourceEncoding=UTF-8
说明:
sonar.projectKey :项目名称,每个项目里必须是唯一的
sonar.projectName项目名称在sonarqube web界面会显示此名称
sonar.projectVersion项目版本
sonar.sources :需要扫描的项目目录位置
sonar.host.url sonar服务访问的url地址此处未指定配置文件中已经指定
sonar.login 令牌名称对应的token此处也没有指定
sonar.java.binaries 项目编译目录java为例则为class文件所在的目录
sonar.sourceEncoding指定的字符集
更多与参数有关的内容可参考这里
执行成功后部分结果如下所示
......
WARN: This may lead to missing/broken features in SonarQube
INFO: 37 files had no CPD blocks
INFO: Calculating CPD for 80 files
INFO: CPD calculation finished
INFO: Analysis report generated in 702ms, dir size=541 KB
INFO: Analysis reports compressed in 447ms, zip size=259 KB
INFO: Analysis report uploaded in 480ms
INFO: ANALYSIS SUCCESSFUL, you can browse http://192.168.176.160:9000/dashboard/index/fw-base-nop
INFO: Note that you will be able to access the updated dashboard once the server has processed the submitted analysis report
INFO: More about the report processing at http://192.168.176.160:9000/api/ce/task?id=AXBMjjIlw7C6SYqgng_B
INFO: Task total time: 14.546 s
INFO: ------------------------------------------------------------------------
INFO: EXECUTION SUCCESS
INFO: ------------------------------------------------------------------------
INFO: Total time: 17.906s
INFO: Final Memory: 14M/60M
然后会在sonar web界面出现一个新的项目如下-
到此关于sonar的基本配置已经完成但是本次虽然测试成功了但所有的操作还都是在shell bash中所以接下来要配置与Jenkins的集成。
jenkins集成sonar-scanner配置
Jenkins与sonar和scanner集成的插件在文章的开头已经安装这里直接演示如何配置。
点击菜单”manage jenkins管理jenkins--> configure system (系统配置)“找到”SonarQube servers“配置参数配置与sonarqube的连接如下所示
其中红色框内为jenkins连接登陆sonarqube的token凭据ID共有两种方法创建该凭据第一种在此处点击”Add“选中jenkins在弹出的框中
Kind参数处选择Secret text
Secret参数处填写前面在安装sonarqube时创建的token
ID和Describption为自定义该凭据的名字其中ID必须为唯一因为jenkins在调用的时候是根据ID调用该凭据的
其它参数默认
如下所示添加完后点击Add即可添加然后在上图红色框处的下拉框中选择此凭据即可。
第二种在jenkins首页点击”凭据-->系统-->全局凭据-->Add Credentials“如下所示
添加配置与上面一样
jenkins添加sonarqube配置完成下面配置jenkins集成sonnarqube-scanner。
jenkins首页点击”manage jenkins管理jenkins--> Global Tool Configuration(全局工具配置)“找到SonarQube Scanner参数值添加scanner在本地的路径并给定一个名称如下所示
到此即完成jenkins与scanner的集成配置。然后我们创建一个Jenkins项目测试一下
创建一个maven项目配置好maven打包配置后在”Post Steps“步骤点击”Execute SonarQube Scanner“配置如下
配置参数不用多说,上面有过介绍,只不过这里的部分参数值使用了变量的方式,有关变量的详细说明可参考${jenkins_url}/pipeline-syntax/globals。Additional arguments指定了-X参数用于在执行scanner时打印详细的日志信息。
执行结果如下所示
有关jenkins与sonar集成的配置使用就先简单的介绍到这里在以后的章节中会继续介绍如何在pipeline中使用sonar-scanner

View File

@@ -0,0 +1,567 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
03.Devops工具链
基础工具介绍安装配置
在前面的两个章节中简单介绍了一下Jenkins以及插件的安装配置等内容。本系列文章虽然是基于Jenkins 进行展开的但是实现Devops中的CI/CD也不是仅凭一个工具就能实现的需要众多Devops工具链的相互协作。所以在配置好Jenkins服务后本章节来介绍几款与Jenkins集成进行持续交付和持续部署的devops工具包。并且在以后的章节中会通过与Jenkins配合以实现应用服务的构建和部署。
本节主要介绍如下工具的安装配置:
Docker
Harbor
Gitlab
Ansible
Docker
介绍
Docker 是什么大家都应该都有所了解一个基于LXC技术构建的的高级容器引擎。Docker以容器为资源分割和调度的基本单位封装整个软件运行时的环境为开发者和系统管理员设计的用于构建、发布和运行分布式应用的平台。它是一个跨平台、可移植并且简单易用的容器解决方案。
Docker 基于Linux 内核的cgroupnamespace以及 AUFS 和Union FS等技术对进程进行封装隔离属于操作系统层面的轻量级虚拟化技术。
说明
Namespace是对全局系统资源的一种封装隔离使得处于不同namespace的进程拥有独立的全局系统资源改变一个namespace中的系统资源只会影响当前namespace里的进程对其他namespace中的进程没有影响。Linux内核中提供了7种namesapce隔离系统调用docker 用到6种
UTSUTS namespace提供主机名和域名的隔离
IPC进程间通信涉及的IPC资源包括常见的信号量消息队列和共享内存。
PIDPID namespace隔离进程PID两个namespace下的进程可以有相同的Pid每个Pid的namespace都有自己的计数程序。PID namespace中的第一个进程为PID 1所有进程的父进程像linux 中 的init进程。
mountmount通过隔离文件系统挂载点对隔离文件系统提供支持
Network Network namespace主要提供了关于网络资源的隔离不是真正的网络隔离只是把网络独立出来
User User namespace主要隔离安全相关的标识符和属性如用户id用户组idroot目录 、秘钥以及权限等。
cgroup对一组进程进行统一的资源监控和限制是Linux内核提供的一种将进程按组进行管理的机制。cgroup 可以限制、记录任务组使用的物理资源cpu、memory 、io等主要作用如下
资源限制:对任务使用的资源总额进行限制
优先级分配:控制任务运行的优先级
资源统计:统计系统资源的使用量
任务控制:对任务执行挂起、恢复等操作
AUFS 和 Union FS
UnionFS 是一种为 Linux 操作系统设计的用于把多个文件系统联合到同一个挂载点的文件系统服务
AUFS 即 Advanced UnionFS ,是 UnionFS 的升级版,它能够提供更优秀的性能和效率。
AUFS 作为联合文件系统它能够将不同文件夹中的层联合Union到同一个文件夹中这些文件夹在 AUFS 中称作分支整个联合的过程被称为联合挂载Union Mount
有关Docker的介绍就简单的介绍到这里下面看一下Docker的安装
安装
Centos7.x 安装Docker
安装前首先卸载旧版本的Docker服务如果曾经安装过
$ sudo yum remove docker docker-common docker-selinux docker-engine docker-client*docker-latest* docker-logrotate
#删除残留文件
$ rm -rf /var/lib/docker
安装存储库包
$ yum install -y yum-utils device-mapper-persistent-data lvm2
设置稳定存储库
$ sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
注意:使用官方的源安装会很慢,可以换成阿里云的
https://mirrors.aliyun.com/docker-ce/linux/centos/docker-ce.repo
安装DOCKER CE
$ yum-config-manager --enable docker-ce-edge && yum install docker-ce -y
启动
$ systemctl start docker
使用脚本自动安装
Docker官方为了简化安装流程提供了一套安装脚本CentOS 系统上可以使用这套脚本安装:
$ curl -fsSL get.docker.com -o get-docker.sh
$ sudo sh get-docker.sh --mirror Aliyun
说明:
--mirror 表示指定的yum源
执行这个命令后脚本就会自动的将一切准备工作做好并且把Docker 安装在系统中。
默认情况下docker命令会使用Unix socket与Docker引擎通讯。而只有root 用户和 docker组的用户才可以访问Docker引擎的Unix socket。出于安全考虑一般Linux系统上不会直接使用root用户。因此更好地做法是将需要使用docker的用户加入docker用户组(如果你不想使用root用户)。
建立docker组
$ sudo group add docker
将当前用户加入docker组
$ sudo usermod -aG docker $USER
其它系统安装docker可以参考官网 ,比如
Ubuntu安装
https://docs.docker.com/install/linux/docker-ce/ubuntu/
Mac安装
https://docs.docker.com/docker-for-mac/install/
安装还是比较简单的。
测试
Docker 安装完以后就可以进行下一步的基础测试了。
这里以根据官方的nginx镜像启动一个容器为例
[root@Docker ~]# docker run -itd -p80:80 nginx
Unable to find image 'nginx:latest' locally
latest: Pulling from library/nginx
bc51dd8edc1b: Pull complete
.....
5c472411317e6f1f13ed522d3963331054c84f747cd88d66f52d67be66709973
启动以后,查看并测试访问一下
[root@Docker ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
72dd0bd9a12f nginx "nginx -g 'daemon ..." 5 seconds ago Up 2 seconds 0.0.0.0:80->80/tcp gallant_nash
[root@Docker ~]# curl localhost:80
......
</style>
</head>
<body>
<h1>Welcome to nginx!</h1>
<p>If you see this page, the nginx web server is successfully installed and
......
</body>
</html>
在使用docker run命令启动一个容器的时候如果该镜像在本地不存在默认会从docker hub拉取镜像在这一过程中可能拉取镜像的速度可能会很慢这是由于国内网络访问Docker hub网络慢造成的。这就需要给docker服务配置镜像加速器了用于提高镜像拉取速度。
对于Centos系统编辑/usr/lib/systemd/system/docker.service文件找到ExecStart= 这一行,在这行最后添加--registry-mirror="加速器地址",如:
ExecStart=/usr/bin/dockerd --registry-mirror=https://jxus37ad.mirror.aliyuncs.com
或者使用docker官方加速器
https://registry.docker-cn.com
或者使用官方的方法
#docker官方镜像加速如果文件不存在则新建文件并且保证文件符合json规范
cat /etc/docker/daemon.json      
{
"registry-mirrors": [
"https://registry.docker-cn.com"
]
}
配置完以后需要重启docker服务
systemctl daemon-reload
systemctl restart docker
在命令行执行 docker info如果从结果中看到了上面添加的加速器内容说明配置成功。比如
Registry Mirrors: https://registry.docker-cn.com/
有关docker的安装配置就简单的介绍到这里。那么Jenkins与Docker集成能干什么呢根据jenkins官方提供的插件来看主要是两个方面的功能一个是使用docker作为应用服务的部署环境另一个是使用docker作为jenkins的动态slave节点。实际工作中常用的功能就是使用docker作为jenkins 的动态slave节点用于提高jenkins服务的性能以及效率关于如何集成使用会在以后的章节中详细介绍。
Harbor
在介绍Harbor之前有必要先了解一下docker的镜像仓库。镜像仓库Repository是集中存放镜像的地方。
一个容易混淆的概念是注册服务器Registry。实际上注册服务器是管理仓库的具体服务器每个服务器上可以有多个仓库而每个仓库下面有多个镜像。从这方面来说仓库可以被认为是一个具体的项目或目录。例如对于仓库地址dl.dockerpool.com/ubuntu来说dl.dockerpool.com是注册服务器地址ubuntu 是仓库名。
大部分时候,并不需要严格区分这两者的概念。
Harbor正是一个用于存储和分发Docker镜像的企业级Registry服务器 Harbor支持安装在多个Registry节点的镜像资源复制镜像全部保存在私有Registry中。作为一个企业级私有Registry服务器Harbor提供了更好的性能和诸如用户管理访问控制和活动审计等功能。提升用户使用Registry构建和运行环境传输镜像的效率。
Harbor组件
Harbor在架构上主要由6个组件构成
ProxyHarbor的registryUItoken等服务通过一个前置的反向代理统一接收浏览器、Docker客户端的请求并将请求转发给后端不同的服务。
Registry 负责储存Docker镜像并处理docker push/pull 命令。由于我们要对用户进行访问控制即不同用户对Docker image有不同的读写权限Registry会指向一个token服务强制用户的每次docker pull/push请求都要携带一个合法的token, Registry会通过公钥对token 进行解密验证。
Core services 这是Harbor的核心功能主要提供以下服务
UI提供图形化界面帮助用户管理registry上的镜像image, 并对用户进行授权。
webhook为了及时获取registry 上image状态变化的情况 在Registry上配置webhook把状态变化传递给UI模块。
token 服务负责根据用户权限给每个docker push/pull命令签发token. Docker 客户端向Regiøstry服务发起的请求,如果不包含token会被重定向到这里获得token后再重新向Registry进行请求。
Database为core services提供数据库服务负责储存用户权限、审计日志、Docker image分组信息等数据。
Job Services提供镜像远程复制功能可以把本地镜像同步到其他Harbor实例中。
Log collector为了帮助监控Harbor运行负责收集其他组件的log供日后进行分析。
Harbor的每个组件都是以Docker容器的形式构建的官方也是使用Docker Compose来对它进行部署。用于部署Harbor的Docker Compose模板位于 harbor/docker-compose.ymlharbor安装好以后通过docker ps命令或者docker-compose ps命令可以看到Harbor是由7个新版本是9个容器组成的如下所示
# docker-compose ps
Name Command State Ports
------------------------------------------------------------------------------------------------------------------------------
harbor-adminserver /harbor/harbor_adminserver Up
harbor-db docker-entrypoint.sh mysqld Up 3306/tcp
harbor-jobservice /harbor/harbor_jobservice Up
harbor-log /bin/sh -c crond && rm -f ... Up 127.0.0.1:1514->514/tcp
harbor-ui /harbor/harbor_ui Up
nginx nginx -g daemon off; Up 0.0.0.0:443->443/tcp, 0.0.0.0:4443->4443/tcp, 0.0.0.0:80->80/tcp
registry /entrypoint.sh serve /etc/ ... Up 5000/tcp
说明
nginxnginx负责流量转发和安全验证对外提供的流量都是从nginx中转所以开放https的443端口它将流量分发到后端的ui和正在docker镜像存储的docker registry。
harbor-jobserviceharbor-jobservice 是harbor的job管理模块job在harbor里面主要是为了镜像仓库之前同步使用的;
harbor-uiharbor-ui是web管理页面主要是前端的页面和后端CURD的接口;
registryregistry就是docker原生的仓库负责保存镜像。
harbor-adminserverharbor-adminserver是harbor系统管理接口可以修改系统配置以及获取系统信息。
harbor-dbharbor-db是harbor的数据库这里保存了系统的job以及项目、人员权限管理。由于本harbor的认证也是通过数据在生产环节大多对接到企业的ldap中
harbor-logharbor-log是harbor的日志服务统一管理harbor的日志。通过inspect可以看出容器统一将日志输出的syslog。
这几个容器通过Docker link的形式连接在一起这样在容器之间可以通过容器名字互相访问。对终端用户而言只需要暴露proxy 即Nginx的服务端口对于其他的组件用途只需要做个简单的了解即可
安装与配置
下载offline 版本的Harbor
https://github.com/goharbor/harbor/releases
配置Harbor
$ wget https://storage.googleapis.com/harbor-releases/release-1.7.0/harbor-offline-installer-v1.7.5.tgz
$ tar xvf harbor-offline-installer-v1.7.5.tgz
$ cd harbor
修改harbor.cfg内容如下
# 访问管理UI与注册服务的IP地址或主机名不要使用localhost或127.0.0.1因为Harbor需要被外部的客户端访问
hostname = 192.168.176.155
# 修改管理员密码
harbor_admin_password = Harbor12345
只需要修改harbor的地址和登录密码即可前提需要安装docker-compose
$ sudo curl -L https://github.com/docker/compose/releases/download/1.17.1/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose
$ sudo chmod +x /usr/local/bin/docker-compose
执行 ./install.sh 即可自动下载镜像并启动
启动以后访问Harbor http://192.168.176.155/harbor/sign-in ,用户/密码 admin Harbor2345
到这里Harbor就安装完成了
关闭与启动 Harbor 命令
# harbor 目录下
#前台启动
docker-compose up
#后台启动
docker-compose up -d
#关闭
docker-compose down
Docker 默认不允许非 HTTPS 方式推送镜像。但是可以通过Docker的配置选项来取消这个限制
$ cat /usr/lib/systemd/system/docker.service |grep dockerd
ExecStart=/usr/bin/dockerd -H unix:// --insecure-registry 192.168.176.155
$ docker login 192.168.176.155
Username: admin
Password:
WARNING! Your password will be stored unencrypted in /root/.docker/config.json.
Configure a credential helper to remove this warning. See
https://docs.docker.com/engine/reference/commandline/login/#credentials-store
Login Succeeded
从上面的登录信息可以看到docker私有仓库认证的凭证存放在/root/.docker/config.json文件。
测试推送镜像
$ docker tag nginx 192.168.176.155/library/nginx:test
$ docker push 192.168.176.155/library/nginx:test
The push refers to repository [192.168.176.155/library/nginx]
332fa54c5886: Pushed
6ba094226eea: Pushed
6270adb5794c: Pushed
test: digest: sha256:e770165fef9e36b990882a4083d8ccf5e29e469a8609bb6b2e3b47d9510e2c8d size: 948
登录harbor查看
到这里harbor的基本安装配制就完成了。
GitLab
介绍
GitLab是一个开源的用于代码仓库管理的项目使用Git作为代码管理工具并在此基础上搭建起来的web服务。
Gitlab也是比较受欢迎并且众多公司在用的免费的代码托管服务除了作为源码仓库服务外Gitlab还有另一个重要的功能Gitlab-CI一套基于Gitlab的持续集成系统。Gitlab 8.0版本开始默认集成CI功能GitLab CI通过在项目内创建.gitlab-ci.yaml 配置文件读取CI任务并通过被称为 GitLab Runner 的Agent端进行持续交付与部署操作。每当gitlab仓库有代码更新的时候就会触发提前定义的脚本.gitlab-ci.yaml进行预设定的操作比如代码编译、测试、部署等
安装
Gitlab的安装比较简单GitLab官网针对不同的系统也给出了比较详细的安装步骤可以参考这里
为了测试方便我这里直接使用docker安装使用如下命令启动容器
docker run -d -p 4443:443 -p 80:80 -p 2222:22 --name gitlab --restart always -v /srv/gitlab/config:/etc/gitlab -v /srv/gitlab/logs:/var/log/gitlab -v /src/gitlab/data:/var/opt/gitlab gitlab/gitlab-ee
说明:
gitlab镜像默认使用的端口为80在使用docker部署gitlab并设置启动参数时宿主机端口最好也使用80也就是 -p80:80参数不要随意改否则会遇到各种意想不到的坑。比如下面这个
在使用docker方式安装gitlab时遇到一个问题使用vm默认没有此问题因为映射的本地端口和gitlab内部端口不同在使用gitlab-runner容器拉取应用代码的时候出现网络不通的问题如下所示
说明:
gitlab-runner默认拉取代码时会使用gitlab 服务docker内部的ip或者hostname去拉取代码肯定会导致网络不通所以就需要做端口映射的时候保持宿主机和容器端口一样由于gitlab默认内部端口为80所以宿主机端口最好也用80
如果非要用别的端口怎么办?比如使用-p8099:80那么下面将会进入一个填坑的过程虽然修改gitlab的配置文件、重启容器和服务可以解决上面的问题但是这个过程坑也比较多比如修改完地址和端口以后虽然能够拉取代码有可能会影响Gitlab-CI的pipeline的操作等或者修改完后重启容器又恢复默认配置等。所以为了保险起见在做端口映射的时候宿主机端口最好用80
在Gitlab中创建好项目后从项目配置中获取项目地址的时候会发现项目的地址为http://$container_id/$group/$project_name.git虽然知道gitlab的实际地址但是每创建一个项目项目地址中的host都是容器的id并且复制出来使用时都要手动进行替换对于严谨的运维来说这是不能接受的所以还需要统一修改项目的url地址。
容器启动以后,修改宿主机/srv/gitlab/config/gitlab.rb或者容器/etc/gitlab/gitlab.rb文件找到external_url关键字去掉注释并改成宿主机的ip地址用于更改新创建的项目里拉取代码的地址
external_url 'http://192.168.176.154'
然后重新启动gitlab
两种方法(任选):
#进入容器
gitlab-ctl restart
或者直接
# 重启容器
docker restart gitlab
测试
创建一个项目,比如我们要测试的项目
点击create project以后跳转到如下所示的界面
该页面表示我们创建项目成功并且给出了第一次提交代码的操作步骤。该步骤针对三种情况第一种是我们要提交到代码仓库的文件不存在时需要我们自己创建一个目录并在该目录下创建文件上传到仓库参考第一至二步骤如果我们的项目文件存在且不是git存储库直接则直接参考第一三步骤即可如果我要上传一个已经存在的项目并且该项目为git存储库则参考第一和第四步骤即可。
我这里使用的第二种情况,直接将已经存在的项目上传到仓库。如下所示:
[root@es-165 helloworldweb]# git config --global user.name "Administrator"
[root@es-165 helloworldweb]# git config --global user.email "[email protected]"
[root@es-165 helloworldweb]# git init
git remote add origin http://192.168.176.154/root/test-helloworld.git
Initialized empty Git repository in /opt/helloworldweb/.git/
[root@es-165 helloworldweb]# git remote add origin http://192.168.176.154/root/test-helloworld.git
[root@es-165 helloworldweb]# git add .
[root@es-165 helloworldweb]# git commit -m "Initial commit"
[master (root-commit) 8cd1730] Initial commit
6 files changed, 76 insertions(+)
create mode 100644 README.md
create mode 100644 pom.xml
create mode 100644 src/main/webapp/.index.jsp.un~
create mode 100644 src/main/webapp/WEB-INF/web.xml
create mode 100644 src/main/webapp/index.jsp
create mode 100644 src/main/webapp/index.jsp~
[root@es-165 helloworldweb]# git push -u origin master
Username for 'http://192.168.176.154': root
Password for 'http://[email protected]':
Counting objects: 12, done.
Delta compression using up to 4 threads.
Compressing objects: 100% (8/8), done.
Writing objects: 100% (12/12), 1.88 KiB | 0 bytes/s, done.
Total 12 (delta 0), reused 0 (delta 0)
To http://192.168.176.154/root/test-helloworld.git
* [new branch] master -> master
Branch master set up to track remote branch master from origin.
然后在其他机器通过git clone命令直接测试即可。
Ansible
上面介绍完docker和gitlab接下来简单介绍并安装一下比较流行的自动化运维工具Ansible。
介绍
Ansible 是一个开源的基于Python paramikoPython实现的ssh协议库开发的分布式、无需客户端、轻量级的自动化配置管理工具。Ansible基于模块化工作本身没有批量部署的能力它只是提供一种框架真正具有批量部署功能的是ansible所运行的模块。ansible实现了批量系统配置、批量程序部署、批量运行命令等功能配置语法使用 YMAL 及 Jinja2模板语言。
安装
Ansible是无代理的自动化工具默认情况下通过SSH协议管理计算机。安装后Ansible不会添加数据库并且没有启动或继续运行守护程序。只需将其安装在一台计算机上它就可以从该中心管理整个远程计算机。
前提条件:
当前Ansible控制节点可以从安装了Python 22.7版或Python 33.5版及更高版本的任何计算机上运行。这包括Red HatDebianCentOSmacOS任何BSD等。控制节点不支持Windows。
安装前首先需要解决依赖关系:
yum -y install python-jinja2 PyYAML python-paramiko python-babel python-crypto
tar包安装
https://github.com/ansible/ansible/releases   ##下载Ansible,解压安装
python setup.py build
python setup.py install
yum 安装
$ yum install https://dl.fedoraproject.org/pub/epel/epel-release-latest-7.noarch.rpm
$ sudo yum install ansible
更多平台安装可参考官网
安装完以后执行下面命令查看是否安装成功
$ ansible --version
ansible 2.7.6
config file = /etc/ansible/ansible.cfg
configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
ansible python module location = /usr/lib/python2.7/site-packages/ansible
executable location = /usr/bin/ansible
python version = 2.7.5 (default, Aug 7 2019, 00:51:29) [GCC 4.8.5 20150623 (Red Hat 4.8.5-39)]
出现上面内容说明ansible安装成功。
修改通过上面命令获取的ansible默认使用的配置文件config file参数所指定的文件host_key_checking参数值去掉注释true改为false
host_key_checking = False
用于解决ansible首次连接_host_服务器时需要验证的问题
测试
首先可以通过默认的localhost组进行ping操作比如
[root@ansible ~]# ansible localhost -m ping
localhost | SUCCESS => {
"changed": false,
"ping": "pong"
}
Ansible通过ssh实现配置管理、应用部署、任务执行等功能。因此要使用ansible需要先配置ansible端能基于免密钥认证连接被管理的节点用户名和密码也可以本节暂时不做演示如下所示
在ansible端通过ssh-keygen -t rsa -P ''生成密钥对以后,执行如下命令
ssh-copy-id -i ~/.ssh/id_rsa.pub <要被管理的主机ip>
执行完成后,创建/etc/ansible/hosts文件将刚刚做完免密认证的主机ip添加进去-
比如
$ cat /etc/ansible/hosts
[ansible_ag1] #该名称自定义
192.168.177.43 #被远程管理的主机ip
然后执行如下命令
$ ansible ansible_ag1 -m shell -a "ifconfig"
192.168.177.43 | CHANGED | rc=0 >>
docker0: flags=4099<UP,BROADCAST,MULTICAST> mtu 1500
inet 172.17.0.1 netmask 255.255.0.0 broadcast 172.17.255.255
inet6 fe80::42:91ff:fea9:955c prefixlen 64 scopeid 0x20<link>
......
ens192: flags=4163<UP,BROADCAST,RUNNING,MULTICAST> mtu 1500
inet 192.168.177.43 netmask 255.255.254.0 broadcast 192.168.177.255
......
上面的ansible命令表示使用-m参数指定的shell模块去远程主机执行通过-a参数指定命令”ifconfig”。
ansible测试没问题以后我们就可以在jenkins中配置ansible的环境变量了。-
点击 “Manage Jenkins”—> “Global Tool Configuration(全局工具配置)“在跳转的界面中找到Ansible 参数项点击新增ansible配置ansible命令的环境变量如下所示
配置好保存即可
有关基础工具安装配置的内容到这里就介绍到这里,内容都相对比较简单,在以后的章节中会具体介绍使用这些工具的方法。

View File

@@ -0,0 +1,251 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
04.初探Jenkins CD实践
在了解了Jenkins的系统配置与插件以后本节开始正式学习一下使用Jenkins进行基础的CD实践。
开始之前要说明的是本文介绍的部署方法以及脚本等比较基础旨在介绍使用以及配置jenkins任务job的一些流程和方法在后续的文章中会对部署方法优化和提高对于不同的场景和需求还需要根据自己的实际情况出发进行项目配置。当然如果你对jenkins使用或者对持续交付的流程已经很熟了你也可以略过此章节。
创建Jenkins项目
Jenkins包含多种类型的项目配置比如freestyle、maven、pipeline等每种类型的项目都有各自的优缺点以及固定的使用场景重点在于要根据自己的实际情况选择合适的项目类型创建项目。下面简单介绍一下不同类型的项目的使用。
创建Freestyle类型的项目
Freestyle创建一个自由风格类型的项目。为什么叫自由风格的项目个人理解为项目的配置扩展性较强对于某项步骤的配置操作不局限于某一个特定的插件实现想要达到的目标的方法具有多样性。
在Jenkins主面板点击”新建“菜单在跳转到的界面中输入任务名称比如我创建的job名称为test-freestyle-project选择”Freestyle project自由风格项目然后在页面最下面点击确定即可。
点击确定后跳转到任务job配置界面如下图所示
该任务包含了如下几个配置步骤(图中红线框内的部分):
general用于配置任务构建时的全局配置比如上面列出的选项框
Run the build inside Docker containers用来定义构建时是否在docker容器内进行构建-
Discard old builds用于配置该项目构建的历史记录个数和保存的时间-
限制项目的运行节点用于配置该项目在指定的Jenkins节点上构建-
This project is parameterized 用于配置该项目使用参数化构建;
源码管理用于配置构建时的源代码仓库。从运维角度去使用Jenkins几乎所有的Job任务都涉及到代码的拉取操作该步骤就是配置代码仓库的地址和对仓库认证凭据信息。代码仓库的类型主要分为CVS、Git、svn等。
构建触发器该步骤主要用于配置什么时候运行Job比如通过”身份令牌”进行远程构建配置通过设置触发器在某个项目构建完成后在构建本项目通过计划任务设置定时构建等。
构建环境:构建环境步骤用于设定构建时的工作环境配置,比如配置构建时使用的凭证设置;配置编译代码时使用的特定的配置文件;构建之前发送编译时需要用的文件到服务器或者执行命令等配置。
构建构建步骤用于进行job构建操作该步骤针对不同的插件有不同的使用方法。比如用于代码编译操作、比如执行命令、如果遇到需要构建镜像的需求也可以构建容器镜像等。
构建后操作该步骤作为整个jenkins 任务配置的最后一步主要进行一些”收尾”工作比如部署应用服务操作有测试操作的生成测试报告对代码进行质量分析操作、或者在上面操作步骤成功后设置构建其他job任务、发送邮件等操作。
对于jenkins新安装的插件的使用会在上面列出的一个或者多个步骤中显示插件的使用选项。
相对于其它类型的Job比如maven、pipeline类型每种类型的job都有自身特定的配置步骤并且各配置步骤会根据插件安装的多少也会有所差异不过大体上的配置步骤都相同。
由于jenkins项目配置的参数选项比较多而且参数选项会根据插件的安装有所不同下面就先看一个最简单的使用示例。
在“构建”步骤的”增加构建步骤“选项框里选择Execute shell该选项表示在目标主机上执行shell命令。需要注意的是如果在”General“步骤没有对项目的运行节点进行特殊配置该任务运行的主机为Jenkins master节点也就是在jenkins master服务器上执行shell命令。
如下所示:
执行结果如下:
输出的结果为jenkins服务所在服务器的主机名。
如果勾选上在”General“步骤中的”限制项目的运行节点“选项框在”标签表达式“输入框中输入在上一节中添加的slave节点的标签(jenkins-slave1)在次执行该job如下所示
该结果表示在slave节点上执行jenkins job成功。
上面虽然成功执行了jenkins job而我们使用jenkins的目的可不仅仅是在jenkins服务器上执行简单的shell命令从运维角度出发用得最多的就是通过jenkins服务拉取应用代码并编译测试最后部署到指定的服务器上。
比如我们使用在上一节中安装Gitlab时创建的test-helloworld仓库时提交的代码作为项目测试用例通过配置jenkins拉取此项目代码并使用maven管理工具编译代码。
代码编译
使用freestyle类型的job配置如下在源码管理步骤中使用git服务。
其中Credntials为创建的对Gitlab仓库认证的凭证点击上图中的Jenkins选项在跳转的界面中在”类型“选项框选择Username with password并在下面的输入框中输入用户名和密码以及用于区分该凭证的用户输入好保存即可。然后就可以在Credntials参数的下拉框中看到刚刚创建的凭证了。有关Jenkins凭证的内容会在下一章节详细介绍。
在”增加构建步骤“的下拉列表中选择 “Invoke top level Maven target“配置maven构建参数。
构建步骤这里使用maven插件进行构建其中
Maven 版本 参数为选择框如果在Jenkins全局配置中配置了Maven工具的环境变量这里从下拉列表中会有显示比如我这里显示的为在Jenkins全局配置里设置的全局名称。
目标 这里应该写maven 编译代码的命令,可用的命令为 clean package/instlall/deploy这些命令都可以进行代码的编译、测试、打包等操作。
POM 为编译代码时的pom.xml文件该文件定义了项目的基本信息用于描述项目如何构建声明项目依赖等。这里可以写该文件的绝对路径也可以写相对路径上面示例配置的为${WORKSPACE}变量jenkins自带的环境变量路径下的pom.xml也就是本项目工作目录下的pom.xml文件所以这里也可以直接写pom.xml而不加任何路径。
Java 虚拟机参数 配置maven编译代码时的参数上面示例配置参数为跳过执行src/test/java中的JUnit测试用例。
配置文件 这里设置的为maven编译代码时使用的配置文件。
如果觉得使用maven插件对话框的方式配置编译参数比较麻烦也可以通过命令进行代码 编译。在“增加构建步骤”中使用”Exec shell“输入mvn clean install即可比较简单这里不再演示了有兴趣的可以自己试一下。
配置好后保存在项目主菜单点击”build now(立即构建)“就开始进行构建了。如下所示:
如图上所示构建完成会在target目录下生成一个war包。下面我们配置job将该war包部署到指定的服务器上去。
添加主机
项目代码编译成功了就该着手部署该项目了实际工作中可能会使用sonar对代码进行分析鉴于该helloworld项目比较简单代码质量分析的步骤就先略过至于sonar的使用在以后的章节中会介绍。将该项目部署到本地服务器上比较容易通过shell命令将war包拷贝到指定的目录下通过tomcat启动或者直接通过java -jar命令启动。下面介绍一下如何部署到远程服务器上。
要部署到远程服务器上首先需要在jenkins系统中添加一台可远程连接的机器步骤如下
点击”Manage Jenkins管理jenkins> “Configure System系统配置“找到 Publish over SSH参数点击”add“添加一台机器。
说明
Publish over SSH参数用来配置要远程连接的服务器信息包括服务器IP地址用户名、密码、端口等本次测试的服务器IP为”192.168.176.160”。
Name 为自定义的主机名称在Jenkins项目配置中会通过该名称来连接指定的服务器。
Remote Directory用来定义将文件copy到远程服务器的路径为”/data”目录所以要确保176.160服务器上存在该目录,否则通过该插件进行文件拷贝的时候会提示路径不存在。
Timeout选项最好设置大一点默认30s也就是如果连接超过30s就会断开连接如果你的项目部署时间超过30s就需要更改此处默认情况下一般不会超过30秒
配置好后下面有test configuration按钮用来测试是否能连接成功。
如果担心远程服务器主机更改密码频繁配置连接该主机也可以通过ssh-key的方式主要配置上面的path to key或者key参数。其中Path to key参数用来配置key文件私钥的路径。Key参数可用于将私钥复制到这个框中。如果你启动Jenkins服务用户已经做了对目标主机的免密认证也可以不用配置这两个参数
配置好以后,保存退出。
代码部署
添加完主机以后就可以将编译好的包部署到该服务器上去了。这里以部署服务到tomcat为例
修改job配置在”构建后操作“步骤的下拉列表中点击”Send build artifacts over SSH“配置如下
说明:
SSH Server下的 Name为要发送文件到或者执行命令的远程主机在上面的添加远程连接服务器步骤中有配置远程目标服务器的名称本例为docker-test
Transfers 选项下的Source files栏这里应该写要从本地复制的jar包或者war包微服务一般都是jar包这里写成**/xx.war的方式类似于linux中的find方式从当前工作空间的所有目录去发现该war包。
Remote Prefix 相当于将找到的该war包路径的前缀去掉因为代码编译好后默认会在项目名称目录下生成target目录该目录存放代码编译后的产物比如jar包war包等。需要注意的是这里的路径是相对于当前项目所在的路径。
Remote directory表示要将文件拷贝到此参数设置的目录下本示例设置的是/目录。需要注意的是这里设置的目录不是linux文件系统下的/目录而是在jenkins”系统配置“里添加该服务器时Remote Directory选项所指定目录下的/目录从”添加主机“小节的截图可看到我在添加服务器时设置的Remote Directory目录为/data所以这里的Remote directory对应到远程主机的目录为/data/,并且这个目录需要在构建前自己创建,否则发送文件时会提示目录不存在。
exec command表示在远程服务器上执行shell命令此处的命令是一行一行执行的并且上一条命令执行出错并不影响下条命令的执行但是如果这些命令是有依赖关系的比如下条命令依赖上条命令会导致部署失败。这里为了方便测试和学习先将命令一行一行写出来也可以将上面的命令写成一个脚本然后通过执行脚本去部署服务。
这样就完成了一个自动部署tomcat服务的job配置。看一下Jenkins执行结果。
然后去浏览器访问tomcat服务ip加端口以及uri此示例为test1即可。如图所示
在配置jenkins项目部署war包的时候除了使用Publish Over SSH插件以外也可以通过在”Exec shell“选项框中直接输入shell命令或者shell脚本部署tomcat服务相比于Publish Over SSH插件该方法更简单灵活扩展性更强管理起来也更加方便。至于如何编写shell脚本这里我就不演示了有兴趣的可以自己实现一下。
创建maven类型的项目
通过上一小节的实践内容相信你对jenkins的使用应该有了进一步的了解下面再来创建一个maven类型的job来加深一下理解。
Maven类型的项目顾名思义就是适合使用maven工具进行代码编译的java项目配置maven类型的项目时配置步骤与使用freestyle类型的项目稍有不同。
比如我这里创建了一个名称为test-maven-project的job默认的配置步骤如下
说明:
相比于freestyle类型的项目maven类型项目中将”构建“步骤拆分为Pre Steps和build两个步骤build步骤专门用来配置使用maven工具编译代码时的配置参数Post Steps与Pre Steps步骤配置参数相同”构建设置“步骤则增加了邮件通知功能。
代码编译
相对于freestyle类型的项目配置”源码管理“步骤配置不变只需要在build步骤配置一下maven参数即可如下所示
说明:
在maven类型的项目配置中只需要配上图中列出的这两项即可对于”Root Pom“的设置同样以当前${WORKSPACE}路径Jenkins工作路径作为基本路径只写pom.xml就代表项目的根目录下的pom.xml文件。
”高级“按钮的下拉框中包含maven构建时的一些参数配置使用的配置文件定义等实际使用过程中使用默认配置就行当前maven部署程序下的配置文件。当然如果配置文件有特殊改动也可以在配置中自己定义。
保存好配置直接构建就行了,是不是很简单。
代码部署
代码部署的方式与使用freestyle方式部署项目的方法相同既可以使用publish over ssh插件也可以使用Exec shell方式。只不过在maven项目中使用Publish over SSH插件的参数选项名称变为Send files execute commands over ssh但是配置参数没有变。
有关上面部署操作这里就不在重复演示了,在以后的章节中会介绍其他部署方式。
创建一个流水线类型的项目
创建与使用一个流水线类型包括多分支流水线类型的项目需要了解jenkins pipeline的一些基础知识由于内容较多并且涉及到一些插件的使用所以有关pipeline的内容将在后续的章节中进行详细的说明。
部署springboot项目到容器
本次jenkins系列课程的核心是使用Jenkins持续交付与部署微服务架构类型的项目。按照微服务的理念如果使用容器作为基础设施能够实现业务应用的的快速交付、快速部署所以下面就简单介绍一下如何将服务部署到容器中。当然这需要你掌握一部分docker相关的知识。
首先在Jenkins界面创建一个maven类型的job如下所示
选中”构建一个maven项目”然后点击最下面的”ok”即可。
创建项目以后需要配置项目源码仓库分支以及pom.xml路径如何设置前面已经说过此处不再演示直接看构建后操作。
在”Add Post-build Steps构建后操作“下拉框里选中Send files or execute commands over ssh配置如下
说明:
有关该插件的各个参数的说明,在上面的示例中有介绍过,这里不再多说。
上面部署的命令中,除了服务器上指定的目录需要事先创建以外,还需要目标服务器上(/data/base/nop目录预先放一个Dockerfile文件用于将maven编译的构建产物jar包文件构建到镜像中去。
Dockerfile如下
FROM fabric8/java-alpine-openjdk8-jre
COPY fw-base-nop.jar /deployments/
CMD java -jar /deployments/fw-base-nop.jar
command中代码如下
docker stop base-nop
docker rm base-nop
docker rmi base-nop
cd /data/fw-base-nop
docker build -t base-nop .
docker run --network host -e TZ="Asia/Shanghai" -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/fw-base-nop:/data/logs/fw-base-nop --name base-nop base-nop
使用docker命令 停止/删除容器、停止/删除镜像、最后启动容器。
执行结果:
这样就完成了一个将服务部署到容器内的流程。配置比较简单,介绍此示例的目的主要是为了了解一下流程,在以后的章节中会基于此示例进行一个优化。
有关使用jenkins的基础实践到这里就结束了。回顾本章节的内容都比较基础使用两个入门级的示例简单介绍了一下使用不同类型的Jenkins项目进行工作的流程在以后的进阶章节除了会优化上面示例中的代码同时也会使用多种不同的方式实现服务的持续交付和部署。

View File

@@ -0,0 +1,499 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
05.Jenkins常用项目配置参数
在上一章节简单介绍了使用jenkins部署服务到不同的机器上但是对于在项目中的其它选项参数都没有过多介绍所以本章的开始先简单的介绍一下项目中一些常用的参数选项。
项目配置
本小节以freestyle自由风格类型的项目为例对不同步骤中涉及到的一些常用的选项参数进行简单的说明需要注意的是每个步骤中的参数选项会根据在Jenkins中安装的插件会有所不同如果下面介绍到的某些选项参数在你的项目中不存在有可能是插件没有安装所以如果遇到这种情况直接搜索所用的插件安装即可。
General
General 步骤主要是用来配置构建任务的一些通用配置,比如:
描述该输入框内容自定义也可以不写出发点是用来记录该job任务的一些描述信息比如该项目属于哪个组服务的用途是什么等内容。
Discard old builds(丢弃旧的构建)用于处理Jenkins项目构建的历史记录”保持构建的天数” 和 “保持构建的最大个数”根据自己情况自定义。
This project is parameterized(参数化构建过程)在非pipeline类型的job中这可能是用的比较多的一个参数用于定义job构建过程中用到的一些参数参数的类型很多在下面会进行详细的介绍。
Throttle builds控制并发的选项用来设置两个任务之间最小间隔和同一个时间内最大任务数量该功能用的很少如果要使用此选项则下面的”在必要的时候并发构建“参数需要勾选。
Restrict where this project can be run(限制项目的运行节点):设置项目在指定的机器上运行。
源码管理
源码管理步骤这里不需要在过多介绍了根据不同的源码仓库选择不同的代码拉取方式即可对代码仓库的认证方式使用jenkins凭证前面有过介绍这里不再多说。
构建触发器
该步骤中使用介绍了几种使job任务自动构建的方法比如
Trigger builds remotely (e.g., from scripts)(触发远程构建 (例如,使用脚本))用于在jenkins外部通过url命令触发需要自己设置身份令牌TOKEN_NAME该参数在配置时已经给出了url的连接比较简单。
Build after other projects are built监控其他job的构建状态触发此job构建。
Build periodically周期进行项目构建输入框内的填写方式参考linux中的crontab如果”分时日月周”中有0可以用H代替。
Poll SCM用于定时检查源码变更如果有变更就拉取代码并执行构建动作。如果没有更新就不会构建同样遵循linux crontab的书写方式。
构建环境
构建环境步骤用于在任务构建之前所做的一些准备配置,比如:
Use secret text(s) or file(s)用于绑定想要使用的jenkins凭证该参数使用的插件为Credentials Binding Plugin。
Send files or execute commands over SSH before the build starts在任务构建之前发送文件或者执行命令该参数使用的插件为Publish Over SSH。
With Ant使用ant与maven工具类似使用的插件为Ant Plugin。
构建
“构建“步骤可以理解为整个jenkins任务工作的核心了在这个步骤中对于中心任务的实现有多种方法相应的参数选项也会比较多这里会根据实际情况酌情介绍部分选项参数。比如
Build/Pubilsh Docker image用于根据指定的Dockerfile构建镜像并上传到私有仓库使用的插件为Docker plugin。
invoke top-level Maven targets前面有做简单介绍这里不重复说明。
Conditional steps(multiple):根据一个或者多个条件进行任务构建,默认自带的选项参数。
Deploy to Kubernetes根据给定的文件部署资源对象到kubernetes集群使用的插件为Kubernetes Continuous Deploy Plugin后面会详细介绍。
Execute Docker command用于执行docker命令或者包括重启容器拉取镜像、打包镜像等基本上支持在bash shell中的所有docker子命令。
Execute sonarQube Scanner执行sonar-scanner命令检测代码质量用到的插件为Sonarqube scanner。
Exec shell使用jenkins启动时设置的用户执行shell命令。
Invoke Ansible ..在jenkins中使用ansible系列命令用到的插件为jenkins ansible plugin。
Provide Configuration files使用在jenkins中配置的某些服务或功能的配置文件使用的插件为Config File Provider Plugin在以后的章节中会介绍到。
Send files or execute commands over ssh通过ssh发送文件或者执行shell命令使用的插件为Publish Over SSH。
有关”构建”步骤中的选项参数就简单的介绍这么多虽然在工作中不一定能全部用到但是了解这些选项的用法对于灵活使用jenkins还是很有帮助的。
构建后操作
构建后操作可以算是Jenkins 任务的收尾工作了在此步骤中可以通过Archive the artifacts选项保存在上一步骤中的生成的归档文件可以使用E-mail Notification选项发送构建详情等信息通知管理人员也可以使用publish over ssh插件发送部署文件或执行命令或者使用Build other project选项根据条件构建其他任务等。
关于Jenkins项目配置的部分选项参数就简单的介绍到这里对于一些选项的使用在以后的章节中会进行详细的介绍。
参数化构建过程
实际工作中在Jenkins UI中配置job或者编写pipeline流水线的时候为了提高Jenkins Job的灵活性肯定会有用到变量的情况。变量的值除了可以使用Jenkins内部预设的变量也可以使用从外部传入的变量。而这些从外部传入的变量在配置jenkins Job时大多数都是以参数化构建的方式定义的。
使用参数化构建可以根据实际情况定义自己想要设定的参数jenkins参数化构建选项提供了十多种类型的参数定义下面针对一些常用类型的参数定义做一个简单的说明。
Boolean Parmeter
布尔型参数该类型的参数的值只能是true或者false。
示例如下勾选This project is parameterized从”添加参数“下拉列表中点击”Boolean Parameter“
在”构建”步骤使用Exec shell编写如下脚本
#!/bin/bash
if ${test_boolean} is true:
then
echo "参数勾选了"
else
echo "参数没有勾选"
fi
传递的参数需要通过${参数名称}引用。
保存后点击Build with Parameters如下所示
构建结果这里就不展示了,有兴趣的自己测试一下。
Choice Parameter
选择参数在jenkins中定义一个参数值列表构建时通过选定的值传给参数并被其他步骤引用。
示例如下:
在”构建”步骤中在”Exec shell” 输入框中输入命令。
#!/bin/bash
echo "current command: git clone $branch http://192.168.176.154/root/test-helloworld.git"
保存后执行job。
构建时可以根据下拉列表中的分支拉取代码。构建结果自己测试即可。
File Parameter
文件参数用于从浏览器表单提交中接受一个文件,并作为构建参数。上传后的文件将会放在当前工作空间($WORKSPACE中指定的位置你可以在构建任务中访问并使用它。
如下示例:
说明:
表单提交中的文件名称就是文件的路径并且是在环境变量中可见的。但是引用该文件时直接使用自定义的文件路径即可。例如把文件放到当前工作目录的test目录不存在会自动创建使用时直接使用该文件路径而不用使用变量。
在”构建“步骤中的”Exec shell” 输入框中输入命令:
#!/bin/bash
cat $WORKSPACE/test/test.xml
点击保存后执行,即可查看该文件的内容。
Extend choice Parameter
顾名思义可扩展的选择参数相比于choice Parameter只能使用单个参数选项外该参数可以同时定义多个参数选项并为每个参数选项设置不同的值该参数使用插件为Extended Choice Parameter Plug-In。
首先看一下该参数支持的参数类型:
其中:
“Name 和Description”参数为自定义。-
该参数支持的类型主要为单选、多选框和输入框至于hidden用处不大这里不做介绍。
单选类型参数
Single Select和Radio Butions均为单选框。
首先看一下Single Select类型选择该类型后点击构建时多个参数会组成一个下拉列表。
其中:
Name为该参数的名称。-
Description该参数的描述。-
Parameter Type为参数类型这里选择的是single select。-
Number of Visible Items可用的参数的数量。-
Delimiter如果设置了多个参数每个参数之间的分隔符。
参数值设置:
说明:
Choose Source for Value该参数得值必须项。-
Choose Source for default Value该参数的默认值非必须。-
Choose Source for Value Description关于该参数值的描述非必须如果没有设置该选项在执行job的时候在参数后面的下拉列表中会直接显示该参数的值如果设置了才参数在执行job的时候在参数后面的下拉列表中会直接显示该参数的值的描述也就是此参数设置的值需要注意的是如果设置了此参数此参数的值数量要与设置的value的值的数量相同如果数量不相同最后的一个值会使用设定的Value的值填充效果可以看下面。
在”构建“步骤中的”Exec shell” 输入框中输入命令:
#!/bin/bash
echo $test_extend_choice
执行时效果如下所示:
说明:
在配置job时对于最后一个job的描述值没有填写这里就会直接显示value的值。-
此时如果选择para1并执行就会打印值value1。
如果设置了基础参数类型为Radio Butions时构建job时显示的是这样的
这里可以看到在选择参数值的时候出现了滚动条,这是因为在设置”可用的参数的数量“的时候,设置的值比实际的参数的数量少造成的,所以为了美观,建议有多少个参数就设置”可用的参数的数量”为多少。
其他设置与使用Single Select时一样这里不再多说有兴趣的可以自己试验一下。
多选类型参数
如果设置了多选框在执行jenkins任务的时候可以设置多个选项参数并使用这些参数指定的值多选框的类型为Multi Select和Check Boxes首先看一下基础版本。
在”Exec Shell“输入框中输入
#!/bin/bash
echo $key
测试一下执行时会输出选定的key的值。
选定哪个参数,就会输出哪个参数对应的值。
除了使用单个Extend choice Parameter以外也可以创建多个Extend choice Parameter组合使用。
示例
比如下面示例:在部署微服务应用的时候,常常会遇到多个微服务应用构建时依赖同一个公共服务的情况,此时在部署多个微服务应用的同时,可以先构建公共服务,在构建应用服务。
首先如果多个项目依赖一个公共服务的情况第一个Extend choice Parameter参数可以使用单选框类型为了美观以及更清楚地列出公共服务列表我使用Radio Buttons类型的参数如下所示
说明:
定义一个依赖服务参数参数选择类型为单选定义两个参数值build和not_build对应的描述分别为”构建依赖服务”和”不构建依赖服务”。
第二个Extend choice Parameter参数配置如下
说明:
该参数名称为app_service描述为应用服务。
在”Exec Shell“输入框中输入如下脚本。
#!/bin/bash
string=$app_service
array=(${string//,/ })
build_job(){
for service in ${array[@]}
do
{
echo "并行构建 $service "
}&
done
wait
}
if [ "$dependency_service" == "build" ]
then
echo "构建依赖服务xxx"
build_job
else
echo "不构建依赖服务"
build_job
fi
执行如下所示:
结果如下:
其次,如果有多个公共服务的时候,可以使用多选框类型的选择参数进行配置,参考上面示例中应用服务的配置即可。
除了使用Check boxes作为多选的选择参数以外还可以用Multi Select只不过该参数在选择多个参数选项的时候需要按住ctrl键操作起来比Check boxes略显麻烦其它都没什么特殊变动这里就不在演示了有兴趣的可以自己尝试一下。
输入类型参数
输入类型参数就比较简单了定义了参数之后参数类型直接使用Text Box类型然后在构建时输入参数的值即可一般用的不多。
Git Parameter
Git Parameter主要用于在构建job之前预先从”源码管理“步骤中设定的git源码库中获取该源码仓库的git标记支持的git标记包括标签、分支、分支或标签、修订、Pull Request并根据标记拉取git代码进行构建。
如下所示点击”Build with Parameter”后会自动列出设定的源码仓库的git标记列出的标记根据设定的git标记类型会有所不同此示例使用的git标记为分支
job设置如下
其中:
参数类型可以根据自己实际情况选择
默认值为必须要填写的参数
此时在设置”源码管理”的时候需要设置分支为上面创建的参数变量。
说明:
Branches to build 虽然名称为指定的分支但是对于上面提到的各种git标记放到这里都是可用的。
Password Parameter
密码类型的参数如果在配置Jenkins任务的时候不想在job中或者console log中显示明文密码可以使用该参数如下所示可以给该参数设置一个默认值。
比如在登录私有docker hub仓库的时候使用该参数。
#!/bin/bash
docker login 192.168.176.155 -u admin -p $test_password_para
虽然整个过程是加密的但是在exec shell中通过echo $test_password_para命令还是能够获取明文密码的并且涉及到密码的使用Jenkins凭据会比使用该参数更有优势所以此参数还是根据个人情况使用吧。
String Parameter
字符串参数最普通的参数类似于Extend choice Parameter中的输入类型参数比较简单这里就不演示了有兴趣的可以自己尝试一下。
有关参数化构建的内容就简单的介绍到这里。
变量
在Jenkins job中除了可以使用上面自定义的变量外还可以使用Jenkins中内置的环境变量内置的环境变量可以在job的任何配置步骤中使用可以通过http://Jenkins_URL/env-vars.html/获取所有的内置变量。如下所示:
……
变量很多但是在工作场景中用的比较频繁也就那么几个比如WORKSPACE变量该变量表示当前job的工作空间也是该job在Jenkins服务器上的绝对路径BUILD_NUMBER变量表示当前job的构建IDJOB_NAME表示当前job的名称也就是jenkins的项目名称。比如在”Exec shell“中输入。
echo $WORKSPACE $BUILD_NUMBER $BUILD_ID $JOB_NAME
构建job后输出的内容如下
关于其他变量的介绍可以自己去试一下,这里不做过多介绍。
视图
使用jenkins的过程中如果Job数量特别大想快速定位想要构建或者修改配置的Job、或者想要对同一个项目组中Job任务进行统一管理可以通过在Jenkins中建立视图来对Job进行管理。jenkins中的视图类似于电脑上的文件夹。默认jenkins中有一个All的视图所有的项目均会在此视图下列出。
创建视图
下面创建一个视图。
在Jenkins的主面板点击”New View“选项也可以点击All视图右面的”+“按钮),在跳转的界面中输入视图名称,如下图所示:
其中:
List view选项比较简单用于从全局job列表中自定义选择要放到该视图下的Job选择Job时可以通过条件过滤或者正则表达式匹配想要放到该视图下的Job同时对于该视图中的显示Job信息的列的字段名称进行自定义添加或者删除如下所示
根据自己实际需求配置即可。
My View选项将会将当前用户所能看到的所有job添加到新创建的视图中。
Pipeline Aggregator View选项用于在jenkins系统中全屏显示pipeline类型job的staeg和Job构建属性信息等一般用处不大。
配置好点击保存就创建了视图。
如果想要删除视图在点击视图名称后直接在jenkins面板中点击”删除视图”即可。并且视图删除后job任务不会被删除。
凭据
凭据cridential是Jenkins访问第三方应用时的认证信息。凭据可以是账号/密码、SSH密钥、加密文件等。Jenkins可以通过设置的凭据与其它第三方应用在可信与可控的范围内进行交互。为了提高安全性Jenkins会对凭据进行加密存储使用该凭据时默认使用凭据ID与第三方应用认证。
Jenkins默认可以存储以下类型的凭据
Secret text - API token之类的token (如GitHub个人访问token),
Username and password - 用户名和密码
Secret file - 保存在文件中的加密内容
SSH Username with private key - SSH 公钥/私钥对
Certificate - PKCS#12 证书文件和可选密码
而如果安装了像docker、kubernetes、openshift这类插件的情况下也会增加对这类系统的凭据类型
Docker Host Certificate Authentication - Docker 仓库认证信息
Kubernetes configurationkubeconfig对kubernetes容器编排系统的认证
openshift usernanme password对openshift系统登录的认证
创建凭据
在jenkins主面板中点击”凭据cridential”—>”系统system”—>”全局凭据Global credentials(unrestricted)“,如下所示:
在跳转的界面中点击”添加凭据add credentials以创建一个Username with password类型的凭据为例。
如下所示:
这里需要说一下ID参数ID参数的值可以自定义如果不填写jenkins会自动生成一个ID至于如何使用这个凭据将在下面的”创建jenkins项目”小节中介绍。
Jenkins凭据在持续交付和部署过程中使用比较频繁在以后的章节中会介绍多种类型的凭据的创建本节有关凭据的内容就简单的介绍到这里。
配置Master-Slave
使用jenkins的时候为了避免Jenkins服务所在的主机资源消耗过大影响Jenkins服务的性能往往会增加一个或多个slave节点来专门执行Jenkins任务这就是我们熟知的Jenkins的Master/slave架构。
Jenkins 的Master/Slave架构相当于Server和agent的概念。Master提供web接口让用户来管理job和slavejob可以运行在master本机或者被分配到slave上运行。一个Master可以关联多个slave用来为不同的job或相同的job的不同配置来服务。
配置Jenkins slave节点不需要安装jenkins服务只需要将jenkins主节点使用的一些基础工具本系列课程用到jdk、maven、git、ansible、docker、sonar等工具栈等安装配置好就可以了。jenkins的slave节点可以是虚拟机也可以是容器可以是静态的主机资源也可以是动态的容器资源鉴于使用动态的容器资源需要相关的jenkins插件方面的知识这里就先介绍一下如何添加静态的主机节点。
点击 “Manage Jenkins(系统管理)”–> “Manage Nodes(节点管理)“可以看到目前只有一个master节点点击左侧的”New Node“给该slave节点设置一个名字比如我这里为”slave1”点击确定
说明:
of executors并发构件数量表示同时进行构建的任务数量根据自己情况填写。
Remote root directory(远程工作目录):为必填项,里面用来保存拉取的应用代码,根据自己情况设定。
Labels标签该slave节点的标签在Jenkins 项目里可以通过该标签匹配到slave节点。
Usage用于设置使用该节点的策略可以设置为尽可能多的使用该节点如上所示也可以设置为only build jobs with label expressions matching this node表示只有jenkins job中设置了指定job任务在该节点运行才会使用该节点。
Launch metheod用于配置jenkins slave节点的启动方式主要分为ssh和jnlp两种其中ssh为master主动连接slave节点jnlp为slave节点主动连接agent图上的配置使用的ssh方式。
Host主机这里写Jenkins Slave服务器的地址。
Credentials(凭据)这里需要添加一下登录Slave服务器的用户名和密码。
Advanced高级用来设置该服务器的一些相关信息。
port如果服务器ssh端口不是22可以设置成别的。
JavaPath这里需要设置一下 slave服务器java可执行文件的绝对路径。比如我的配置为/usr/local/jdk1.8.0_231/bin/java
设置完成后点击最下方的保存按钮。在跳转到的界面然后点击launch Agent按钮。日志如下表示添加节点成功。
节点添加成功后可以修改jenkins项目配置勾选Restrict where this project can be run在出现的框中输入slave节点的名称或者label保存后再次构建该项目就会在新添加的节点构建该项目。如下所示
除了使用vm虚拟机作为jenkins的slave节点以外也可以使用docker容器作为jenkins的slave节点同时slave节点也可以通过容器编排工具进行编排在以后的章节中这些内容都会介绍到并且对于常用的使用动态slave作为job执行的环境的内容也会进行说明敬请期待。
有关jenkins项目配置内容到这里就结束了在后面的章节开始进入实践阶段。

View File

@@ -0,0 +1,983 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
06.Jenkins部署之Docker要点
docker 基础操作
在前面的实践章节介绍了将服务部署到容器中有些读者对于docker可能并不熟悉而且在后面的文章中基本上所有的实践案例都是围绕着容器展开的所以介绍后面的实践内容之前有必要先学习一下docker的基础操作。当然如果你对docker的操作比较熟悉也可以略过此节的内容。
Docker的三大核心组件镜像、容器和镜像仓库。使用docker服务的操作基本上也是围绕这三大核心组件开展的在前面的”基础工具安装“章节对于企业级的镜像仓库已经做了介绍本节主要来介绍一下镜像操作和容器操作。如果你时一个docker老司机那么你也可以略过此章节。
镜像基本操作
本小节主要涉及到的内容如下:
获取镜像
列出镜像
删除镜像
构建镜像
获取镜像
从Docker 官方Registry获取镜像的命令是docker pull。其命令格式为
docker pull [option] [Docker Registry地址]/<仓库名>:<标签>
具体的参数选项可以通过docker pull --help命令查看这里说一下镜像地址名称的格式。
Docker Registry地址的格式一般是
<域名/IP>[:端口号]
默认地址是Docker Hub的地址。
<仓库名>:如上面语法所示,这里的仓库名是两段式名称,即
<用户名>/<镜像名>
对于 Docker Hub如果不给出用户名则默认为library也就是官方镜像。比如
$ docker pull ubuntu:14.04
14.04:Pulling from library/ubuntu
......
Digest:sha256:147913621d9cdea08853f6ba9116c2e27a3ceffecf3b492983ae97c3d643fbbe
Status:Downloaded newer image for ubuntu:14.04
上面的镜像地址没有给出默认使用docker hub下library用户下镜像获取官方library/ubuntu仓库中标签为14.04的镜像。
如果从其他镜像仓库获取,例如:阿里云
docker pull registry.cn-beijing.aliyuncs.com/daimler-jenkins/jenkins-slave-java
如果想要查询官方的某个镜像怎么办?使用
docker search <关键字>
命令可以根据关键字搜索镜像比如我要搜索jenkins镜像就会列出存在的jenkins镜像如下所示
[root@glusterfs-160 ~]# docker search jenkins
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
jenkins Official Jenkins Docker image 4642 [OK]
jenkins/jenkins The leading open source automation server 1904
jenkinsci/blueocean https://jenkins.io/projects/blueocean 490
......
也可以通过在浏览器访问hub.docker.com官方界面通过输入关键字搜素想要获取的镜像这里就不在演示了有兴趣大家可以试一下
列出镜像
要想列出已经下载下来的镜像,可以使用 docker images 命令。
$ docker images
REPOSITORYTAG IMAGE ID CREATED SIZE
redis latest 5f515359c7f8 5 days ago 183MB
nginx latest 05a60462f8ba 5 days ago 181MB
上面列表包含了仓库名、标签、镜像ID、创建时间以及所占用的空间。镜像ID是镜像的唯一标识一个镜像可以对应多个标签。
如果仔细观察会注意到这里标识的所占用空间和在Docker Hub上看到的镜像大小不同。
比如openjdk:latest 镜像大小拉取下来为495 MB但是在 Docker Hub显示的却是 244 MB。这是因为Docker Hub中显示的体积是压缩后的体积。在镜像下载和上传过程中镜像是保持着压缩状态的因此 Docker Hub 所显示的大小是网络传输中更关心的流量大小。
而 docker images命令显示的是镜像下载到本地后展开的大小准确说是展开后的各层所占空间的总和因为镜像到本地后查看空间的时候更关心的是本地磁盘空间占用的大小。
另外一个需要注意的问题是docker images 列表中的镜像体积总和并非是所有镜像实际硬盘消耗。由于 Docker 镜像是多层存储结构,并且可以继承、复用,因此不同镜像可能会因为使用相同的基础镜像,从而拥有共同的层。由于 Docker 使用 Union FS相同的层只需要保存一份即可因此实际镜像硬盘占用空间很可能要比这个列表镜像大小的总和要小的多。
虚悬镜像
如果在images列出的这中镜像既没有仓库名也没有标签并且均为”&lt;none&gt;“的镜像,比如
<none> <none> 00285df0df87 5 days ago 342 MB
这类镜像为虚悬镜像虚悬镜像的产生大多是因为镜像仓库里的镜像更新并重新拉取后旧镜像的标签转移到新镜像上来导致的另一种情况是通过build操作构建镜像时构建失败导致的。一般来说虚悬镜像已经失去了存在的价值是可以随意删除的。
可通过如下命令列出所有的虚悬镜像
$ docker images -f dangling=true
可以用下面的命令删除
$ docker rmi $(docker images -q -f dangling=true)
中间层镜像
为了加速镜像构建、重复利用资源Docker会利用中间层镜像。所以在使用一段时间后可能会看到一些依赖的中间层镜像。
与之前的虚悬镜像不同,有些无标签的镜像很多都是中间层镜像,是其它镜像所依赖的镜像。这些无标签镜像不应该删除,否则会导致上层镜像因为依赖丢失而出错。实际上,如果这些镜像在使用中,默认也是不能删除的。这些镜像也没必要删除,因为相同的层只会存一遍,而这些镜像是别的镜像的依赖,因此并不会因为它们被列出来而多存了一份,无论如何你也会需要它们。
只要删除那些依赖它们的镜像后,这些依赖的中间层镜像也会被连带删除。
默认的docker images列表中只会显示顶层镜像如果希望显示包括中间层镜像在内的所有镜像的话需要加-a参数。
$ docker images -a
列出部分镜像
不加任何参数的情况下docker images命令会列出所有顶级镜像但是有时候我们只希望列出部分镜像。docker images命令有多个参数可以帮助做到这个事情。
#根据仓库名列出镜像
$ docker images Ubuntu
#列出特定的某个镜像,也就是说指定仓库名和标签
$ docker images ubuntu:16.04
根据指定条件列出镜像
比如根据REPOSITORY和Tag列出匹配的镜像
$ docker images --filter=reference='busy*:*libc'
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox uclibc e02e811dd08f 5 weeks ago 1.09 MB
busybox glibc 21c16b6787c6 5 weeks ago 4.19 MB
也可以使用多个filter
$ docker images --filter=reference='busy*:uclibc' --filter=reference='busy*:glibc'
REPOSITORY TAG IMAGE ID CREATED SIZE
busybox uclibc e02e811dd08f 5 weeks ago 1.09 MB
busybox glibc 21c16b6787c6 5 weeks ago 4.19 MB
--filter支持的关键字如下
dangling (boolean - true or false),列出虚悬镜像
label (label= or label==) 根据指定lable列出镜像
before ([:], or) - 过滤出在给定的image id之前创建的镜像
since ([:], or) - 过滤出引用此image id创建的镜像
reference 根据给出的匹配条件过滤出镜像
以特定格式显示
默认情况下docker images 会输出一个完整的表格但是我们并非所有时候都会需要这些内容。比如刚才删除虚悬镜像的时候我们需要利用docker images 把所有的虚悬镜像的ID列出来然后才可以交给docker rmi命令作为参数来删除指定的这些镜像这个时候可以使用-q 参数用于列出镜像的id
$ docker images -q
5f515359c7f8
如果希望自己组织列获取指定的解析结果可以用Go的模板语法。
比如下面的命令会直接列出镜像结果并且只包含镜像ID和仓库名
$ docker images --format "{{.ID}}:{{.Repository}}“
5f515359c7f8: redis
或者打算以表格等距显示,并且有标题行,和默认一样,不过自己定义列:
$ docker images --format "table{{.ID}}\t{{.Repository}}\t{{.Tag}}"
IMAGE ID REPOSITORY TAG
5f515359c7f8 redis latest
format支持如下关键字
.ID
Image ID
.Repository
镜像仓库名称
.Tag
镜像标签
.Digest
镜像 摘要
.CreatedSince
镜像创建后经过多长时间
.CreatedAt
镜像创建时间
.Size
镜像占用空间大小
删除镜像
如果要删除本地的镜像可以使用docker image rm命令其格式为
$ docker image rm [选项] <镜像1> [<镜像2>...]
或者使用
$ docker rmi [OPTIONS] IMAGE [IMAGE...]
可以通过 docker rmi --help 查看更多帮助
#用docker images 命令来配合
可以使用 docker images -q 来配合使用docker rmi这样可以成批的删除希望删除的镜像。删除虚悬镜像的指令是
$ docker rmi $(docker images -q -f dangling=true)
#比如我们需要删除所有仓库名为redis的镜像
$ docker rmi $(docker images -q redis)
#默认情况下,正在运行使用的镜像是不能被删除的,需要先停止容器在进行删除操作,也可以使用 -f 参数强制删除
构建镜像
使用 Dockerfile 定制镜像
要构建镜像就要制定构建的指令而Dockerfile就是包含一些自定义的指令和格式、用来构建镜像的文本文件。
Dockerfile 提供了一系列统一的资源配置语法,用户可以根据这些语法命令自定义配置,构建自定义镜像。
在学习构建镜像之前先学习一下Dockerfile的一些基本介绍以及基本指令
此次Dockerfile主要介绍如下内容:
Docker build 构建过程
Dockerfile 基础指令
使用 Dockerfile 定制镜像
构建过程大致如下:
Docker Client 端收到用户指令解析命令行参数并发送给Docker ServerDocker Server 端收到http请求后
首先创建一个临时目录将context指定的文件系统解压到该目录下
读取并解析Dockerfile
根据解析出的Dockerfile遍历其中所有指令并分发到不同的模块去执行
解析器指令parser为每一个指令创建一个临时容器并在里面执行当前命令然后通过commit使此容器生成一个镜像层。所有层的集合就是build后的结果。最后一次commit的镜像id就会作为此镜像的最终Id
Dockerfile 基础指令
COPY
复制文件,格式如下
COPY <源路径>...<目标路径>COPY ["<源路径1>",..."<目标路径>"]
COPY 指令有两种格式,一种类似于命令行,一种类似于函数调用。
COPY 指令将从构建上下文目录中<源路径> 的文件/目录复制到新的一层的镜像内的<目标路径> 位置。
比如:
COPY package.json /usr/src/app/
<源路径> 可以是多个甚至可以是通配符其通配符规则要满足Go的 filepath.Match规则
COPY hom* /mydir/COPY hom?.txt /mydir/
说明:
<目标路径> 可以是容器内的绝对路径也可以是相对于工作目录的相对路径工作目录可以用WORKDIR指令来指定
<目标路径>不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。
此外还需要注意一点使用COPY指令源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用Git进行管理的时候。
ADD 高级的复制文件
ADD 指令和COPY的格式和性质基本一致。但是在COPY基础上增加了一些功能
比如 <源路径> 可以是一个URL这种情况下Docker引擎会试图去下载这个链接的文件放到<目标路径>下载后的文件权限自动设置为600如果这并不是想要的权限那么还需要增加额外的一层RUN进行权限调整。
另外如果下载的是个压缩包需要解压缩也一样还需要额外的一层RUN指令进行解压缩。所以不如直接使用RUN指令然后使用wget或者curl工具下载处理权限、解压缩、然后清理无用文件更合理。因此这个功能其实并不实用而且不推荐使用。
如果 <源路径> 为一个tar压缩文件的话压缩格式为gzipbzip2以及xz的情况下ADD指令将会自动解压缩这个压缩文件到<目标路径> 去。
在某些情况下这个自动解压缩的功能非常有用比如官方镜像ubuntu中
FROM scratch
ADD ubuntu-xenial-core-cloudimg-amd64-root.tar.gz /...
但在某些情况下如果我们真的是希望复制个压缩文件进去而不解压缩这时就不可以使用ADD命令了。最适合使用ADD的场合就是所提及的需要自动解压缩的场合。
因此在COPY和ADD指令中选择的时候可以遵循这样的原则所有的文件复制均使用COPY指令仅在需要自动解压缩的场合使用ADD。
CMD 容器启动命令
Docker 容器实质就是进程。既然是进程那么在启动容器的时候需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。
在指令格式上一般推荐使用exec格式这类格式在解析时会被解析为JSON数组因此一定要使用双引号”而不要使用单引号。
提到CMD就不得不提容器中应用在前台执行和后台执行的问题。
Docker 不是虚拟机容器中的应用都应该以前台执行而不是像虚拟机、物理机里面那样用upstart/systemd去启动后台服务容器内没有后台服务的概念。
CMD service nginx start 会被理解为 CMD [ "sh","-c", "service nginx start"]因此主进程实际上是sh。那么当 service nginx start命令结束后sh也就结束了sh作为主进程退出了自然就会令容器退出。
正确的做法是直接执行nginx可执行文件并且要求以前台形式运行。比如
CMD ["nginx","-g", "daemon off"]
ENTRYPOINT
ENTRYPOINT 的格式和RUN指令格式一样分为exec格式和 shell 格式。ENTRYPOINT 的目的和CMD一样都是在指定容器启动程序及参数。
  当指定了ENTRYPOINT 后CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数传给 ENTRYPOINT指令即实际执行时将变为
<ENTRYPOINT> "<CMD>"
说通俗了就是相当于在容器内部或者dockerfileCMD执行的命令列表里加上了新传入的参数并且执行
示例如下
首先看一下CMD命令
$ cat Dockerfile
FROM nginx
CMD ["echo","hello"]
$ docker build -t nginx:test .
$ docker run -it nginx:test
效果如下所示:
说明
重新构建的镜像加入了CMD命令此命令会覆盖之前存在镜像里的nginx daeomon off命令所以运行时会打印出新加入的命令并且运行后会自动关闭容器因为命令执行完毕了
在运行一个容器,并且传入命令
docker run -it nginx:test world
说明
新传入的命令 world 会覆盖cmd里的echo hello命令由于此命令不存在所以会报错
修改DockerfileCMD改成ENTRYPOINT
$ cat dockerfile
FROM nginx
ENTRYPOINT ["echo","hello"]
$ docker build -t nginx:ent .
$ docker run -it nginx:ent
$ docker run -it nginx:ent world
从执行结果可以看到world 被当做参数传去了
WORKDIR 指定工作目录
格式:
WORKDIR <工作目录路径>
使用WORKDIR指令可以来指定工作目录或者称为当前目录以后各层的当前目录就被改为指定的目录如该目录不存在WORKDIR 会帮你建立目录。
  在 Shell中连续两行是同一个进程执行环境因此前一个命令修改的内存状态会直接影响后一个命令而在Dockerfile中这两行RUN命令的执行环境根本不同是两个完全不同的容器。
因此如果需要改变以后各层的工作目录的位置那么应该使用WORKDIR指令。
示例
FROM nginx
WORKDIR /home
RUN pwd
ENTRYPOINT ["pwd"]
效果如下所示:
ENV 设置环境变量
格式:
ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...
这个指令是设置环境变量无论是后面的其它指令如RUN还是运行时的应用都可以直接使用这里定义的环境变量。
ENV NODE_VERSION 7.2.0
RUN touch $NODE_VERSION-txt
CMD ["ls"]
下列指令可以支持环境变量展开:
ADD、COPY、ENV、EXPOSE、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD。
ARG 构建参数
格式:
ARG <参数名>[=<默认值>]
构建参数和ENV的效果一样都是设置环境变量。所不同的是ARG所设置的构建环境的环境变量在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用ARG 保存密码之类的信息因为docker history 还是可以看到所有值的。
Dockerfile中的 ARG 指令是定义参数名称,以及定义其默认值。该默认值可以在构建命令 docker build 中用 --build-arg &lt;参数名&gt;=&lt;&gt; 来覆盖。
在1.13 之前的版本,要求--build-arg中的参数名必须在Dockerfile中用ARG定义过才能用。换句话说就是--build-arg指定的参数必须在Dockerfile中使用了。如果对应参数没有被使用则会报错退出构建。
从1.13 开始这种严格的限制被放开不再报错退出而是显示警告信息并继续构建。这对于使用CI系统用同样的构建流程构建不同的Dockerfile的时候比较有帮助避免构建命令必须根据每个Dockerfile的内容修改。
如下示例:
$ cat Dockerfile
ARG full_name
ENV JAVA_APP_JAR $full_name
ENV AB_OFF true
ADD$JAVA_APP_JAR /deployments/
#构建
$ docker build -t image_name --build-arg full_name=full_name .
EXPOSE 声明端口
格式:
EXPOSE <端口1> [<端口2>...]
EXPOSE指令是声明运行时容器提供服务端口这只是一个声明在运行时并不会因为这个声明应用就会开启这个端口的服务。
在Dockerfile中写入这样的声明有两个好处
帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;
在运行时使用随机端口映射时也就是docker run -P时会自动随机映射 EXPOSE的端口。
要将EXPOSE和在运行时使用 -p 宿主端口:容器端口 区分开来。-p是映射宿主端口和容器端口。换句话说就是将容器的对应端口服务公开给外界访问而EXPOSE仅仅是声明容器打算使用什么端口而已并不会自动在宿主进行端口映射。
VOLUME 定义匿名卷
格式:
VOLUME ["<路径1>","<路径2>"...]
VOLUME <路径>
VOLUME /data
  这里的/data目录就会在运行时自动挂载为匿名卷任何向/data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置,比如:
docker run -d -v /mydata:/data xxxx
在这行命令中就使用了mydata这个命名卷挂载到了/data 这个位置替代了Dockerfile中定义的匿名卷的挂载配置。
示例
$ cat dockerfile
FROM nginx
WORKDIR /home
RUN pwd
volume ["/data"]
$ docker build -t nginx:volume .
$ docker run -itd nginx:volume
通过docker inspect docker_id 可查看此volume 映射到本地的位置,创建一个文件,写入测试的内容,然后退出容器,可发现文件在本地还在
USER 指定当前用户
USER 指令和 WORKDIR 相似都是改变环境状态并影响以后的层。WORKDIR 是改变工作目录USER 则是改变之后层的执行RUNCMD 以及ENTRYPOINT 这类命令的身份。
当然,和 WORKDIR 一样USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换
RUN groupadd -r redis && useradd -r -g redisredis
USER redis
RUN ["redis-server"]
如果以root 执行的脚本在执行期间希望改变身份比如希望以某个已经建立好的用户来运行某个服务进程不要使用su或者sudo这些都需要比较麻烦的配置而且在TTY 缺失的环境下经常出错。
# 建立redis用户并使用gosu换另一个用户执行命令
RUN groupadd -r redis && useradd -r -g redis redis
# 设置CMD并以另外的用户执行
CMD ["exec", "gosu","redis","redis-server"]
到此常用的dockerfile的指令就介绍的差不多了熟悉dockerfile的各个指令以后可以帮助我们更加快速的编写dockerfile文件
使用 Dockerdile 定制镜像
了解了Dockerfile的常用指令后编写好Dockerfile就可以通过docker build命令构建镜像了。
首先看一下构建语法
$ docker build --help
$ docker build [OPTIONS] PATH | URL | -
PATH或URL指向的文件为context上下文路径此路径包含build过程中的Dockerfile文件以及其它需要构建到镜像中的资源文件。
使用Dockerfile 定制镜像,那一定是以一个镜像为基础,在其上进行定制。就像下面 redis镜像的容器对其进行修改一样基础镜像是必须指定的。而FROM就是指定基础镜像因此一个Dockerfile中FROM是必备的指令并且必须是第一条指令。
先看一个初学者示例(只做演示用,非最终版):
$ cat Dockerfile
FROM debian:jessie
RUN apt-get update
RUN apt-get install -y gcclibc6-dev make
RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz"
RUN mkdir -p /usr/src/redis
RUN tar -xzf redis.tar.gz -C /usr/src/redis--strip-components=1
RUN make -C /usr/src/redis
RUN make -C /usr/src/redis install
说明:
FROM 指定一个基础镜像,必须存在
RUN 指令用来执行命令行命令,最常用的指令之一
Dockerfile 中每一个指令都会建立一层RUN也不例外。每一个RUN的行为就和刚才我们手工建立镜像的过程一样新建立一层在其上执行这些命令执行结束后commit这一层的修改构成新的镜像。
而上面的这种写法创建了7层镜像。这是完全没有意义的而且很多运行时不需要的东西都被装进了镜像里比如编译环境、更新的软件包等等。结果就是产生非常臃肿、非常多层的镜像不仅仅增加了构建部署的时间也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。Union FS 是有最大层数限制的,比如 AUFS曾经是最大不得超过 42 层,现在是不得超过 127 层。
所以修改上面dockerfile 如下所示:
FROM debian:jessie
RUN apt-get update \
&& apt-get install -y gcclibc6-dev make \
&& wget -O redis.tar.gz "http://download.redis.io/releases/redis-3.2.5.tar.gz" \
&& mkdir -p /usr/src/redis \
&& tar -xzf redis.tar.gz -C /usr/src/redis--strip-components=1 \
&& make -C /usr/src/redis \
&& make -C /usr/src/redis install
另一点需要注意的是Dockerfile的文件名并不要求必须Dockerfile。也可以使用-f参数指定其他Dockerfile名称此时后面的工作目录需要指定Dockerfile的上下文目录。
比如我的Dockerfile在/root/docker/目录下此目录下还有一个aa.json文件Dockerfile如下
FROM debian:jessie
RUN build Deps='gcc libc6-dev make wget'\
&& mkdir -p /usr/src/redis
COPY ./aa.json /usr/src/redis
那么构建的命令为
docker build -t redis:v1 -f /root/docker/Dockerfile /root/docker/
如果目录下有些东西确实不希望构建时传给Docker引擎那么可以用.gitignore一样的语法写一个.dockerignore该文件是用于剔除不需要作为上下文传递给Docker引擎的。
echo ".git" > .dockerignore
在Dockerfile文件所在目录根据上面的docker build命令进行镜像构建
$ docker build -t redis:v3 .
说明:
”.” 表示当前目录可以理解为Dockerfile目录而dockerfile就在当前目录也是在指定的上下文的目录
有关镜像操作的内容就介绍到这里,下面看一下容器的操作
容器操作
本小节主要介绍以下内容:
Docker命令分类
启动容器
后台运行容器
查看容器
进入容器
导入导出容器
删除容器
Docker 命令分类
在进行容器实操之前,先看看容器的一些命令分类
Docker 环境信息 info、version
容器生命周期管理 Create、exec、kill、pause、restart、rm、run、start、stop、unpause
镜像仓库命令 login、logout、pull、push、search
镜像管理 build、images、import、load、rmi、save、tag、commit
容器运维操作 attach、export、inspect、port、ps、rename、stats、top、wait、cp、diff、update
容器资源管理 volume、network
系统信息日志 events、history、logs #events打印容器的实时系统事件 history 打印出指定镜像的历史版本信息logs打印容器中进程的运行日志
更多命令可参考官网
启动容器
启动容器有两种方式一种是基于镜像新建一个容器并启动另外一个是将在终止状态stopped的容器重新启动。
命令语法
$ docker run --help
Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG...]
新建并启动,所需要的命令主要为 docker run。例如下面的命令输出一个 “Hello World”之后终止容器。
$ docker run ubuntu:14.04 /bin/echo 'Hello world'
Hello world
注意:容器启动后只是输出 hello world输出后容器关闭退出
下面的命令则启动一个 bash终端允许用户进行交互。
$ sudo docker run -t -i -m 2G --cpu-shares 1536 ubuntu:14.04 /bin/bash
root@af8bae53bdd3:/#
其中:
-t 参数让Docker分配一个伪终端pseudo-tty并绑定到容器的标准输入上
-i 参数则让容器的标准输入保持打开。
-c 参数用于给运行的容器分配cpu的shares值
-m 参数用于限制为容器的内存信息,以 B、K、M、G 为单位
-v 参数用于挂载一个volume可以用多个-v参数同时挂载多个volume
-p 参数用于将容器的端口暴露给宿主机端口 格式hostportcontainerport
需要注意的是,如果没加-d后台运行参数从容器bash终端退出后此容器会终止运行
启动和终止容器
可以使用 docker start/stop/restart 命令,直接将一个已经存在的容器启动/关闭/重启。容器的核心为所执行的应用程序所需要的资源都是应用程序运行所必需的。除此之外并没有其它的资源。可以在伪终端中利用ps或top命令来查看进程信息。
$ docker start/stop/restart <container_Id/containerName>
后台运行容器
更多的时候需要让Docker在后台运行而不是直接把执行命令的结果输出在当前宿主机下。可以通过添加 -d 参数来实现。
注意:
容器是否会长久运行是和docker run指定的命令有关和 -d 参数无关。docker 容器后台运行必须加 -it参数使用-d参数启动后会返回一个唯一的 id也可以通过 docker ps 命令来查看容器信息。
要获取容器的输出信息,可以通过如下命令获取。
docker logs -f [container ID or container NAMES]
终止后台运行的容器
可以使用docker container stop来终止一个运行中的容器当Docker容器中指定的应用结束时容器也自动终止。 只启动了一个终端的容器,用户通过 exit 命令或 Ctrl+d 来退出终端时,所创建的容器立刻终止。
终止状态的容器可以用docker container ls -a或者docker ps -a 命令看到。
例如:
$ sudo docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ba267838cc1b ubuntu:14.04 "/bin/bash" 30 minutes ago Exited (0) About a minute ago trusting_newton
98e5efa7d997 training/webapp:latest "python app.py" About an hour ago Exited (0) 34 minutes ago backstabbing_pike
处于终止状态的容器可以通过docker start命令来重新启动(如果不是意外终止的容器)。docker restart命令会将一个运行态的容器终止然后再重新启动它。
查看容器
容器启动了那么该如何查看呢通过docker ps命令docker ps命令不加参数情况下用来列出正在运行的容器。
首先看一下ps命令的参数
$ docker ps -h
Flag shorthand -h has been deprecated, please use --help
Usage: docker ps [OPTIONS]
List containers
Options:
-a, --all Show all containers (default shows just running)
-f, --filter filter Filter output based on conditions provided
--format string Pretty-print containers using a Go template
-n, --last int Show n last created containers (includes all states) (default -1)
-l, --latest Show the latest created container (includes all states)
--no-trunc Don't truncate output
-q, --quiet 只显示容器id
-s, --size Display total file sizes
-a 参数用来显示所有的容器,无论状态是运行的还是退出的
$ docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
336dded76f59 gitlab/gitlab-ee "/assets/wrapper" 2 months ago Up 2 months (healthy) 0.0.0.0:80->80/tcp, 0.0.0.0:2222->22/tcp, 0.0.0.0:2443->443/tcp gitlab
854e0ae79353 goharbor/harbor-jobservice:v1.9.1 "/harbor/harbor_jobs…" 4 months ago Exited (128) 4 months ago harbor-jobservice
.....
-f 参数用来根据关键字过滤出想要列出的容器,比如
根据name获取指定的容器
$ docker ps --filter "name=nostalgic"
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
715ebfcee040 busybox "top" 3 seconds ago Up 1 second i_am_nostalgic
9b6247364a03 busybox "top" 7 minutes ago Up 7 minutes nostalgic_stallman
根据指定退出码列出容器
$ docker ps -a --filter 'exited=0'
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ea09c3c82f6e registry:latest /srv/run.sh 2 weeks ago Exited (0) 2 weeks ago 127.0.0.1:5000->5000/tcp desperate_leakey
$ docker ps -a --filter 'exited=137'
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b3e1c0ed5bfe ubuntu:latest "sleep 1000" 12 seconds ago Exited (137) 5 seconds ago grave_kowalevski
根据容器状态列出容器status
可选参数有 created, restarting, running, removing, paused, exited and dead.
$ docker ps --filter status=running
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
715ebfcee040 busybox "top" 16 minutes ago Up 16 minutes i_am_nostalgic
$ docker ps --filter status=paused
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
673394ef1d4c busybox "top" About an hour ago Up About an hour (Paused)
使用-f参数还可以使用format关键字将获取到的内容格式化或者获取想要的指定的容器信息
例如要获取容器的容器id和容器名称
$ docker ps -a --filter 'exited=0' --format "table {{.ID}}\t{{.Names}}"
CONTAINER ID NAMES
0f15dba71e5c runner-Zm2_mVvw-project-2-concurrent-0-cache-c33bcaa1fd2c77edfc3893b41966cea8
5e3cff545d1f runner-Zm2_mVvw-project-2-concurrent-0-cache-3c3f060a0374fc8bc39395164f415a70
format参数支持的关键字如下
.ID
Container ID
.Image
Image ID
.Command
command 命令
.CreatedAt
创建时间
.RunningFor
运行时间
.Ports
expose的端口
.Status
容器状态
.Size
容器占用的disk大小
.Names
容器名称
.Label
特殊的容器标签
有关容器的查看操作就简单介绍到这里
进入容器
在使用-d参数时容器启动后会进入后台。某些时候需要进入容器进行操作有很多种方法包括使用docker attach/exec 命令或 nsenter 工具等。
exec 命令
sudo docker exec -it 775c7c9ee1e1 /bin/bash
更简单的,下载 .bashrc_docker并将内容放到 .bashrc 中
$ cat .bashrc_docker
alias docker-pid="sudo docker inspect --format '{{.State.Pid}}'"
alias docker-ip="sudo docker inspect --format '{{ .NetworkSettings.IPAddress }}'"
#the implementation refs from https://github.com/jpetazzo/nsenter/blob/master/docker-enter
function docker-enter() {
#if [ -e $(dirname "$0")/nsenter ]; then
#Change for centos bash running
if [ -e $(dirname '$0')/nsenter ]; then
# with boot2docker, nsenter is not in the PATH but it is in the same folder
NSENTER=$(dirname "$0")/nsenter
else
# if nsenter has already been installed with path notified, here will be clarified
NSENTER=$(which nsenter)
#NSENTER=nsenter
fi
[ -z "$NSENTER" ] && echo "WARN Cannot find nsenter" && return
if [ -z "$1" ]; then
echo "Usage: `basename "$0"` CONTAINER [COMMAND [ARG]...]"
echo ""
echo "Enters the Docker CONTAINER and executes the specified COMMAND."
echo "If COMMAND is not specified, runs an interactive shell in CONTAINER."
else
PID=$(sudo docker inspect --format "{{.State.Pid}}" "$1")
if [ -z "$PID" ]; then
echo "WARN Cannot find the given container"
return
fi
shift
OPTS="--target $PID --mount --uts --ipc --net --pid"
if [ -z "$1" ]; then
# No command given.
# Use su to clear all host environment variables except for TERM,
# initialize the environment variables HOME, SHELL, USER, LOGNAME, PATH,
# and start a login shell.
#sudo $NSENTER "$OPTS" su - root
sudo $NSENTER --target $PID --mount --uts --ipc --net --pid su - root
else
# Use env to clear all host environment variables.
sudo $NSENTER --target $PID --mount --uts --ipc --net --pid env -i $@
fi
fi
}
# 将以上内容放到 .bashrc文件下
$ source ~/.bashrc
这个文件中定义了很多方便使用Docker的命令例如docker-pid可以获取某个容器的PID而docker-enter可以进入容器或直接在容器内执行命令。
$ docker-enter c0c00b21f8f8
root@c0c00b21f8f8:~# ls
root@c0c00b21f8f8:~# pwd
/root
root@c0c00b21f8f8:~# exit
logout
$ docker-pid c0c00b21f8f8
11975
删除容器
可以使用 docker rm 来删除一个处于终止状态的容器。 例如
$sudo docker rm trusting_newton(<container_name>)
trusting_newton
如果要删除一个运行中的容器,可以添加 -f 参数。Docker 会发送 SIGKILL 信号给容器。
清理所有处于终止状态的容器
用 docker ps -a 命令可以查看所有已经创建的包括终止状态的容器,如果数量太多要一个个删除可能会很麻烦,用 docker rm $(docker ps -a -q) 可以全部清理掉。
注意:这个命令其实会试图删除所有的包括还在运行中的容器,不过就像上面提过的 docker rm 默认并不会删除运行中的容器。
删除所有终止的容器
$ docker container prune    #删除所有停止的容器
-f 强制删除, --filter "until=24h" 删除停止超过24小时的容器
有关docker容器的内容就简单介绍到这里

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,419 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
08.Jenkins集成Ansible持续部署服务到Docker容器
在前面的jenkins基础实践章节中简单介绍了使用jenkins内置插件实现应用代码的发布通过上一节Ansible基础知识的介绍本节来使用Ansible对jenkins基础实践章节的内容继续进行一下重构和提优化。
使用与配置
在前面的章节中已经对ansible做过基础的介绍这里不多说首先看一下Jenkins与Ansible的集成如何使用。
Jenkins与Ansible的集成使用主要有三种方式
一种是在步骤中通过在”Exec shell“中直接输入Ansible命令来进行工作
一种就是使用Jenkins中安装的Ansible插件来工作
然后就是在pipeline中使用ansible插件本章节暂时不涉及此内容
下面分别介绍一下前两种的使用方法以一个freestyle类型的job为例。
使用Exec shell
使用”Exec shell“在输入框中输入
ansible localhost -m shell -a "hostname"
说明:
该命令用来在本机执行hostname命令。-
localhost是ansible内置的一个组表示ansible本机。
除了可以使用ansible Ad-hoc命令意外还可以使用ansible-playbook命令执行playbook有兴趣的可以自己尝试一下。
使用ansible插件
在”构建“步骤中点击”Invoke Anisble Ad-hoc command“配置如下
其中:
Ansible installation 用来选择ansible的命令此处的命令获取方式为一个下拉列表该列表的值是从Jenkins的全局工具中配置ansible相关命令的环境变量获取到的在前面的章节中我们已经做过此操作如果没有的话需要重新添加ansible的环境变量比如我这里在Jenkins中配置的ansible命令的环境变量如下
Host pattern用来填写要操作的主机列表可以是一个主机ip也可以是一个主机组名称。无论是那种都要确保该主机ip或者主机组名在ansible的主机清单inventory文件中存在localhost和all除外这两个是ansible内置的组
Inventory主机清单列表此处可以自己定义是使用文件还是手动输入默认使用的文件为/etc/ansible/hosts
module此处为要使用的ansible的模块名称在ansible命令中通过-m参数指定的值ansible实现主机批量管理是通过模块实现的如果不填写默认为command模块
Playbook path只有使用Invoke Ansible Playbook选项参数才会有此参数配置用来执行要执行的plyabook文件
Module arguments or command to execute这里需要指定在目标主机上执行的命令也就是ansible命令中-a参数指定的值
Credentials用来指定ansible连接目标主机时对目标服务器认证的凭据如果在ansible主机清单文件中配置了可以不填写
Vault Credentials加密后的凭据只支持file和text类型的加密凭据
become在目标主机执行命令的用户仅支持目标主机上的用户为sudo用户并且没有password
sudo : 提升用户权限为root
除了可以使用Ad-hoc命令外还以可以使用ansible-playbook命令ansible-valut命令分别对应的参数选项为Invoke Ansible Playbook(用于执行playbook脚本)和Invoke Ansible vault(用来对playbook内容加密处理)。
配置保存后执行job执行结果如下
在实际工作中通过jenkins与ansible集成使用ansible ad-hoc命令的情况还是比较少见的大多数都是通过去执行playbook来进行代码发布和部署工作。
遇到的问题
无论是使用ansible ad-hoc命令还是使用ansib-playbook命令如果执行job后报如下错误找不到ansible命令
或者(连接不上远程主机)。
导致以上报错的出现的原因有两种一种是真正意义上的命令找不到或者到主机网络不可达主机认证失败另一种就是Jenkins用户权限问题导致的这里简单说明一下第一种情况可能不多见因为安装好ansible后默认就配好了ansible的全局环境变量至于主机不可达与主机认证相信大家都会测试在服务器终端执行一下命令就知道了这里不多说主要说一下Jenkins的权限问题。
jenkins安装后在执行job任务的时候默认使用jenkins用户进行而jenkins用户对ansible以及使用root的ssh-key连接远程服务器默认是没有权限的。所以要确保用户jenkins对这些命令和文件具有执行权限。这里给出两种解决此问题的方法。
方法一
修改Jenkins配置文件。
$ vim /etc/sysconfig/jenkins
#修改JENKINS_USER并去掉当前行注释
JENKINS_USER="root"
修改Jenkins相关文件夹用户权限根据实际情况做修改也可以略过此步骤
chown -R root:root /var/lib/jenkins
chown -R root:root /var/cache/jenkins
chown -R root:root /var/log/jenkins
重启Jenkins若是其他方式安装的Jenkins则重启方式略不同
systemctl restart jenkins
方法二
配置jenkins的用户终端修改jenkins用户shell为bash。
cat /etc/passwd
jenkins:x:989:985:Jenkins Automation Server:/var/lib/jenkins:/bin/bash
配置jenkins用户连接ssh免秘钥。
[root@ansible ]# su jenkins
bash-4.2$ ssh-keygen -t rsa
bash-4.2$ ssh-copy-id root@ip
使用哪一种方法都可以但是建议使用方法一配置好后重新执行job就可以了。
基础示例
了解完jenkins与ansible集成如何使用后下面演示一下使用ansible-playbook部署服务。
部署服务到tomcat
以在”Jenkins基础实践“章节部署的test-helloworld为例在该章节中使用内置插件虽然实现了项目部署的自动化但是在向大规模的主机中部署项目的时候使用publish over ssh插件显然已经很难满足要求所以下面看一下如何用playbook实现该项目的部署。
脚本如下:
$ cat /home/single-playbook-helloworld.yaml
- hosts: "{{ host }}"
gather_facts: False
vars:
war_file: "{{ workspace }}/target/Helloworldwebapp.war"
project_dir: "/usr/share/tomcat/webapps/{{ project_name }}"
tasks:
- name: 判断目录是否存在
shell: ls {{ project_dir }}
register: dict_exist
ignore_errors: true
- name: 关闭tomcat
shell: systemctl stop tomcat
- name: 备份旧代码
shell: chdir={{ project_dir }}/ tar -czf /bak/{{ project_name }}-{{ build_num }}-$(date -d "today" +"%Y%m%d_%H%M%S").tar.gz {{ project_dir }}/
when: dict_exist is succeeded
ignore_errors: true
- name: 删除旧版本的配置文件
file:
state: absent
dest: "{{ project_dir }}"
when: dict_exist is succeeded
- name: clean cache|清除缓存
shell: chdir={{ project_dir }}/../../ nohup rm -rf work/
- name: 创建目录
file:
state: directory
dest: "{{ project_dir }}"
mode: 0755
- name: 解压war包
unarchive:
src: "{{ war_file }}"
dest: "{{ project_dir }}/"
copy: yes
mode: 0755
- name: 启动tomcat
shell: systemctl restart tomcat
流程说明:
该脚本适用于新建项目和已经在用的项目的服务部署;
为了提高playbook的灵活性在playbook中使用了变量分别指定了目标主机、war包目录、服务部署到远程服务器的路径服务备份路径等提高playbook的灵活性降低维护成本
在playbook的开始会判断项目部署的目录是否存在以区分该项目是新建项目还是已经在运行的项目
无论是新项目还是老项目部署应用都要关闭tomcat服务
备份旧代码 任务会根据”判断目录是否存在“任务的执行结果进行判断,决定要不要执行当前任务;
上面的脚本既可以项目中使用插件配置也可以在“Exec shell“中执行命令我这里为了简便直接在”Exec shell”中输入命令。
/opt/apache-maven-3.5.4/bin/mvn install
ansible-playbook -i /etc/ansible/hosts /home/single-playbook-helloworld.yaml -e "workspace=${WORKSPACE} host=192.168.176.160 project_name=hello_world build_num=$BUILD_ID"
执行结果如下:
这样就通过ansible将服务部署到vm中去了。
下面在看一个示例。
部署服务到容器
使用ansible将微服务部署到容器中去大致流程与上面的示例相似但是对于容器和ansible的使用技术有了稍微提高。
同样引用”Jenkins实践“章节中”部署服务到容器“的示例对比上面的ansible示例来简单说明一下使用内置插件部署服务到容器的局限性
将服务通过镜像部署到单独的容器上如果想要满足负载均衡和高可用的需求就需要将该镜像部署到多台服务器去了如果使用了如swarm、kubernetes、mesos这样的容器编排工具只要通过命令替换一下镜像名称即可前提是没有其他特殊配置的情况但是如果要将容器部署到多个虚拟主机上并通过如haproxy/nginx之类的负载均衡软件进行代理很显然这种方法就不适用了
其次如果项目数量庞大在jenkins中显然不能每个项目都要使用publish over ssh插件配置如果中途要修改某些项目的某些配置还需要一个项目一个项目的去修改这样就和普通的人肉运维没有什么区别浪费时间的同时还降低了工作效率
再者如果多个项目同时在一台jenkins主机上构建的话会加大jenkins负载严重时可能会导致系统或者jenkins服务崩溃影响所有项目的构建部署操作。
首先使用ansible将在Jenkins基础基础实践章节中”部署服务到容器“的部署流程先过一遍如下
[root@ansible ansible]# cat /tmp/dev-test.yaml
---
- hosts: 192.168.176.160
remote_user: root
gather_facts: no
vars:
jar_name: "{{ workspace }}/fw-base-nop/target/fw-base-nop.jar"
remote_dest: /data/base/nop
tasks:
- name: copy jar file
copy: src={{ jar_name }} dest={{ remote_dest }}
- name: stop container
shell: docker stop base-nop
- name: delete container
shell: docker rm base-nop
- name: delete image
shell: docker rmi base/nop
- name: build_image
shell: "cd {{ remote_dest }} && docker build -t base/nop ."
- name: run container
shell: docker run --network host -e TZ="Asia/Shanghai" -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/fw-base-nop:/data/logs/fw-base-nop --name base-nop base/nop
在jenkins的命令行配置如下将原来使用Send files or execute commands over ssh变成使用execute shell。
然后在执行,执行结果如下:
说明:
上面示例使用ansible-playbook将在jenkins里执行的命令进行了重写拉取代码和代码编译配置没有变。
该脚本加入了两个变量其中workspace是从外部传入的变量使用了job的路径remote_dest用于指定远程服务器存放jar包和dockerfile的目录。
由上图可看到使用这个playbook执行成功了但是该脚本的成功执行有如下前提-
必须保证远程服务器的remote_dest路径存在本项目之前已经创建对于新建项目还是要手动创建的。
Dockerfile也要事先存在于目标主机上同样对于新项目还需要手工创建。
在执行的所有任务列表中只要有一个任务失败那么整个playbook部署就会失败并退出。例如如果jenkins新添加的项目没有容器启动没有构建的镜像又或者老项目这些都存在但是容器运行一段时间后异常退出stop container任务又会报错部署还是会失败所以这个playbook基本是不成立的。
如果有新的jenkins项目还需要在次修改playbook的内容显然这也是频繁并且麻烦的操作。
所以基于上面可能会出现的问题对这个playbook进一步优化如下
- hosts: "{{ target_host }}"
remote_user: root
gather_facts: False
vars:
jar_src: "{{ jar_file }}"
dest_dict: "/data/{{ project_name }}/"
tasks:
- name: 判断目录是否存在
shell: ls {{ dest_dict }}
register: dict_exist
ignore_errors: true
- name: 创建相关目录
file: dest="{{ item }}" state=directory mode=755
with_items:
- "{{ dest_dict }}"
- /data/logs/{{ project_name }}
when: dict_exist is failure
- name: 拷贝jar包和dockerfile到目标主机
copy: src={{ item }} dest={{ dest_dict }}/
with_items:
- '{{ jar_file }}'
- '/etc/ansible/Dockerfile'
- name: 查看容器是否存在
shell: "docker ps -a --filter name={{ project_name }} |grep -v COMMAND"
ignore_errors: true
register: container_exists
- name: 查看容器状态
shell: "docker ps -a --filter name={{ project_name }} --format '{{ '{{' }} .Status {{ '}}' }}'"
ignore_errors: true
register: container_state
when: container_exists.rc == 0
- name: 关闭容器
shell: "docker stop {{ project_name }}"
when: "('Up' in container_state.stdout)"
ignore_errors: true
- name: 删除容器
shell: "docker rm {{ project_name }}"
when: container_exists.rc == 0
ignore_errors: true
- name: 查看镜像是否存在
command: "docker images --filter reference={{ project_name }}* --format '{{ '{{' }} .Repository {{ '}}' }}:{{ '{{' }}.Tag {{ '}}' }}'"
register: image_exists
ignore_errors: true
- name: 删除镜像
shell: "docker rmi -f {{ item }}"
loop: "{{ image_exists.stdout_lines }}"
ignore_errors: true
when: image_exists.rc == 0
- name: 镜像构建
shell: "cd {{ dest_dict }} && docker build -t {{ image_name }} --build-arg project_name={{ project_name }} ."
- name: 启动容器
shell: 'docker run {{ run_option }} --name {{ project_name }} {{ image_name }}'
说明
hosts 指定的主机需要在inventory清单文件中存在这里可以写一个主机ip也可以写一个主机组名同样也可以通过变量的形式传入。
上面 playbook里vars参数下定义的两个变量分别代表jar包所在路径、要拷贝jar包和Dockerfile到远程服务器的目录为了区分每个项目为每个项目根据项目名称创建目录。
任务”查看容器是否存在“定义了一个变量container_exists如果该值为0时说明通过上面命令执行时有输出也就是容器存在无论是处于启动还是关闭状态为非0时说明命令执行没有输出结果放到此示例也就是没有容器存在。
任务”查看容器状态”和”查看镜像状态”使用的语法大致相同有些不同的是在ansible的playbook中执行时默认会对”{{ }}“进行转义所以这里需要对特殊符号进行处理如上例所示如果不明白可参考前面ansible基础章节的”转义”小节。
在”镜像构建“任务列表根据项目名称传入参数进行构建同时镜像名也是根据项目名称和git short id自定义的看下面
Jenkins里执行时需要传入参数
#!/bin/bash
#获取git commit的short id
git_id=`git rev-parse --short HEAD`
#定义项目名称
project_name=fw-base-nop
#定义image
image_name="${project_name}:${git_id}"
#查找jar包路径
cd ${WORKSPACE}/ && jar_file=`find "$(pwd)" ./ -name ${project_name}.jar |head -1`
#定义容器运行时的参数
container_run_option="--network host -e TZ="Asia/Shanghai" -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/${project_name}:/data/logs/${project_name}"
#执行playbook传入参数
ansible-playbook -i /etc/ansible/hosts /root/dev-deploy.yaml -e "{'jar_file':${jar_file},'project_name':'${project_name}','image_name':'${image_name}' ,'run_option':'$container_run_option','target_host':'192.168.176.160'}"
说明:
每次新添加项目时只要修改项目名称即可,如果对容器运行时的参数有要求的话也可以修改一下。
由于我的项目名称和maven打包生成的jar包名称相同所以这里没有在定义jar包名称可根据自己需要自定义。
上面的”定义image”、”查找jar包路径”、”定义容器运行时参数” 等变量设置可以全部放在playbook中去。
对于目标主机的定义此脚本使用变量的方式传入了一个ip也可以传递主机组名称这样就可以在多台主机上部署服务了。
该文目的是为了演示一下实现持续交付和持续部署的流程所以playbook的实现用了很多shell模块对于容器操作任务列表也可以通过docker_containers参数去实现有兴趣的可以去试一下我这里就不给大家演示了。
由于构建镜像时使用了变量所以dockerfile也需要重构一下。
FROM fabric8/java-alpine-openjdk8-jre
ARG project_name
ENV JAVA_APP_JAR ${project_name}.jar
COPY $JAVA_APP_JAR /deployments/
CMD java -jar /deployments/$JAVA_APP_JAR
使用优化后的playbook基本上可以满足微服务下服务部署的需求了但依然存在不足之处。
对于使用不同镜像的服务没有对dockerfile做处理可以通过设置变量传参的方式实现。
对于项目名称与jar包名称不同的服务没有做预处理同样添加一个变量也能解决此问题。
如果不想将这些变量的定义放到”Exec shell“输入框中去也可以通过”参数化构建过程“选项去定义这些变量的值或者将一些可以自动获取值的参数放到playbook文件中只保留需要手动输入值的参数。
解决了最初提到的两个问题下面来看第三个问题关于jenkins的构建时的编译与镜像打包等操作。如果在多个项目同时构建的情况下难免遇到Jenkins卡死或者服务崩溃的情况笔者曾经遇到过这是就需要通过添加slave节点的方式来解决此问题。
jenkins的slave节点可以部署在虚拟机上也可以部署在容器中也可以通过配置动态生成。在前面的文章中有介绍如何添加与使用slave节点比较简单这里就不重复介绍了同时需要注意的是在slave节点安装相应的工具。
到这里本节要介绍的内容就结束了。回顾本节的内容演示了如何将服务部署到vm的tomcat服务以及docker容器中去至于部署到容器编排系统中将在课程末尾介绍。

View File

@@ -0,0 +1,684 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
10.实现自动化引擎之Jenkins Pipeline脚本式语法
前面章节介绍了pipeline的声明式语法 本节通过对比来介绍一下脚本式语法的使用。虽然两种类型的语法都是建立在底层流水线的子系统上的语法但是脚本式流水线在使用一些常用插件的时候显得更加容易脚本式流水线只有stage指令在声明式流水线中的stages和steps关键字在脚本式流水线中是不能使用的。下面针对在声明式流水线中介绍到的部分关键字介绍一下在脚本式流水线中的实现方法。
node
声明式流水线中使用agent关键字指定执行Jenkins流水线的代理在脚本式流水线中通过node关键字来指定代理节点。
基本示例如下
node('jenkins-slave1') {
stage('test1') {
sh 'hostname'
}
}
其中jenkins-slave为jenkins代理节点的名称或者标签名称如果”()“为空则默认会从所有的节点中选择一个slave节点
除了可以为所有stage提供一个全局的代理节点以外也可以为不同的stage提供不同的代理节点。
如下示例
node() {
stage('test-node'){
node('jenkins-slave1'){
stage('test1'){
sh 'hostname'
}
}
}
stage('test-node2'){
node('jenkins-slave169'){
stage('test2'){
sh 'hostname'
}
}
}
}
该示例为不同的stage步骤使用了不同的slave节点
使用node也可以用容器作为agent比如
node() {
docker.image('maven').inside {
stage('te') {
sh "hostname"
}
}
docker.image('nginx').inside {
stage('te1') {
sh "hostname"
}
}
}
该配置针对不同的流水线stage使用不同的镜像启动容器提供脚本执行的环境docker.image().inside()是docker pipeline插件里的一个指定镜像并启动容器的方法在以后的章节会介绍这里只需要知道可以这么操作即可
基于上面示例的思考,是不是可以在不同的节点上分别启动容器来进行不同的工作呢?答案是可以的,如下示例
node() {
stage('test1'){
node('slave1'){
docker.image('maven').inside {
stage('te') {
sh "hostname"
}
}
}
}
stage('test2'){
node('slave2'){
docker.image('nginx').inside {
stage('te1') {
sh "hostname"
}
}
}
}
}
该示例会在两个slave节点上分别启动容器作为流水线的执行环境。既然使用脚本式语法能实现在不同的slave节点启动不同的容器在声明式语法中也能做到在以后的章节中会有说明
tool
声明式流水线中使用tools定义在jenkins系统中设置的工具的环境变量在脚本式流水线中使用def 和tool关键字来使用这些工具例如
node{
def maven = tool name: 'maven-3.5.4'
env.PATH = "${maven}/bin:${env.PATH}"
stage('test'){
sh 'mvn --version'
}
}
该指令的语法片段也可以通过片段生成器生成。
withEnv
在声明式pipeline中通过environment来定义流水线中用到的环境变量而在脚本式pipeline中使用withEnv来定义全局或者局部环境变量。
比如定义maven的环境变量
node() {
withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
sh 'mvn --version'
}
}
也可以写成这样
def maven = 'maven-3.5.4'
withEnv(["PATH+MAVEN=${tool maven}/bin"]) {
sh 'mvn --version'
}
也可以直接指定实际的值
withEnv(['MYTOOL_HOME=/usr/local/']) {
sh '$MYTOOL_HOME/bin/start'
}
parallel(并行)
parallel除了可以使用声明式语法定义外也可以使用脚本式语法定义。
下面的例子可以放到脚本式语法中执行
node {
stage('Test') {
parallel slave1: {
node('jenkins-slave1') {
try {
sh 'hostname'
}
finally {
echo "test"
}
}
},
slave169: {
node('jenkins-slave169') {
sh 'hostname'
}
}
}
}
同时在两个节点执行shell命令
properties
声明式脚本通过parameters来定义流水线中使用到的参数在脚本式语法中则需要通过properties关键字来定义在stage中使用的参数可以通过片段生成器生参考下面成相应的语法片段。
如下示例-
生成的语法片段如下
properties([
buildDiscarder(
logRotator(artifactDaysToKeepStr: '',
artifactNumToKeepStr: '',
daysToKeepStr: '2',
numToKeepStr: '4')
),
parameters([string(defaultValue: 'v1',
description: '镜像tag',
name: 'tag',
trim: true)
])
])
示例如下
properties([
buildDiscarder(
logRotator(artifactDaysToKeepStr: '',
artifactNumToKeepStr: '',
daysToKeepStr: '2',
numToKeepStr: '4')
),
parameters([
string(
defaultValue: 'v1',
description: '镜像tag',
name: 'tag',
trim: true
)
])
])
node{
stage('test1'){
echo "${params.tag}"
}
}
这里需要说明的是properties语法片段既可以放到node{}里也可以放到node{}上面
异常处理
在使用jenkins的时候难免遇到job执行失败的情况。声明式语法中可以通过post关键字对故障进行处理而在脚本式流水线中则需要使用 try/catch/finally块来处理故障。当step失败 ,无论什么原因,它们都会抛出一个异常。
如下示例
node {
stage('Example') {
try {
sh 'exit 1'
currentBuild.result = 'SUCCESS'
}
catch (exc) {
currentBuild.result = 'FAILURE'
throw exc
}finally{
if(currentBuild.currentResult == "ABORTED" || currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE"') {
echo "---currentBuild.currentResult
result is:${currentBuild.currentResult
}"
}
else {
echo "---currentBuild.currentResult
result is:${currentBuild.currentResult
}"
}
}
}
}
需要注意的是try语句后面一定要有catch或者finally语句catch与finally语句可以同时存在也可以只存在一个。
Jenkinsfile
通过学习pipeline的两种语法对于如何编写脚本式和声明式pipeline脚本应该有了基本的认识实际工作中使用的jenkinsfile大多数是用声明式脚本编写的当然脚本式也能用)至于在什么情况下使用哪种类型的语法取决于个人对两种语法的熟悉程度。有些情况下比如在后面章节要用到的docker插件kubernetes插件使用脚本式语法可以很方便的与这些插件集成而如果使用声明式语法就会复杂一些所以在准备使用pipeline之前有必要先做一下调研。
jenkinsfile脚本文件通常用于放到源码仓库中在配置jenkins流水线项目时通过checkout script from SCM的方式拉取jenkinsfile和所需的文件拉取下来以后会自动进行流水线操作。
比如在定义流水线脚本使用方式时使用Pipeline Script的方式配置
node {
checkout scm
}
说明
checkout scm这里应该为实际的拉取代码的指令用于从代码仓库拉取代码。比如git clone等。
此时放到Jenkinsfile中的流水线脚本就不需要使用node{}块包含了
也可以通过Pipeline script from SCM的方式配置如下所示-
需要注意的是脚本路径指定的文件名称为从git仓库获取的根目录下的文件名称。文件拉取到本地后就会自动执行流水线操作。
了解了Jenkinsfile文件如何使用后下面介绍一下针对两种pipeline语法都能使用的一些指令和关键字。
变量
pipeline脚本支持使用变量。在pipeline中的变量分为多种形式可以自定义变量可以使用jenkins的环境变量也可以将通过命令获取的结果动态设置为变量等。下面对这些变量一一进行介绍
自定义变量
Jenkins 使用与 Groovy 相同的规则进行变量赋值。Groovy 支持使用单引号或双引号声明一个字符串,例如:
def singlyQuoted = 'Hello'
def doublyQuoted = "World"
对于变量的引用,可以使用单引号和双引号。如果没有特殊字符,用单引号和双引号是一样的;如果要执行多行命令,用三个单引号或三个双引号。如果有特殊字符需要解释,用双引号,需要转义的情况,用\转义符。
在单引号之间的所有特殊字符都失去了特殊含义;而在双引号之间的绝大多数特殊字符都失去了特殊含义,除了以下特例:
$ 美元号用来提取变量的值
` 反冒号用于执行命令
\ 反斜杠用来转义字符
比如:
def username = 'Jenkins'
echo 'Hello Mr. ${username}'
echo "I said, Hello Mr. ${username}"
其结果是:
Hello Mr. ${username}
I said, Hello Mr. Jenkins
执行命令示例如下:
sh '''
whoami
pwd
ls -ltra
'''
结果
+ whoami
root
+ pwd
/var/lib/jenkins/workspace/test-mytest
+ ls -ltra
total 4
drwxr-xr-x 2 root root 6 Mar 16 17:02 .
drwxr-xr-x 8 root root 4096 Mar 16 17:02 ..
而对于定义多个变量如果每个变量都都使用def指令定义那酒显得有些重复造轮子了。groovy官方也给出了解决办法对于上面的示例可以写成这样
def cc=[username='jenkins',version="v2.19.0"]
引用时也简单
echo "$cc.usernmae $cc.version"
如果要在脚本式语法中使用通过def命令定义的变量需要使用script{}块将变量定义的步骤包含起来。
环境变量
在 Jenkins 流水线中可以定义整个流水线使用的环境变量也可以使用jenkins自带的环境变量。
对于如何自定义环境变量取决于使用的是声明式还是脚本式流水线语法它们设置环境变量的方法不同。关于在声明式和脚本式语法中使用环境变量的方法在前面已经介绍过这里不再多说下面看一下使用jenkins内置的环境变量。
Jenkins 流水线通过全局变量 env 提供环境变量,它在 Jenkinsfile 文件的任何地方都可以使用。Jenkins 流水线中可访问的完整的环境变量列表记录在 ${YOUR_JENKINS_URL}/pipeline-syntax/globals#env
比如:
BUILD_ID-
当前构建的 ID与早期jenkins版本的 BUILD_NUMBER 是完全相同的。
BUILD_NUMBER-
当前构建号,比如 “153”。
BUILD_TAG-
字符串 jenkins-${JOB_NAME}-${BUILD_NUMBER}。可以放到源代码、jar 等文件中便于识别。
BUILD_URL-
可以定位此次构建结果的 URL比如 http://buildserver/jenkins/job/MyJobName/17/
EXECUTOR_NUMBER-
用于识别执行当前构建的执行者的唯一编号(在同一台机器的所有执行者中)。这个就是你在“构建执行状态”中看到的编号,只不过编号从 0 开始,而不是 1。
JAVA_HOME-
如果你的任务配置了特定的 JDK工具那么这个变量就被设置为此 JDK 的 JAVA_HOME。当设置了此变量时PATH 也将包括 JAVA_HOME 的 bin 子目录。
JENKINS_URL-
Jenkins 服务器的完整 URL比如 https://example.com:port/jenkins/ (注意:只有在“系统设置”中设置了 Jenkins URL 才可用)。
JOB_NAME-
本次构建的项目名称
NODE_NAME-
运行本次构建的节点名称。对于 master 节点则为 “master”。
WORKSPACE-
workspace 的绝对路径。也是job的路径
……
更多变量内容参考pipeline-sytanx即可
示例如下
pipeline {
agent any
stages {
stage('Example') {
steps {
echo "Running ${env.BUILD_ID} on ${env.JENKINS_URL}"
}
}
}
}
动态设置变量
环境变量可以在运行时设置可以通过shell命令linux sh、Windows 批处理脚本bat和 Powershell 脚本powershell获取变量的值并提供给下面的各stage阶段使用。各种脚本都可以返回 returnStatus 或 returnStdout。
下面是一个使用 shshell的声明式脚本的例子既有 returnStatus 也有 returnStdout
pipeline {
agent any
environment {
// 使用 returnStdout
CC = """
${sh(
returnStdout: true,
script: 'echo "clang"'
)}
"""
// 使用 returnStatus
EXIT_STATUS = """
${sh(
returnStatus: true,
script: 'exit 1'
)}
"""
}
stages {
stage('Example') {
environment {
DEBUG_FLAGS = '-g'
}
steps {
sh 'printenv'
}
}
}
}
说明:
使用 returnStdout 时,返回的字符串末尾会追加一个空格。可以使用 .trim() 将其移除。
该指令可以通过片段生成器生成语法片段
相对于脚本式语法就简单的多
node {
stage('s'){
script {
cc=sh(returnStdout: true, script: 'hostname').trim()
}
echo "$cc"
}
}
条件判断
Jenkinsfile 从顶部开始向下串行执行执行过程中难免遇到使用条件判断的情况声明式语法中可以通过when关键字做一些基础的判断但是在脚本式语法中无法使用when关键字。而使用if/else语句在两种pipeline语法中都可以使用并且提高条件判断的灵活性。
在pipeline中使用的if/else语句同样遵循Groovy语法如下示例
node {
stage('Example') {
if (env.BRANCH_NAME == 'master') {
echo 'I only execute on the master branch'
} else {
echo 'I execute elsewhere'
}
}
}
或者如下示例
script{
test_result=sh(script:"ls /tmp/uu.txt",returnStatus:true)
echo test_result
if(test_result == 0){
echo "file is exist"
}else if(test_resultt == 2){
echo "file is not exist"
}else{
error("command is error,please check")
}
}
}
fileExists
该关键字用来判断当前工作空间下指定的文件是否存在,文件的路径设定时使用基于当前工作空间的相对路径,返回的结果为布尔值。该关键字的使用语法可以通过片段生成器来生成。
如下示例
script {
json_file = "${env.WORKSPACE}/testdata/test_json.json"
if(fileExists(json_file) == true) {
echo("json file is exists")
}else {
error("here haven't find json file")
}
}
说明:
该示例用于判断json_file变量指定的文件是否存在
error指令用于定义并返回自定义的错误信息
dir
dir()方法用于改变当前的工作目录在dir语句块填写要进入的目录路径即可。
示例如下
stages{
stage("dir") {
steps{
echo env.WORKSPACE
dir("${env.WORKSPACE}/fw-base-nop"){
sh "pwd"
}
}
}
}
deletedir
deleteDir()方法默认递归删除WORKSPACE下的文件和文件夹没有参数通常它与dir步骤一起使用用于删除指定目录下的内容。
示例如下
stage("deleteDir") {
steps{
script{
sh("ls -al ${env.WORKSPACE}")
deleteDir() // clean up current work directory
sh("ls -al ${env.WORKSPACE}")
}
}
}
或者删除指定目录
node {
stage('s'){
dir('/base'){
deleteDir()
}
}
}
script
script 步骤需要script-pipeline块并在流水线中执行。 对于大多数用例来说声明式流水线中的script步骤是不必要的该关键字大多数情况下用来执行命令。
示例如下
pipeline {
agent any
stages {
stage('Example') {
steps {
echo 'Hello World'
script {
def browsers = ['chrome', 'firefox']
for (int i = 0; i < browsers.size(); ++i) {
echo "Testing the ${browsers[i]} browser"
}
}
}
}
}
}
其中通过def定义的变量可以在下面的stage的中被引用
stash/unstash
stash用于将文件保存起来以便同一次构建的其他stage或step使用 如果整个流水线在同一台机器上执行那stash是多余的stash一般用于跨Jenkins node使用 stash步骤会将文件存储在tar文件中对于大文件的stash操作将会消耗Jenkins master的计算资源Jenkins官方文档推荐当文件大小为5100MB时应该考虑使用其他替代方案
stash 指令的参数如下
name字符串类型保存文件的集合的唯一标识
allowEmpty布尔类型允许stash内容为空
excludes字符串类型排除文件如果排除多个使用逗号分隔
includes字符串类型stash文件留空表示全部
useDefaultExcludes布尔类型true使用Ant风格路径默认排除文件 Ant风格路径表达式
除了name参数其他参数都是可选的excludes和includes使用的是Ant风格路径表达式
与stash对应的是unstash该指令用于取出之前stash的文件-
unstash步骤只有一个name参数即stash时的唯一标识通常情况下stash与unstash步骤同时使用示例如下
pipeline {
agent none
stages {
stage('stash') {
agent { label "master" }
steps {
dir('target') {
stash(name: "abc", include: "xx.jar")
}
}
}
stage("unstash") {
agent { label "jenkins-slave1" }
steps {
script {
unstash("abc")
cp xx.jar /data
...
}
}
}
}
}
unstash根据stash指定的name的值获取文件等流水线执行完毕后文件会删除
archiveArtifacts
jenkins流水线的archiveArtifacts指令也是用来保存文档文件与stash不同的是该指令会将文件保存到本地流水线执行完成后不会被销毁保存的文件会放到到 Jenkins jobs/JOB_NAME/builds/BUILD_NO目录下
示例如下
pipeline {
agent any
stages {
stage('Archive') {
steps {
archiveArtifacts artifacts: '**/target/*.jar', onlyIfSuccessful: true
}
}
}
}
archiveArtifacts指令包含多个参数其中artifacts参数为必须参数onlyIfSuccessful参数表示只有成功时保存文件其他参数使用可以参考官方文档
片段生成器
jenkins内置的Snippet Generator工具片段生成器用于将插件或者关键字的使用语法动态的生成steps语法片段添加到流水线中片段生成器界面可以从pipeline项目中最底部的pipeline syntax超链接中进入也可以直接通过${YOUR_JENKINS_URL}/pipeline-syntax访问
Jenkins对于片段生成器分成了两种类别一种是基于pipeline声明式语法的片段生成器Declarative Directive Generator菜单另一种是基于jenkins内置插件以及脚本式语法使用片段生成器菜单的片段生成器下面分别对这两种生成器进行介绍
基于声明式语法的片段生成器用于在不知道关键字语法的情况下生成语法片段同样是访问上面的连接点击Declarative Directive Generator菜单如下所示-
可以看到在下拉框中显示了这些指令的相关语法选中任意指令配置好后点击下面的Generate Declarative Directive即可生成相应的语法片段
而对于声明式脚本以及内置插件使用的片段生成器直接点击片段生成器菜单即可点击Sample Setp会列出所有支持的插件列表选中一个比如下图所示
根据需要配置好必须项点击下方的Generate Pipeline Script就会生成相应的代码片段
然后就可以复制粘贴放到pipeline项目中的脚本中去了
有关Jenkins脚本式语法的内容就介绍到这里在下一节中将使用实践案例在对本节以及上节学习的内容进行实践以加深理解

View File

@@ -0,0 +1,948 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
11.Pipeline语法进行持续交付与基础实践
在前两节介绍了使用声明式流水线和脚本式流水线的基本语法,本节通过一些基础实践来加深一下对前两节内容的理解。由于每个人的实际情况和理解程度不一样, 笔者会将要学习的内容从最基础开始演示采用不断优化代码的方式尽量将在之前pipeline章节介绍的语法通过示例都展示出来。
使用声明式语法进行持续交付与基础实践
Agent代理
有些公司在技术上可能没有达到使用容器作为流水线agent代理的条件但这并不影响使用pipeline脚本在虚拟机上编译代码构建镜像。所以节内容中使用的agent代理均为虚拟机至于如何使用容器作为agent代理将在以后的章节介绍。
配置agent代理没有什么要特别说明的直接通过agent{}指令配置要使用的代理节点的label或者名称即可.
以一个基础示例开始本节的内容比如使用master主机执行shell命令
pipeline{
agent { node { label 'master'} }
stages{
stage('test'){
steps{
sh "hostname"
}
}
}
}
需要注意的是在编写声明式脚本时在stage()中必须加上对该stage的描述虽然是自定义的内容但是不添加也会报错
本次pipeline脚本的全部操作在jenkins-slave1节点进行所以对于agent定义如下
agent { node { label 'jenkins-slave1' } }
使用agent指令还是比较简单的
代码拉取与编译
下面创建一个流水线类型的Jenkins Job然后编写pipeline脚本如下
pipeline {
agent { node { label 'jenkins-slave1' } }
stages {
stage('代码拉取并编译'){
steps {
sh "git clone http://root:[email protected]/root/base-nop.git"
echo "开始编译"
sh ' source /etc/profile && cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
}
}
}
}
使用最基础的代码拉取并编译操作只用了两行命令并且拉取代码操作还使用了明文的用户名密码,这显然是不能符合要求的。
使用在”pipeline语法“章节介绍的片段生成器可以将代码拉取操作换成语法片段前面有过介绍
如下所示
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
同样对于在使用mvn命令编译代码时如果遇到maven命令找不到或者jdk环境变量找不到的情况除了使用shell命令刷新环境变量的方式也可以用tools指令定义这些工具的环境变量。
比如:
tools {
maven 'maven-3.5.4'
jdk 'jdk-1.8'
}
对于代码编译如果不想使用cd命令进入指定的目录进行编译pipeline提供了ws指令用于指定目录进行操作所以上面的编译代码步骤也可以写成如下示例
ws('directory'){
mvn clean install
}
那么上面最初的脚本就可以改成这样
pipeline {
agent { node { label 'jenkins-slave1' } }
tools {
maven 'maven-3.5.4'
jdk 'jdk-1.8'
}
stages {
stage('代码拉取并编译'){
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
ws("$WORKSPACE/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
}
}
}
说明:
cd命令的基础路径是当前工作空间${WORKSPACE})路径
ws 指令里使用的目录需要是绝对路径
如果想要在pipeline中测试本地主机或者本地路径是哪个可通过执行hostname命令或者pwd命令获取
使用sonar
既然是持续交付那么肯定少不了代码质量分析。上面的示例只是将代码进行了编译正常流程还需要进行代码质量分析这就用到了之前搭建的sonarqube平台。在前面《Jenkins插件安装》章节有介绍如何使用代码质量分析工具sonarQube。这里不重复说明接下来主要看一下在流水线脚本中使用soncarQube以及sonar-scanner。
本章节我们使用的节点全是基于vm的机器所以此次介绍的sonar-scanner工具的使用只是基于在Jenkins UI中配置的sonar-scanner命令工具。
在前面的章节有介绍sonar-scanner的命令以及参数在pipeline中使用时我们只需要根据片段生成生成sonar-scanner的语法片段即可。如下所示
选择好凭据以后生成语法片段然后将在之前章节中使用的sonar-scanner命令以及参数粘贴到withSonarQubeEnv(){}代码块里即可,如下所示:
stage('sonar'){
steps{
script{
def sonarqubeScannerHome = tool name: 'sonar-scanner-4.2.0'
withSonarQubeEnv(credentialsId: 'sonarqube') {
sh "${sonarqubeScannerHome}/bin/sonar-scanner -X "+
"-Dsonar.login=admin " +
"-Dsonar.language=java " +
"-Dsonar.projectKey=${JOB_NAME} " +
"-Dsonar.projectName=${JOB_NAME} " +
"-Dsonar.projectVersion=${BUILD_NUMBER} " +
"-Dsonar.sources=${WORKSPACE}/fw-base-nop " +
"-Dsonar.sourceEncoding=UTF-8 " +
"-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " +
"-Dsonar.password=admin "
}
}
}
}
这样就配置好了在pipeline中使用sonarqube与sonar-scanner。在执行完代码编译步骤后就会自动执行sonar-scanner命令。
构建并推送镜像
基础版
上面示例使用pipeline脚本就完成了代码的编译工作下面看一下将代码构建成镜像并推送到私有仓库的操作还是先看一下基础的代码。
stage('构建镜像'){
steps {
sh "cp $WORKSPACE/fw-base-nop/target/fw-base-nop.jar /data/fw-base-nop/"
sh "cd /data/fw-base-nop && docker build -t fw-base-nop:${BUILD_ID} ."
}
}
stage('上传镜像'){
steps {
sh "docker tag fw-base-nop:${BUILD_ID} 192.168.176.155/library/fw-base-nop:${BUILD_ID}"
sh "docker login 192.168.176.155 -u admin -p da88e43d88722c2c9ca09da644eeb015"
sh "docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}"
}
}
该基础代码虽然可以实现我们的目的,但是对于有想法的年轻人,对于示例中的纯命令式操作还是不能接受的,对于部分步骤还是能够优化的。比如:
构建镜像阶段:
1、对于构建产物jar包的引用使用了cp命令拷贝到agent代理节点上指定的目录如果遇到添加其他项目时要使用这个pipeline的模板还要去频繁修改jar包路径和名称所以这里还需要简单的优化一下。
2、对于构建镜像的命令也可以直接通过命令docker build -t fw-base-nop:${BUILD_ID} /data/fw-base-nop实现。
3、需要在指定的目录此示例为/data/fw-base-nop/预先存放Dockerfile文件可参考第四章节中的Dockerfile
上传镜像阶段:
1、需要先将上一步骤构建的镜像添加tag以后才能上传到指定私有仓库。在构建镜像时我们可以直接将镜像构建成想要通过tag命令标记的镜像名。
2、上传到私有仓库之前还需要对私有仓库进行认证简单的几个步骤就使用了如上示例这么多命令。
针对上面列出的问题,部分步骤可以通过使用插件或指令进行优化:
比如使用find命令从当前路径中查找构建的产物并且将镜像直接构建成名称为Docker Registry地址/仓库名:标签的格式,可以修改成如下:
stage('构建镜像'){
steps {
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
}
sh """
cp $jar_file /data/fw-base-nop/
docker build -t 192.168.176.155/library/fw-base-nop:${BUILD_ID} /data/fw-base-nop/.
"""
}
}
使用withCredentials方法对私有仓库进行认证
stage('上传镜像'){
steps {
withCredentials([usernamePassword(credentialsId: 'auth_harbor', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
sh "“”
docker login -u ${env.dockerHubUser} -p ${env.dockerHubPassword} 192.168.176.155
docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}
"""
}
}
}
因为是在虚拟机主机上进行镜像构建操作为了避免job执行次数过多导致构建的镜像在宿主机保存数量过多的情况还需要在最后阶段执行删除镜像的操作比如
stage('删除本地镜像'){
steps{
sh "docker rmi -f 192.168.176.155/library/fw-base-nop:${BUILD_ID}"
}
}
根据上面的更改完整的pipeline脚本如下
pipeline {
agent { node { label 'jenkins-slave1'}}
tools {
maven 'maven-3.5.4'
jdk 'jdk-1.8'
}
stages {
stage('代码拉取并编译'){
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始编译 "
ws("${WORKSPACE}/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
}
stage('构建镜像'){
steps {
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
}
sh """
cp $jar_file /data/fw-base-nop/
docker build -t 192.168.176.155/library/fw-base-nop:${BUILD_ID} /data/fw-base-nop/.
"""
}
}
stage('上传镜像'){
steps {
withCredentials([usernamePassword(credentialsId: 'auth_harbor', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
sh "docker login -u ${env.dockerHubUser} -p ${env.dockerHubPassword} 192.168.176.155"
sh 'docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}'
}
}
}
stage('删除本地镜像'){
steps{
sh "docker rmi -f 192.168.176.155/library/fw-base-nop:${BUILD_ID}"
}
}
}
}
执行结果如下:-
-
-
这样基础版的pipeline脚本算是完成了。
进阶版
虽然脚本是完成了但是回顾前面pipeline章节介绍的相关语法还是能够发现一些存在的问题并且有些步骤还可以进行优化对部分步骤的代码还能进行缩减。
比如:
1、 在镜像构建步骤中使用的存放dockerfile和jar包的目录可以通过挂载共享存储的方式挂载到agent代理节点指定的目录下这样如果Dockerfile发生变更或者要拷贝镜像到指定目录的路径发生变更时只需要修改一次即可并且在任何slave节点都可以通过挂载共享存储的方式使用不会限制job只使用固定的agent代理节点。
2、对于”上传镜像“步骤中对私有仓库认证的操作除了使用withCredentials方法外也可以直接在agent指定代理节点上对私有仓库进行认证这样在上传镜像时就可以直接略过向私有仓库认证操作。
3、对于”删除本地镜像“步骤中的删除操作可以直接放到”上传镜像“步骤中。
下面针对上面列出的问题进行简单的优化。
1、对于使用共享存储和在代理节点对私有仓库认证的操作在代码中不会体现共享存储我这里使用nfs模拟代替目录没有变
2、仓库认证直接在agent代理节点上通过docker login命令登录即可这样就可以将上传镜像和删除镜像两个stage和并为一个同时为了规范操作将在代码编译步骤中的构建镜像操作也放到上传镜像操作stage中。
如下所示:
stage('上传并删除本地镜像'){
steps {
sh """
docker build 192.168.176.155/library/fw-base-nop:${BUILD_ID}
docker push 192.168.176.155/library/fw-base-nop:${BUILD_ID}
docker rmi -f 192.168.176.155/library/fw-base-nop:${BUILD_ID}
"""
}
}
对于项目数量较大以及需要对项目分组管理并且想要使用同一套pipeline脚本作为模板的时候可以将该pipeline脚本中使用的部分通用配置设置成变量每次新建项目只更改变量的值即可。
比如:
对于项目名称,项目构建的产物名称,项目所属的组可以设置成变量
对于私有仓库的地址以及项目所属组也可以通过变量的方式配置
对于镜像的版本,同样可以通过变量的方式配置
根据上面列出的优化思路,后续的脚本修改如下:
pipeline {
agent { node { label 'jenkins-slave1'}}
tools {
maven 'maven-3.5.4'
jdk 'jdk-1.8'
}
environment {
project_name = 'fw-base-nop'
jar_name = 'fw-base-nop.jar'
registry_url = '192.168.176.155'
project_group = 'base'
}
stages {
stage('代码拉取并编译'){
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
ws("${WORKSPACE}/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
}
stage('构建镜像'){
steps {
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
}
sh """
cp $jar_file /data/$project_group/$project_name/
docker build -t $registry_url/$project_group/$project_name:${BUILD_ID} /data/$project_group/$project_name/.
"""
}
}
stage('上传镜像'){
steps {
sh """
docker push $registry_url/$project_group/$project_name:${BUILD_ID}
docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}
"""
}
}
}
}
配置比较简单学会使用environment关键字即可。
除了使用environment参数以外也可以在job构建时传入参数用来代替部分变量这就需要用到前面学过的parameters指令了。
比如对于设定的jar包名称和所属项目组分别添加一个string类型的参数和choice类型的参数。
如下代码:
parameters {
string defaultValue: 'fw-base-nop.jar', description: 'jar包名称必须以.jar后缀结尾', name: 'jar_name', trim: false
choice choices: ['base', 'open', 'tms'], description: '服务所属项目组', name: 'project_group'
}
既然定义了外部参数传入变量对于通过environment指令定义的这两个变量也就没必要存在了。
这样在执行pipeline job时就会变成参数化构建类型的job如下图所示-
构建时输入想要的参数值即可使用parameters这种方式个人觉得比较臃肿可能使用的场景不对所以对于parameters指令要根据实际情况考虑要不要使用。
成型版
本次版本对于进阶版的示例没有什么明显的改动只是将与docker相关的操作通过docker pipeline插件去执行。有关Docker pipeline 插件相关的内容还没有介绍,此处通过示例先简单演示一下。
对于stages中的代码变化如下
stages {
stage('代码拉取并编译'){
steps {
checkout([$class: 'GitSCM', branches: [[name: "*/master"]], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始编译 "
ws("${WORKSPACE}/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name ${jar_name} |head -1").trim()
}
sh """
cp $jar_file /data/${project_group}/$project_name/
"""
}
}
stage('构建并上传镜像'){
steps {
script {
def customImage = docker.build("$registry_url/${project_group}/$project_name:${BUILD_ID}", "/data/${project_group}/$project_name/.")
customImage.push()
}
sh """
docker rmi -f $registry_url/${project_group}/$project_name:${BUILD_ID}
"""
}
}
}
这样使用docker pipeline插件中的build和push方法就简单实现了镜像的构建和推送。从上面的脚本可以看到在使用插件方法时使用了script{}块包含是因为Jenkins中的插件基本上都是适配与脚本式语法而对于声明式语法需要使用script块来声明使用脚本式语法的部分功能。
在以后的章节会详细介绍docker pipeline插件的使用语法这里先简单演示一下如何使用。
使用多agent
对于分工比较明确的agent节点比如某些agent代理节点只能进行代码编译操作某些agent代理节点只能进行docker镜像构建操作或者对于同一个项目有不同的脚本语言编写的代码需要使用使用不同的编译工具构建时时对于这些特殊情况可以使用多agent构建方式在不同的步骤使用不同的agent节点进行项目构建操作。
以上面的代码为示例在代码拉取和编译步骤使用master节点在镜像构建和push步骤使用jenkins-slave1节点代码如下
pipeline {
agent none
tools {
maven 'maven-3.5.4'
jdk 'jdk-1.8'
}
environment {
project_name = 'fw-base-nop'
jar_name = 'fw-base-nop.jar'
registry_url = '192.168.176.155'
project_group = 'base'
}
}
stages {
stage('代码拉取并打包'){
agent { node { label 'master'} }
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
ws("${WORKSPACE}/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
}
sh "scp $jar_file root@jenkins-slave1:/data/$project_group/$project_name/"
}
}
stage('上传镜像'){
agent { node { label 'jenkins-slave1'}}
steps {
script {
def customImage = docker.build("$registry_url/${project_group}/$project_name:${BUILD_ID}", "/data/${project_group}/$project_name/.")
customImage.push()
}
sh """
docker rmi -f $registry_url/${project_group}/$project_name:${BUILD_ID}
"""
}
}
}
}
说明:
1、 使用多agent代理节点进行构建时最顶部的agent需要指定为none。
2、 由于使用了不同的agent节点对于构建产物需要从master主机拷贝到jenkins-slave1主机上相对于使用同一个agent这应该算是比较复杂的操作了吧但是如果用上共享存储或使用同一个agent这应该不算是问题。
3、 对于代码编译和镜像构建分别使用不同的工具进行不同类型的操作所以使用两个stage即可完成所有操作。
异常处理
在使用pipeline脚本执行任务的时候难免遇到执行出现异常的情况并且出现异常后流水线会自动终止退出为了第一时间获取流水线执行的结果或者状态就需要我们在脚本中加入异常处理的代码。
前面介绍过声明式语法使用post指令根据任务执行状态进行下一步的处理操作下面简单看一下post在该示例中的应用。
post {
always {
echo "xxx"
}
success {
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 更新正常",
body: """
详情:<br>
Jenkins 构建 ${currentBuild.result} <br>
项目名称 ${env.JOB_NAME} <br>
项目构建id${env.BUILD_NUMBER} <br>
URL ${env.BUILD_URL} <br>
构建日志:${env.BUILD_URL}console
""",
to: "[email protected]",
recipientProviders: [[$class: 'CulpritsRecipientProvider'],
[$class: 'DevelopersRecipientProvider'],
[$class: 'RequesterRecipientProvider']]
)
}
failure {
echo "构建日志:${env.BUILD_URL}console"
}
unstable {
echo "构建日志:${env.BUILD_URL}console"
}
changed {
echo "changed"
}
}
将post指令放到全局stages步骤下面该post指令对于流水线脚本执行的多种状态结果通过使用emailext插件发送邮件通知管理人员。邮件内容可以根据自己实际情况定义可以参考上面的示例。
如果觉得将每种状态结果分别单独处理比较麻烦可以根据job执行状态进行分组处理比如将状态分为成功和失败两种状态可以修改脚本如下
post {
always {
script{
sh 'docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}'
if (currentBuild.currentResult == "ABORTED" || currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ){
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建结果",
body: """
详情:\n<br>
Jenkins 构建 ${currentBuild.currentresult} '\n'<br>
项目名称 ${env.JOB_NAME} "\n"
项目构建id${env.BUILD_NUMBER} "\n"
URL ${env.BUILD_URL} \n
构建日志:${env.BUILD_URL}console
""",
to: "[email protected]",
recipientProviders: [[$class: 'CulpritsRecipientProvider'],
[$class: 'DevelopersRecipientProvider'],
[$class: 'RequesterRecipientProvider']]
)
}else{
echo "构建成功"
}
}
}
}
}
使用判断语句对执行结果进行判断并分别处理。有关currentBuild变量的介绍可以参考”全局变量“界面中currentbuild方法。
Parallel并行执行
在实际工作中可能会遇到并行执行一些job的情况。比如同属一个项目组的多个应用服务依赖同一个或多个基础服务如果要构建应用服务时需要优先构建基础服务这种情况先我们就可以使用并行构建的方案。
例如我们有基础服务service1和service2应用服务app1、app2和app3有些情况下开发提交了基础服务代码到service1或service2同时提交了应用服务app1 app2 app3的代码需要构建这种情况下如果先手动构建service1和service2等着两个都构建完了再去手动构建三个应用服务无疑是繁琐的。
所以本小节就用pipeline中的Parallel关键字来解决类似的问题代码如下
pipeline {
agent any
parameters {
extendedChoice description: '是否构建基础服务', descriptionPropertyValue: 'base_service1,base_service2', multiSelectDelimiter: ',', name: 'base_service', quoteValue: false, saveJSONParameterToFile: false, type: 'PT_CHECKBOX', value: 'service1,service2', visibleItemCount: 2
extendedChoice description: '是否构建应用服务', descriptionPropertyValue: 'app_service1,app_service2,app_service3', multiSelectDelimiter: ',', name: 'app_service', quoteValue: false, saveJSONParameterToFile: false, type: 'PT_CHECKBOX', value: 'app1,app2,app3', visibleItemCount: 3
}
stages(){
stage('deploy'){
parallel{
stage('deploy service1'){
when {
expression {
return params.base_service =~ /service1/
}
}
steps{
sh "echo build job base_service1"
// build job: 'service1'
}
}
stage('deploy service2'){
when {
expression {
return params.base_service =~ /service2/
}
}
steps{
sh "echo build job base_service2"
// build job: 'service2'
}
}
}
}
stage('build job'){
parallel{
stage('构建应用服务1') {
when {
expression {
return params.app_service =~ /app1/
}
}
steps('build') {
sh "echo build job app1"
// build job: 'app1'
}
}
stage('构建应用服务2') {
when {
expression {
return params.app_service =~ /app2/
}
}
steps('build') {
sh "echo build job app2"
// build job: 'app2'
}
}
stage('构建应用服务3') {
when {
expression {
return params.app_service =~ /app3/
}
}
steps('build') {
sh "echo build job app3"
// build job: 'app3'
}
}
}
}
}
}
说明:
1 、在该示例中构建job操作使用shell命令代替在实际工作中需要使用build job指令来构建一个job示例中被注释的部分为构建job的代码。
2、 除了使用parameters指令指定参数外也可以通过在”General“步骤中勾选This project is parameterized来自定义参数使用方法在前面章节有介绍此处不再重复说明。
有关使用声明式语法进行持续交付与部署的实践到此结束。
使用脚本式语法进行持续交付与部署实践
通过在声明式语法章节的学习对于代码编译镜像构建整个流程应该都不用重复介绍了本节与使用声明式语法实践一样也是只通过使用虚拟机作为agent代理节点来执行流水线脚本对于部分重复的内容会一笔带过如果有疑问可以随时联系笔者。
node代理
声明式语法通过agent关键字使用代理脚本式语法通过node关键字使用代理。
根据声明式语法中的流程以及示例,使用脚本式语法的基础示例代码如下:
node('jenkins-slave1'){
stage('代码编译'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始编译 "
ws("$WORKSPACE/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
stage('镜像构建'){
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
}
sh """
cp $jar_file /data/base/fw-base-nop/
docker build -t 192.168.176.155/library/fw-base-nop:${BUILD_ID} /data/base/fw-base-nop/.
"""
}
}
代码编译
在上面基础示例中简单实现了一下使用脚本式语法进行代码拉取、编译、镜像构建的操作,下面根据在声明式脚本中的示例,将部分代码通过使用脚本式语法指令的方式实现,对于一些可能存在的问题以及先关优化方案在前面已经做过介绍,这里就不在重复介绍了。只需要关注一下脚本的变更即可。
定义工具
比如对于代码编译时用到的工具的环境变量的定义在脚本式语法中可以使用tool和withEnv指令设置jdk和maven工具的环境变量代替source /etc/profile命令代码如下所示
def jdk = tool name: 'jdk-1.8'
env.PATH = "${jdk}/bin:${env.PATH}"
stage('代码编译'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
ws("$WORKSPACE/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
}
该脚本使用def指令和env命令定义了jdk的环境变量使用withenv指令定义了maven的环境变量。
上面定义变量的方式也可以写成如下:
node {
def jdk=tool name: 'jdk-1.8'
def mvn=tool name:'maven-3.5.4'
env.PATH = "${jdk}/bin:${mvn}/bin:${env.PATH}"
stage('代码编译'){
......
echo "开始打包 "
ws("$WORKSPACE/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
}
构建并推送镜像
在构建并推送镜像阶段同样可以使用docker pipeline插件实现镜像的构建与推送操作。
代码如下:
stage('镜像构建'){
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name fw-base-nop.jar |head -1").trim()
}
sh """
cp $jar_file /data/base/fw-base-nop/
"""
}
stage('推送镜像到仓库'){
def customImage=docker.build("192.168.176.155/base/fw-base-nop:${env.BUILD_ID}",'/data/base/fw-base-nop/')
customImage.push()
}
说明:
1、 如果dockerfile的名字不是”Dockerfile或者dockerfile“还需要通过-f参数指定dockefile的名称。
2、 推送镜像阶段的默认条件是agent代理节点已经对私有仓库做了认证如果agent节点没有对私有仓库做认证这里还需要加上认证的方法参考下面的“对私有仓库服务认证”。
3、 由于docker pipeline插件没有删除镜像的属性所以本地构建的镜像还是要使用shell命令去删除与在声明式语法中使用相同的命令即可这里不再演示。
私有仓库服务认证
使用脚本式语法对私有仓库服务认证相对于声明式语法就更简单了只需要使用docker pipeline插件中的with_registry方法即可轻松解决认证问题如下代码
stage("构建并推送镜像"){
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
def customImage=docker.build("${vars.registry_url}/${vars.project_group}/${vars.project_name}:${env.BUILD_ID}","/data/${vars.project_group}/${vars.project_name}/")
customImage.push()
}
}
与在声明式语法中使用docker插件的build方法、push方法一样在声明式语法中使用该方法同样需要用script{}块包含起来,代码示例如下:
stage('构建并上传镜像'){
steps {
script {
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
customImage.push()
}
}
}
}
使用变量
在声明式语法中使用environment来定义变量在脚本式语法中使用def指令来定义变量如下所示
def vars=[project_name:'fw-base-nop',jar_name:'fw-base-nop.jar',registry_url:'192.168.176.155',project_group:'base']
使用时,通过${vars.project_name}来引用该值,在该示例中配置如下:
node('jenkins-slave1'){
def jdk = tool name: 'jdk-1.8'
env.PATH = "${jdk}/bin:${env.PATH}"
def vars=[project_name:'fw-base-nop',jar_name:'fw-base-nop.jar',registry_url:'192.168.176.155',project_group:'base']
stage('代码编译'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
ws("$WORKSPACE/fw-base-nop"){
sh ' mvn clean install -DskipTests -Denv=beta'
}
}
}
stage('镜像构建'){
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name ${vars.jar_name} |head -1").trim()
}
sh """
cp $jar_file /data/${vars.project_group}/${vars.project_name}/
"""
}
stage('推送镜像到仓库'){
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
def customImage=docker.build("${vars.registry_url}/${vars.project_group}/${vars.project_name}:${env.BUILD_ID}","/data/${vars.project_group}/${vars.project_name}/")
customImage.push()
}
}
}
使用多agent
使用多agent的方式在语法章节中已经做了简单介绍首先回顾一下基本语法。
node() {
stage('test-node'){
node('jenkins-slave1'){
stage('test1'){
sh 'hostname'
}
}
}
stage('test-node2'){
node('jenkins-slave169'){
stage('test2'){
sh 'hostname'
}
}
}
}
对于核心脚本代码的实现只需要将在声明式语法示例中的代码复制过来放到stage下即可还是比较简单的。
异常处理
在前面的pipeline章节中有介绍脚本式语法使用try/catch/finally关键字来捕捉异常并对异常进行处理。使用try指令比较灵活它可以用来包含全局的stage也可以包含特定的stage中try{}后边必须包含catch或者finally关键字也可以同时都包含。
代码如下:
node('jenkins-slave1'){
def jdk = tool name: 'jdk-1.8'
env.PATH = "${jdk}/bin:${env.PATH}"
def vars=[project_name:'fw-base-nop',jar_name:'fw-base-nop.jar',registry_url:'192.168.176.155',project_group:'base']
try{
stage('代码编译'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
withEnv(["PATH+MAVEN=${tool 'maven-3.5.4'}/bin"]) {
sh "cd ${vars.project_name}/ && mvn clean install -DskipTests -Denv=beta"
}
}
stage('镜像构建'){
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name ${vars.jar_name} |head -1").trim()
}
sh """
cp $jar_file /data/${vars.project_group}/${vars.project_name}/
"""
}
stage('推送镜像到仓库'){
def customImage=docker.build("${vars.registry_url}/${vars.project_group}/${vars.project_name}:${env.BUILD_ID}","/data/${vars.project_group}/${vars.project_name}/")
customImage.push()
}
}catch(all){
currentBuild.result = 'FAILURE'
}
if(currentBuild.currentResult == "ABORTED" || currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ) {
echo "---currentBuild.result is:${currentBuild.currentResult}"
}
else {
echo "---currentBuild.result is:${currentBuild.currentResult}"
}
}
对于if和else判断后的步骤同样可以使用emailext方法将构建结果发送邮件给管理员参考声明式语法中发送邮件的配置即可。
删除工作空间
有些项目为了防止缓存可能会需要在构建完成后删除当前的工作目录。比较简单的方式是使用shell命令直接删除。但是既然是使用pipeline这里就用pipeline指令删除此时也许你已经想到方法了没错就是使用dir和deleteDir指令。
针对上面的示例可以在”推送镜像到仓库“步骤后添加一个新的stage通过dir命令进入目录然后通过deleteDir指令删除。具体的代码如下
stage('清除工作空间'){
dir('$WORKSPACE'){
deleteDir()
}
}
这样在执行该stage的时候就会删除当前的工作目录。
代码部署
前面的内容介绍的都是对代码的编译以及构建等操作关于部署的操作没有提及。如何部署可以参考之前ansible章节编写的playbook只需要做简单的修改即可。当然如果你不会也没关系在后面的章节中我会专门用一个章节来写一下如何部署。
有关使用pipeline语法进行持续交付与部署的基础实践就介绍到这里在下一节中将介绍一下常用的Docker pipeline的语法。

View File

@@ -0,0 +1,610 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
12.Jenkins Docker Pipeline插件动态生成Slave节点语法剖析
越来越多的公司和用户在实际工作中引入Jenkins在使用Jenkins自动化构建部署的过程中使用动态容器作为构建部署工作的执行环境的方案也越来越多的被采纳不仅提高了资源利用率同时还降低配置成本和维护管理成本。
在pipeline语法章节介绍Jenkins agent代理的时候有提到可以使用Docker容器作为流水线的执行环境。对于可以在Linux上运行的构建步骤同样可以在容器中进行。每个流水线项目仅需要选择一个包含所需要使用的工具和库的镜像即可。而docker插件设计的目的除了操作容器以外另一个比较重要的功能是通过指定的镜像启动容器作为单个stage或全局流水线的执行环境。
为了方便学习,同样将两种类型的语法分开介绍。
脚本式语法
在文章的开始首先介绍一下Docker pipeline插件的使用毕竟无论是声明式语法还是脚本式语法都是围绕该插件来启动代理节点。该插件在脚本式流水线中使用比较简单下面介绍一下基本语法。
docker-pipeline插件在安装后会产生一系列的变量要获取这些变量的使用方法的详细信息可以在任何一个pipeline类型的job里通过pipeline syntax流水线语法来查看在跳转的界面中点击”全局变量参考菜单“
如下所示-
更多变量内容可去Jenkins pipeline项目里查看下面对一些经常用到的变量方法进行简单的介绍
image
image方法用于根据镜像名称或者镜像id定义一个镜像对象然后根据该方法下的多个属性来执行不同的操作
比如根据镜像名称定义对象通过inside属性启动一个容器
node {
docker.image('maven:3.3.3-jdk-8').inside {
git '…your-sources…'
sh 'mvn -B clean install'
}
}
说明
image方法根据image名称从Docker Hub拉取镜像并定义对象通过inside属性启动容器。默认使用jenkins所在主机运行该容器如果要在其他节点可以在node里添加节点名称如下所示
node('jenkins-slave1') {
docker.image('maven:3.3.3-jdk-8').inside('-v $HOME/:/.m2/') {
git '…your-sources…'
sh 'mvn -B clean install'
}
}
该示例会在jenkins的系统配置里寻找label或者name为jenkins-slave1的节点拉取镜像并启动容器-
1、 inside属性用于启动容器并添加容器启动时的参数该属性可以没有参数上面示例添加了一个volume挂载的参数-
2、 镜像也可以从私有仓库拉取同时也可以使用image_id启动一个容器但该方法用的不多
上面示例使用inside属性启动了一个容器除了使用这种方式外还可以通过将image定义的对象赋予变量的方式使用比如
node('jenkins-slave1') {
def maven = docker.image('maven:latest')
maven.pull()
maven.inside {
sh 'hostname'
}
}
说明
pull方法用于确保该镜像在镜像仓库里的版本是最新的
inside属性启动容器
build
在前面pipeline章节的声明式语法中有介绍可以使用docker和dockrefile来启动一个容器。在脚本式语法中除了根据镜像名称或者镜像id启动一个容器以外也可以通过dockerfile方式构建一个容器并启动。为了构建 Docker 镜像Docker pipeline插件提供了一个 build() 方法用于在流水线运行之前根据源码库中提供的Dockerfile 构建一个新的镜像。
使用语法 docker.build("my-image-name") 可以构建一个镜像,使用定义变量的方式的好处在于在下面能够使用该变量的其他属性方法
比如:
node {
checkout scm
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.inside {
sh 'make test'
}
}
默认情况下, build() 方法是根据拉取的源码当前目录下的Dockerfile文件构建镜像。
build方法也可以通过提供一个包含 Dockerfile文件的目录路径作为build() 方法的第二个参数 ,用来在指定的目录下构建镜像
比如:
node {
checkout scm
def testImage = docker.build("test-image", "./dockerfiles/test")
testImage.inside {
sh 'make test'
}
}
说明:
通过在 ./dockerfiles/test/目录下发现的Dockerfile文件构建test-image。
checkout scm表示从源码仓库拉取Dockerfile文件可以单独放置可以与应用代码放到一起 。需要注意的是该配置可以通过git clone urlcheckout命令直接代替包括下面所有示例中的checkout scm
也可以通过传递 -f参数覆盖默认的 Dockerfile ,当使用这种方法传递参数时, 该字符串的最后一个值必须是Dockerfile文件的路径
比如
node {
checkout scm
def dockerfile = 'Dockerfile.test'
def customImage = docker.build("my-image:${env.BUILD_ID}", "-f ${dockerfile} ./dockerfiles")
}
说明:
从在./dockerfiles/目录发现的Dockerfile.test文件构建 my-image:${env.BUILD_ID}。
上面的示例中通过def指令定义了构建镜像操作的变量名称该变量可以用于通过 push() 方法将Docker 镜像推送到私有仓库
比如
node {
checkout scm
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.push()
}
说明
push方法除了可以将自定义执行流水线的docker镜像推送到镜像仓库以外也可以将应用代码经过代码编译和镜像构建后推送到私有仓库。
push方法在推送镜像时也可以自定义镜像的tag
如下所示
node {
checkout scm
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.push()
customImage.push('latest')
}
使用多个容器
在前面的pipeline章节有介绍在脚本式流水线语法中使用多个agent代理这里在来回顾一下
node('jenkins-slave1') {
stage('Back-end') {
docker.image('maven:3-alpine').inside {
sh 'mvn --version'
}
}
stage('Front-end') {
docker.image('node:7-alpine').inside {
sh "node --version"
}
}
}
结合前两节学过的内容,可以在上面语法的基础上对其进行扩展。
比如在不同的阶段使用不同的slave节点启动不同容器作为流水线执行的环境
如下示例
node() {
stage('Back-end') {
node('slave1'){
stage("test1"){
docker.image('maven:3-alpine').inside {
sh 'mvn --version'
}
}
}
}
stage('Front-end') {
node('slave2'){
stage('test2'){
docker.image('node:7-alpine').inside {
sh "node --version"
}
}
}
}
}
这样针对不同的步骤就会在不同的slave节点上启动容器执行流水线
使用远程docker服务器
除了使用jenkins节点宿主机上的docker进程外还可以使用其他服务器上的docker进程。docker pipeline提供了withServer方法用于连接远程的docker服务器。
要使用远程Docker服务器上的进程需要Docker服务器开启2375端口如下所示
修改/usr/lib/systemd/system/docker.service 配置文件
#在ExecStart=/usr/bin/docker daemon 后追加 -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock
#如:
ExecStart=/usr/bin/docker daemon -H tcp://0.0.0.0:2375 -H unix:///var/run/docker.sock
#修改完后重启docker
$ systemctl daemon-reload
$ systemctl restart docker
#查看进程
$ netstat -ntlp|grep 2375
tcp6 0 0 :::2375 :::* LISTEN 5554/dockerd
配置好后修改pipeline脚本如下
node {
docker.withServer('tcp://192.168.176.160:2375',) {
docker.image('maven:latest').inside {
sh "mvn --version"
}
}
}
这样,就会在远程服务器上启动该容器,可以执行想要执行的命令,比较简单。
withRegistry使用私有仓库
默认情况下, Docker 流水线 插件集成了 Docker Hub的私有仓库。 在编写流水线脚本时直接使用image方式拉取的镜像一般都是从Docker Hub拉取的。在上面的声明式流水线语法中介绍了在执行流水线过程中对私有仓库服务的认证的几种方法。对于脚本式流水线这就简单的多了那就是使用withRegistry()方法
例如
node {
checkout scm
docker.withRegistry('https://registry.example.com') {
docker.image('my-custom-image').inside {
sh 'make test'
}
}
}
此时是没有对镜像仓库服务进行认证的。
而如果想使用自定义的仓库怎么办使用vm作为agent代理时在声明式流水线语法可以使用凭据挂载目录等方式。而使用docker插件就没这么复杂了通过docker插件的withRegistry() 方法在使用时通过指定的参数对私有仓库认证认证方式也是通过jenkins系统里的凭据。
例如对于需要身份验证的Docker 私有仓库直接在Jenkins凭据中添加一个 “Username/Password” 凭据, 然后可以通过片段生成器使用凭据ID作为 withRegistry()的第二个参数,生成语法片段
比如
node {
checkout scm
docker.withRegistry('https://registry.example.com', 'credentials-id') {
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.push()
}
}
声明式语法
首先回顾一下在pipeline语法章节介绍声明式语法时使用docker启动agent代理的一些使用方法然后在做一下补充
首先来看一下在声明式脚本中使用docker作为agent的范例
例如,启动一个容器并执行一条命令
pipeline {
agent {
docker { image 'node:7-alpine' }
}
stages {
stage('Test') {
steps {
sh 'node --version'
}
}
}
}
当执行上面的流水线脚本时, Jenkins将会自动地启动指定的镜像如果镜像不存在会自动下载并在其中执行指定的stage当stage执行完好该容器会销毁。
除了使用该方式生成容器以外也可以使用docker pipeline插件例如
pipeline {
agent any
stages {
stage('Test') {
steps {
script {
docker.image('maven:latest').inside(){
sh 'mvn --version'
}
}
}
}
}
}
需要使用script{}块将pipeline语法块包含起来
使用Dockerfile
除了使用上面指定容器镜像的方法启动一个容器外还可以使用自定义的dockerfile启动容器。这种情况就需要将jenkinsfile文件和dockerfile文件放到源码仓库中了。
pipeline支持从源代码仓库的Dockerfile中构建和运行容器。使用 agent { dockerfile true } 语法会从 Dockerfile 中构建一个镜像并启动容器,而不是从 Docker Hub或者私有仓库中拉取。
如下示例
$ cat Dockerfile
FROM maven:latest
RUN apt-get update \
&& apt-get install lsof
$ cat Jenkinsfile
pipeline {
agent { dockerfile true }
stages {
stage('Test') {
steps {
sh 'lsof'
}
}
}
}
将上面的Dockerfile和Jenkinsfile文件一起放到源码仓库中然后配置jenkins项目时使用Pipeline script from SCM方式配置该源码仓库的地址即可-
说明:
Script Path 指定文件名要与jenkinsfile的文件名一样
使用dockerfile的其他参数可以参考pipeline章节中关于agent的介绍这里不再多说
容器的缓存数据
使用容器作为流水线的执行环境当流水线执行完以后容器就会销毁流水线构建的产物也会随之丢失。对于使用maven构建工具编译代码的时候默认会下载外部依赖包并将它们缓存到本地.m2仓库中以便于再次编译或者其他项目编译的的时候使用。 同时对于编译好的服务包有时候也需要做一次备份,所以我们需要将一些需要重复利用依赖包缓存到本地。
流水线支持向Docker中添加自定义的参数, 允许用户在启动容器时自定义挂载volume用来缓存数据。
下面的示例将会在流水线运行期间缓存maven仓库目录 ~/.m2, 从而避免了项目在从新构建时重新下载依赖导致部署效率低下的问题。
pipeline {
agent {
docker {
image 'maven:3-alpine'
args '-v $HOME/.m2:/root/.m2'
}
}
stages {
stage('Build') {
steps {
sh 'mvn -version'
}
}
}
}
说明:
上面流水线示例使用args指令传递容器运行时的参数-v指定的目录默认挂载到宿主机的目录上如果宿主机节点过多的情况下可以使用共享存储目录来代替宿主机目录
使用多个容器
在构建项目的时候如果遇到多种语言写的代码需要使用不同的编译工具进行代码的编译又或者对于一套流水线脚本对于不同功能的stage需要使用不同的镜像时就可以在一个流水线脚本中启动多个容器镜像
例如一个应用既有基于Java的后端API实现又有基于JavaScript的前端实现这种情况就可以通过agent {} 指令在不同的stage使用不同的容器进行编译操作。
如下示例
pipeline {
agent none
stages {
stage('Back-end') {
agent {
docker { image 'maven:3-alpine' }
}
steps {
sh 'mvn --version'
}
}
stage('Front-end') {
agent {
docker { image 'node:7-alpine' }
}
steps {
sh 'node --version'
}
}
}
}
说明:
示例中在顶层的agent指令里定义代理主机为none就必须要在每个stage里设置独立的agent代理
分别对后端和前端使用不同的镜像启动构建环境
如果要在指定的node节点上启动容器或者在不同节点上分别启动不同的容器又该如何操作这里就要使用到docker pipeline插件了。
如下所示在slave1节点启动两个容器
pipeline {
agent { node { label 'slave1' } }
stages {
stage('Build') {
steps {
echo 'Building..'
sh 'npm install'
}
}
stage('Test') {
steps {
echo 'Testing..'
script {
docker.image('selenium:latest').inside(){
.......
}
}
}
}
stage('build') {
steps {
script {
docker.image('maven:lastet').inside(){
docker build ..
}
}
}
}
}
}
使用多个node启动多个容器配置如下
pipeline {
agent none
stages {
stage('Test') {
agent { node { label 'jenkins-slave1'} }
steps {
echo 'Testing..'
script {
docker.image('selenium:latest').inside(){
.......
}
}
}
}
stage('build') {
agent { node { label 'jenkins-slave169'} }
steps {
script {
docker.image('maven:lastet').inside(){
docker build ..
}
}
}
}
}
}
虽然可以实现,但是该方案可能用的比较少。有兴趣的自己可以尝试一下
使用私有仓库
将应用代码编译完成并构建成镜像以后,需要将该镜像推送到指定的私有仓库上去,这个流程中涉及到一个向私有仓库服务认证的过程,那么在声明式流水线语法中如何向私有仓库服务认证呢?可以参考如下方法:
使用docker login命令
如果你怕麻烦并且对于Jenkins的安全设置也比较有把握那么你可以选择用docker login命令加明文密码。如下示例
stage('Docker Push') {
steps {
sh "docker login -u xxx -p xxx registry_url"
sh 'docker push 192.168.176.154/base/nop:latest'
}
}
该示例在流水线中直接使用docker login命令向私有仓库服务认证。
如果觉得明文密码太过显眼也可以使用”参数化构建过程“选项添加一个password类型的参数然后使用login命令登录时-p参数直接使用变量名称即可。但是此方法每次构建时都要输入一下密码显得有些笨拙。此时在之前的章节中介绍到的凭据就派上用场了。
除了使用”参数化构建过程“选项提供参数以外还可以使用Jenkins凭据提供私有仓库服务认证的用户名和密码。
stage('Docker Push') {
steps {
withCredentials([usernamePassword(credentialsId: 'dockerregistry', passwordVariable: 'dockerHubPassword', usernameVariable: 'dockerHubUser')]) {
sh "docker login -u ${env.dockerHubUser} -p ${env.dockerHubPassword}"
sh 'docker push 192.168.176.155/library/base-nop:latest'
}
}
}
说明:
该语法片段可以通过片段生成器生成
也可以直接使用参数指定仓库的url和凭据id示例如下
agent {
docker {
image '192.168.176.155/library/jenkins-slave'
args '-v $HOME/.m2:/root/.m2 -v /data/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock'
registryUrl 'http://192.168.176.155'
registryCredentialsId 'auth_harbor'
}
}
这样便实现了与上面withCredentials方式同样的效果。不过需要说明的是在使用Docker插件时建议还是使用脚本式语法该插件与脚本式语法集成使用会更加方便。
使用目录挂载
如果不想用Jenkins里的凭据怎么办可以使用挂载docker配置文件的方法。使用docker命令登录私有仓库的时候默认会将认证信息保存在docker本地服务器指定的文件里。这是可以使用args参数时将该文件挂载到容器内指定的文件上。
示例如下
agent {
docker {
image '192.168.176.155/library/jenkins-slave'
args '-v $HOME/.m2:/root/.m2 -v /data/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock -v /root/.docker/config.json:/root/.docker/config.json'
}
}
说明:
docker向私有仓库认证的配置文件根据docker安装方式的不同会有所差异。yum方式安装的docker使用的认证的文件为/root/.docker/config.json该文件存储了认证信息至于如何获取该文件的具体路径可以通过docker login registry_url命令认证成功后就会提示存放认证文件的路径。
使用Docker pipeline插件
Docker pipeline实际使用的时候在脚本式语法中应用的相对简单但也可以在声明式流水线中使用只不过需要将插件的方法通过script{}块包起来。
比如使用私有仓库的方法,在声明式流水线中可以写成如下
pipeline{
agent ...
stages(){
stage(){
steps{
....
}
}
stage(){
steps{
script{
docker.withRegistry('https://registry.example.com', 'credentials-id') {
def customImage = docker.build("my-image:${env.BUILD_ID}")
customImage.push()
}
}
}
}
}
}
该方式同样适用于docker插件的其它属性方法。
有关Pipeline与Docker集成的语法说明就简单的介绍到这里。在下面的章节中会通过一些示例来演示通过jenkins与Docker集成实现持续交付和持续部署的过程。

View File

@@ -0,0 +1,792 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
13.使用 Docker Pipeline插件动态生成Jenkins Slave 实践
前面有提到过使用docker pipeline插件的主要目的是为了动态生成Jenkins的slave节点作为pipeline执行的环境。本节根据上一节的语法示例通过一个实践案例来加深一下理解。
构建镜像
首先需要说明的是无论使用哪种类型的语法、是使用一个镜像还是多个镜像作为流水线stage的agent代理首先要做的是构建好这些提供流水线脚本执行的镜像。
经过前面一系列实践可以发现本系列文章用到工具主要包括jdk、git、maven、docker和ansible等。所以我们需要自定义一个包含以上一个多个工具的镜像dockerfile如下
FROM alpine:latest
RUN apk update && apk add openjdk8 git maven
COPY settings.xml /usr/share/java/maven-3/conf/settings.xml
COPY docker/docker /usr/bin/docker
其中:
settings.xml文件为maven配置文件里面配置了Maven私有仓库管理器的地址及认证信息这里需要根据你实际情况考虑要不要添加在后面的章节中会介绍其它方法来自定义该文件的使用。
docker/dockerdocker的二进制文件这里需要注意一点如果想要将docker可执行文件构建到镜像中或者在启动时挂载docker可执行文件一定要使用二进制可执行文件不要使用yum安装生成的二进制文件否则在使用时会提示docker命令找不到。这是因为使用yum安装生成的docker二进制文件挂载到容器内目录或者构建到其它镜像中时必须保证宿主机和镜像内的OS相同否则无法执行另一个系统平台上的docker二进制文件。
docker/docker从官方下载的docker tar包文件解压后为一个docker目录。
通过build命令构建
$ docker build -t alpine-cicd:latest .
构建好以后测试一下pipeline脚本如下
pipeline {
agent {
docker{
image 'alpine-cicd:latest'
args '-v /var/run/docker.sock:/var/run/docker.sock'
}
}
stages('build') {
stage('测试'){
steps {
sh """
docker version
mvn --version
git --version
"""
}
}
}
}
流水线正常执行表示镜像构建成功。
一个统一的镜像构建好了如果遇到使用多个agent的情况也可以使用这个统一的镜像或者分别构建特定工具的镜像这里不再进行演示有兴趣的可以自己尝试一下。
声明式语法
构建完基础镜像就可以正式学习如何使用pipeline与docker插件集成了。在前面章节介绍了使用vm作为Jenkins的slave节点的实践示例使用docker容器作为Jenkins的slave节点与使用vm有很多相同的地方比如使用共享存储保存构建后的产物使用pipeline方法实现对私有仓库的认证等学会了前面章节的内容对于本章的学习还是相对较简单的。
Agent代理
首先来回顾一下使用docker容器作为agent代理的基本语法。
使用一个全局agent
pipeline {
agent { docker 'maven:3-alpine' }
stages {
stage('build') {
steps {
sh 'mvn clean install'
}
}
}
}
使用stage级别的agent
pipeline {
agent none
stages {
stage('Build') {
agent { docker 'maven:3-alpine' }
steps {
echo 'Hello, Maven'
sh 'mvn --version'
}
}
stage('Test') {
agent { docker 'openjdk:8-jre' }
steps {
echo 'Hello, JDK'
sh 'java -version'
}
}
}
}
而在学习了脚本式语法以及docker pipeline插件语法后使用多agent也可以这样写在不同的代理节点启动不同的容器。
pipeline {
agent none
stages {
stage('Build') {
agent { node { label slave1 } }
steps {
script{
docker.image('maven').inside(){
sh 'hostname'
}
}
}
}
stage('Test') {
agent { node { label slave2 } }
steps {
script{
docker.image('nginx').inside(){
sh 'hostname'
}
}
}
}
}
}
该方式用的情况可能不多,但在这里还是有必要说一下:
使用全局agent
使用docker容器作为agent只要修改agent指令的配置即可配置比较简单示例如下
pipeline {
agent {
docker{
image 'alpine-cicd:latest'
args '-v /var/run/docker.sock:/var/run/docker.sock -v /data/:/data/ -v /root/.docker/config.json:/root/.docker/config.json -v /root/.m2:/root/.m2'
}
}
environment {
project_name = 'fw-base-nop'
jar_name = 'fw-base-nop.jar'
registry_url = '192.168.176.155'
project_group = 'base'
}
stages('build') {
stage('代码拉取并打包'){
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
sh "cd $project_name && mvn clean install -DskipTests -Denv=beta"
}
}
stage('构建镜像'){
steps {
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
}
sh """
cp $jar_file /data/$project_group/$project_name/
"""
}
}
stage('上传镜像'){
steps {
script {
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
customImage.push()
}
}
}
}
}
post {
always {
script{
sh 'docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}'
if (currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ){
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建结果",
body: """
详情:\n<br>
Jenkins 构建 ${currentBuild.result} '\n'<br>
项目名称 ${env.JOB_NAME} "\n"
项目构建id${env.BUILD_NUMBER} "\n"
URL ${env.BUILD_URL} \n
构建日志:${env.BUILD_URL}console
""",
to: "[email protected]",
recipientProviders: [[$class: 'CulpritsRecipientProvider'],
[$class: 'DevelopersRecipientProvider'],
[$class: 'RequesterRecipientProvider']]
)
}else{
echo "构建成功"
}
}
}
}
}
说明:
相对于使用vm作为agent代理流水线核心代码没变只是修改了agent的配置。
image 参数指定了在上一个小节中构建的镜像名称。
args 定义了启动镜像的参数:
-v /var/run/docker.sock:/var/run/docker.sock用于挂载宿主机docker服务的sock文件到容器中如果不挂载该文件使用docker时会提示docker进程没有启动并无法进行镜像构建工作
-v /data/:/data/用于将共享存储的data目录挂载到容器中该目录包含了项目组文件夹项目文件夹以及dockerfile文件目录结构根据自己实际情况自定义代码编译的产物会放到指定的项目文件夹中构建镜像时会使用项目文件夹路径作为上下文路径进行构建
-v /root/.docker/config.json:/root/.docker/config.json 用于将宿主机上docker进程向私有仓库认证的文件挂载到容器内指定的文件上这里需要注意的使用宿主机当前用户需要对docker命令有操作权限至于该容器在构建镜像时默认是root用户
流水线执行时默认会在jenkins的所有节点中选择一台节点并启动容器虽然是在容器中进行镜像构建与推送到私有仓库操作但使用的各种配置大多都依赖于宿主机。
使用多个agent
使用多个agent与使用全局agent没有明显的区别只是将最顶层的agent关键字的值变为none在不同的步骤通过agent关键字指定不同的镜像即可。
如下示例:
pipeline {
agent none
environment {
project_name = 'fw-base-nop'
jar_name = 'fw-base-nop.jar'
registry_url = '192.168.176.155'
project_group = 'base'
}
stages('build') {
stage('代码拉取并打包'){
agent {
docker {
image 'alpine-cicd:latest'
args '-v /root/.m2:/root/.m2'
}
}
steps {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始打包 "
sh "cd $project_name && mvn clean install -DskipTests -Denv=beta"
}
}
stage('构建镜像'){
agent {
docker {
image 'alpine-cicd:latest'
args ' -v /var/run/docker.sock:/var/run/docker.sock -v /data/:/data/'
}
}
steps {
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
}
sh """
cp $jar_file /data/$project_group/$project_name/
"""
}
}
stage('上传镜像'){
agent {
docker {
image 'alpine-cicd:latest'
args ' -v /data/:/data/ -v /var/run/docker.sock:/var/run/docker.sock
}
}
steps {
script {
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
customImage.push()
}
}
}
}
}
post {
always {
node (null){
script{
sh 'docker rmi -f $registry_url/$project_group/$project_name:${BUILD_ID}'
if (currentBuild.currentResult == "ABORTED" || currentBuild.currentResult == "FAILURE" || currentBuild.currentResult == "UNSTABLE" ){
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建结果",
body: """
详情:<br>
Jenkins 构建 ${currentBuild.result} <br>
项目名称 ${env.JOB_NAME} <br>
项目构建id${env.BUILD_NUMBER} <br>
URL ${env.BUILD_URL} <br>
构建日志:${env.BUILD_URL}console
""",
to: "[email protected]",
recipientProviders: [[$class: 'CulpritsRecipientProvider'],
[$class: 'DevelopersRecipientProvider'],
[$class: 'RequesterRecipientProvider']]
)
}else{
echo "构建成功"
}
}
}
}
}
}
说明:
有一点与使用全局agent不同在最后post指令中需要单独设定node节点。此处不能用agent指令但可以通过node('slave-label')的方式指定一个节点slave的标签也可以设定为null此时默认会自动选择一个slave节点进行post下的操作。这种情况如果要部署该镜像可直接通过宿主机的脚本进行部署。
另一点需要注意的是,代码编译与镜像构建要挂载相同的数据目录来使用同一个构建产物。
私有仓库认证
前面的章节中在声明式脚本中对私有仓库的认证介绍了使用withCredentials()与withRegistry<br/>()对于在声明式语法中使用docker容器作为agent对私有仓库进行认证时也可以通过在agent指令中添加对私有仓库认证的附加参数比如
docker {
image '192.168.176.155/library/maven:v2'
args '-v $HOME/.m2:/root/.m2 -v /data/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock'
registryUrl 'http://192.168.176.155'
registryCredentialsId 'auth_harbor'
}
通过在容器启动参数中加入registryUrl和registryCredentialsId参数在容器启动后对私有仓库认证。除了可以在拉取工作镜像时使用也可以在构建应用镜像后推送私到私有仓库时使用。
完整版的配置如下:
pipeline{
agent {
docker {
image '192.168.176.155/library/jenkins-slave'
args '-v $HOME/.m2:/root/.m2 -v /data/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock'
registryUrl 'http://192.168.176.155'
registryCredentialsId 'auth_harbor'
}
}
stages {
stage('clone') {
steps('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
}
stage("mvn") {
steps('build'){
sh """
cd fw-base-nop && mvn clean install -DskipTests -Denv=beta
cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/
hostname && pwd
"""
}
}
stage("build and push to registry") {
environment {
dockerfile = '/data/Dockerfile'
}
steps('push'){
script {
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
customImage.push()
}
}
}
}
}
}
有关在声明式语法中使用Docker插件的内容就简单的介绍到这里下面重点介绍一下在脚本式语法中使用Docker pipeline插件。
脚本式语法
使用脚本式语法动态生成slave工作节点开始之前首先来回顾一下使用docker pipeline插件启动容器的语法如下所示
node {
docker.image('maven:3.3.3-jdk-8').inside {
git '…your-sources…'
sh 'mvn -B clean install'
}
}
通过image属性指定一个容器然后通过inside属性启动一个容器。
下面分多个步骤介绍该插件的使用:
版本一:在容器内进行代码编译,在宿主机进行镜像构建并推送至私有仓库。
版本二:在容器内编译构建并上传到私有仓库
版本一
使用脚本式语法相对于使用声明式语法在指定的slave节点上启动容器时相对简单的多比如在jenkins-slave1主机上启动容器。
node('jenkins-slave1') {
docker.image('192.168.176.155/library/maven:v2').inside('-v $HOME/.m2:/root/.m2') {
stage('checkout'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
}
stage('编译'){
sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
sh 'hostname && pwd'
}
}
}
说明:
通过在node()中指定slave节点的标签可以设置在指定的节点上启动容器进行工作。
该示例会在jenkins-slave1节点上使用inside属性启动一个maven容器用来进行代码编译工作需要注意的是即便不传参数也要保证inside属性存在。
执行日志如下:
由上面的jenkins执行结果可以看到在job执行后如果本地没有设定的maven镜像会先拉取maven镜像并将该镜像启动。启动的方法和参数可以通过上面的截图看到。
执行结果如下:-
由上图可看到,通过容器拉取代码并编译,编译完成以后容器会自动销毁,所以编译后的产物也会随之消失。此时,在上一章节中使用的共享存储就到了彰显自我价值的时候了,只需要将编译好的包保存到共享存储上即可,一个比较简单的方法,通过在宿主机挂载共享存储目录并将该目录映射到容器中指定的目录下。
如下示例:
node('jenkins-slave1') {
docker.image('192.168.176.155/library/maven:v2').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data') {
stage('checkout'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
}
stage('编译'){
sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
sh 'hostname && pwd'
}
stage('拷贝jar包'){
sh 'cp ${WORKSPACE}/fw-base-nop/target/*.jar /data/fw-base-nop.jar'
}
}
}
这里模拟/data/目录为共享存储挂载到宿主机的目录。编译好后将构建产物拷贝到容器的/data目录就相当于拷贝到宿主机/data/base/fw-base-nop目录下然后在该目录事先创建Dockerfile文件就可以构建镜像了。
而在宿主机进行镜像构建与推送到私有仓库操作,直接参考上一章节相关的操作即可。
node('jenkins-slave1') {
docker.image('192.168.176.155/library/maven:v2').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data') {
stage('checkout'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
}
stage('maven'){
sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
sh 'hostname && pwd'
}
stage('拷贝jar包'){
sh 'cp ${WORKSPACE}/xxx-management/xxx-admin/target/*.jar /data/admin.jar'
}
}
stage('build and push'){
docker.withRegistry('http://192.168.176.155','auth_harbor') {
def dockerfile = '/data/bsse/fw-base-nop/Dockerfile'
def customImage = docker.build("192.168.176.155/library/admin:v1", "-f ${dockerfile} /data/bsse/fw-base-nop/.")
customImage.push()
}
}
}
构建效果图这里不展示了有兴趣的可以自己进行扩展并测试。如果你的slave节点配置了私有仓库认证这里你可以不用withRegistry方法。
版本二
上面已经完成了在容器内的代码编译并在宿主机构建镜像。下面只需要在此基础上将镜像构建操作放到容器中即可。
直接编写pipeline script
node ('jenkins-slave1') {
docker.withRegistry('http://192.168.176.155','auth_harbor') {
docker.image('alpine-cicd:latest').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock) {
stage('checkout'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
stage('打包'){
sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
sh 'hostname && pwd'
}
stage('copy jar file'){
sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
}
stage('build and push'){
def dockerfile = '/data/Dockerfile'
def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/.")
customImage.push()
}
stage('delete image'){
sh "docker rmi 192.168.176.155/library/fw-base-nop:${imageTag}"
}
}
}
}
说明
在版本一中我将withRegistry方法放到了指定的stage下还可以将该方法放到pipeline脚本的最顶层。
script 包含的命令用来执行命令获取提交的代码的commit 的short id作为镜像的版本号。
脚本最后不要忘记加上删除刚刚构建的镜像的操作这里再说说明一下该操作是因为构建的镜像会存在宿主机上而且每个镜像的tag不一样如果构建次数多了会导致镜像数量庞大占用空间并且该镜像已经上传到私有仓库这里就没必要在保存在宿主机上了。
有一点需要注意就是jenkins slave节点的docker版本问题本例docker版本为docker-ce-18.06非ce版本可能会出现如下问题
/usr/bin/docker: .: line 2: can't open '/etc/sysconfig/docker'
或者
You don't have either docker-client or docker-client-latest installed. Please install either one and retry.
执行结果如下:-
使用多个agent代理
在脚本式语法中使用多个agent的配置比较灵活比如
示例一在同一个node节点上启动多个容器执行流水线。
node ('jenkins-slave1') {
docker.withRegistry('http://192.168.176.155','auth_harbor') {
docker.image('xxx').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data ') {
stage('checkout'){
checkout(xxxx)
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
stage('编译'){
sh 'xxx'
}
stage('拷贝jar包'){
sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
}
}
docker.image('xxx').inside('-v /data/base/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock') {
stage('构建') {
def dockerfile = '/data/Dockerfile'
def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/.")
customImage.push()
}
}
}
}
示例二在不同的node节点上进行不同的操作比如在slave1节点上进行代码编译工作在slave2节点上进行镜像构建与推送到私有仓库操作这种一般使用共享存储来共享工作目录
node {
stage('working in slave1'){
node('jenkins-slave1'){
docker.image('alpine-cicd:latest').inside('-v $HOME/.m2:/root/.m2 -v /data/base/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock') {
stage('checkout in slave1'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
stage('build in slave1'){
sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
sh 'hostname && pwd'
}
stage('copy jar file in slave1'){
sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
}
}
}
}
stage('working in slave169'){
node('jenkins-slave169'){
stage('build and push in slave169'){
docker.withRegistry('http://192.168.176.155','auth_harbor') {
def dockerfile = '/data/base/fw-base-nop/Dockerfile'
def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/base/fw-base-nop/.")
customImage.push()
}
}
}
}
}
此示例分别在两台机器上进行代码编译和镜像构建需要注意的是两个slave节点用到的共同的目录(比如maven私有库存放目录构建产物的存放目录dockerfile的存放目录等)需要使用共享存储挂载,才能保证数据的一致性。
构建效果这里就不贴图了,执行结果大致都相同。
Pipeline script from SCM
除了上面使用 pipeline script的方式直接在jenkins项目里编写pipeline脚本外还可以将写好pipeline脚本放到源代码仓库里即Pipeline script from SCM。
将上面pipeline脚本写到文件Jenkinsfile中然后提交到源代码仓库然后在jenkins项目中通过选择Pipeline script from SCM然后配置该代码仓库的地址用户名和密码构建即可。-
-
上面配置保存即可。使用这种方式构建项目,官方建议使用声明式语法编写脚本。
异常处理
有关异常处理的内容在上一章节以及pipeline实践章节中已经有过介绍在接下来的内容只要将其拿过来直接使用即可。
首先回顾一下在脚本式语法中使用try/catch/finally指令的语法
node ('jenkins-slave1') {
try {
stage('exec command'){
sh 'hostnamae && pwd'
currentBuild.currentresult = 'SUCCESS'
}
}catch(e) {
currentBuild.currentresult = 'FAILURE'
def errorMsg = "================== 错误信息 START ===================\n"
errorMsg += "${e.toString()}|${e.message}\n"
errorMsg += "==================== 错误信息 END ======================"
echo "errrormsg is ${errorMsg}"
}
finally{
if (currentBuild.result == 'SUCCESS') {
echo "---currentBuild.currentresult is:${currentBuild.currentresult}---"
}
else {
echo "---currentBuild.currentresult is:${currentBuild.currentresult}
}
}
}
}
}
说明
try 指令包含要执行的任务try指令后面必须跟随catch或者finally指令catch和finally可以不用同时存在但当有try指令时这俩指令必须有一个存在。
该示例根据job执行的结果分别做出不同的操作。
根据上面的语法,就可以将上面持续交付的代码套进这个模板里,如下所示:
node ('jenkins-slave1') {
docker.withRegistry('http://192.168.176.155','auth_harbor') {
docker.image('192.168.176.155/library/jenkins-slave').inside('-v $HOME/.m2:/root/.m2 -v /data/fw-base-nop:/data -v /var/run/docker.sock:/var/run/docker.sock -v /usr/bin/docker:/usr/bin/docker') {
stage('checkout'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
}
try {
stage('maven'){
sh 'cd fw-base-nop && mvn clean install -DskipTests -Denv=beta'
sh 'hostname && pwd'
currentBuild.currentresult = 'SUCCESS'
}
}catch(e) {
currentBuild.currentresult = 'FAILURE'
def errorMsg = "================= 错误信息 START ======================\n"
errorMsg += "${e.toString()}|${e.message}\n"
errorMsg += "=================== 错误信息 END ======================"
echo "${errorMsg}"
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建异常",
body: """
详情:\n
failure: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] \n
状态:${env.JOB_NAME} jenkins 构建异常 \n
URL ${env.BUILD_URL} \n
项目名称 ${env.JOB_NAME} \n
项目构建id${env.BUILD_NUMBER} \n
错误信息: ${errorMsg} \n
""",
to: "[email protected]",
recipientProviders: [[$class: 'DevelopersRecipientProvider']]
)
}
if (currentBuild.currentresult == 'SUCCESS') {
echo "---currentBuild.result is:${currentBuild.result}------"
stage('拷贝拷贝jar包'){
sh 'cp ${WORKSPACE}/fw-base-nop/target/fw-base-nop.jar /data/'
}
stage('build and push'){
def dockerfile = '/data/Dockerfile'
def customImage = docker.build("192.168.176.155/library/fw-base-nop:${imageTag}", "-f ${dockerfile} /data/.")
customImage.push()
}
}else {
echo "---currentBuild.currentresult is:${currentBuild.currentresult}"
}
}
}
}
}
说明:
上面使用了emailext方法用于在指定的步骤发送邮件可以在多个步骤设置不同的邮件模板用于快速定位异常问题。
currentBuild为默认自带的全局变量该变量下有多个方法可以通过在${YOUR_JENKINS_URL}/pipeline-syntax/globals#env中获取到
对于流水线中的所有核心步骤都可以通过try指令去捕捉流水线各阶段执行的结果配置比较灵活此示例仅做演示。
有关pipeline与docker插件集成的进行持续交付内容到这里就结束了。在以后的章节中会详细介绍如何进行持续部署此处先略过。在下一节的内容会介绍一下jenkins与kubernetes的集成配置。

View File

@@ -0,0 +1,410 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
14.搞定不同环境下的Jenkins与Kubernetes集群连接配置
在前面的章节中简单的介绍了使用Jenkins与Docker集成的一些基本语法以及实现持续交付和持续部署的配置。在Jenkins中除了可以与docker集成外还可以与容器编排工具kubernetes集成。
Jenkins使用kubernetes插件主要用于完成两方面的工作一是用于在kubernetes集群内动态生成一个pod作为Jenkins 的slave节点提供流水线执行的工作环境二是用于将应用代码持续部署到kubernetes集群中。
基于上面提到的这两方面的用途在剩下的章节会详细介绍Jenkins与kubernetes的集成配置与使用方法。
同docker pipeline一样在Jenkins里集成kubernetes也是依赖于插件的所以在介绍如何配置与使用jenkins集成kubernetes之前需要先安装插件
Kubernetes plugin
Kubernetes CLI
Kubernetes Continous Deploy
上面列出的插件第一个用于在kubernetes集群中动态生成jenkins slave节点后两个插件用于通过不同的方式持续部署代码到kubernetes集群。无论哪种插件使用前都要先保证jenkins能够连接到kubernetes集群所以本章节就从不同环境下的jenkins连接kubernetes集群的配置说起。
配置Jenkins连接Kubernetes
使用docker pipeline做持续交付的时候默认直接使用的宿主机的docker进程。而Jenkins与kubernetes的集成主要是通过调用Kubernetes的API去kubernetes集群中进行工作的。大多数公司在安装kubernetes集群配置apiserver服务时使用了证书所以在配置jenkins连接kubernetes集群时需要根据kubernetes的配置文件生成一系列证书以及key并将证书上传到Jenkins用来对apiserver进行认证。
下面针对部署在kubernetes集群环境下的Jenkins和非kubernetes集群环境下的jenkins连接kubernetes集群的配置进行详细介绍。
部署在非kubernetes集群内Jenkins连接kubernetes配置
安装好插件以后进入Jenkins首页点击菜单”Manage Jenkins(系统管理)-> Configure System(系统设置)” 在跳转到的界面中,到最底部,点击” Add a new cloud新建一个云> kubernetes“。
如下所示:
其中:
名称这里用于填写要添加的这个云cloud的名称默认为”kubernetes”如果不想用这个可以自定义。在编写pipeline的时候会用到。
kubernetes 地址用于填写kubernetes集群的地址做了多master集群高可用的环境直接写vip地址加端口只有单个master的环境直接写master加端口地址即可。
Kubernetes 服务证书 key用于填写与kubernetes集群认证的证书内容。
Kubernetes 命名空间用于填写调用kubernetes时生成的pod工作的namespace。
Credentials凭据用于连接kubernetes的凭证。
Jenkins 地址Jenkins的连接地址。
了解了基本配置的参数说明下面主要说一下”Kubernetes 服务证书 key“和Credentials凭据配置。
kubernetes集群安装的时候生成了一系列证书以及key并且在配置kubernetes中kubectl客户端命令权限的时候根据这些证书以及key生成了一个kubeconfig文件用于kubectl与集群通信这个文件默认为/root/.kube/config文件对集群有最高操作权限如果给了cluster-admin权限
Jenkins需要根据这个文件生成的证书与集群通信所以我们在生产环境配置Jenkins连接kubernetes集群的时候需要注意一下kubeconfig文件绑定的用户的权限最好从新生成一个低权限的kubeconfig文件而不要用kubectl命令使用的文件。
我这里为了测试方便就先使用kubectl命令使用的kubeconfig文件至于如何生成低权限的kubeconfig文件这里不做介绍有兴趣的可以私聊我。
配置证书key
首先看一下config文件。
[root@k8s_master1 ~]# cat /root/.kube/config
apiVersion: v1
clusters:
- cluster:
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2akNDQXFhZ0F3SUJBZ0lVT0g4cDd6QXZaR3p4cGxUVy9xe......
WRnVRNm9IcjZ0Yk0wa1NJVkhvN2JNQjRWOGZoUWk4WjlLS243ZTFsQWdaVWhyWGMzTzRqVS9xVHRWRQpSRXc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K
server: https://192.168.176.156:6443
name: kubernetes
contexts:
- context:
cluster: kubernetes
user: admin
name: kubernetes
current-context: kubernetes
kind: Config
preferences: {}
users:
- name: admin
user:
client-certificate-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQzVENDQXNXZ0F3SUJBZ0lVQmI1ZTJFaUk1WndSY1JVeFVyZ......
Qk9DTzRBcEVzWXNOa084UVF2RTlwVEhpNlE0LzhLV0NtU0wyNgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg==
client-key-data: LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeDdjNkpRTFdQaC90REtjUDQrcDV5aCtWdWRCUmFacFJ2THM2MU1POGFZWFRCT09pCkJyZzFQb0laRzZEbXNBNnUyUStOVnlGOWg5RTQ3VVpFNjI5ND...... K3dGdUF2S29vRm9lRFZCS3I3NjdiTFA3ZTBibkVqS1ExRmNHVFFFQVU9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg==
获取文件中certificate-authority-data的内容并转化成base64 encoded文件
echo LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUR2akNDQXFhZ0F3SUJBZ0lVT0g4cDd6QXZaR3p4cGxUVy9xejRWRmxLQllRd0RRWUpLb1pJaHZjTkFRRUwKQlFBd1pURUxNQWtHQTFVRUJoTUNRMDR4RURBT0JnTlZCQWdUQjBKbG......
WRnVRNm9IcjZ0Yk0wa1NJVkhvN2JNQjRWOGZoUWk4WjlLS243ZTFsQWdaVWhyWGMzTzRqVS9xVHRWRQpSRXc9Ci0tLS0tRU5EIENFUlRJRklDQVRFLS0tLS0K |base64 -d > ca.crt
将ca.crt的内容粘贴到kubernetes server certificate key(Kubernetes 服务证书 key)框里,如下图所示:
配置凭据
获取/root/.kube/config文件中client-certificate-data和client-key-data的内容并转化成base64 encoded文件
echo LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUQzVENDQXNXZ0F3SUJBZ0lVQmI1ZTJFaUk1WndSY1JVeFVyZ......
Qk9DTzRBcEVzWXNOa084UVF2RTlwVEhpNlE0LzhLV0NtU0wyNgotLS0tLUVORCBDRVJUSUZJQ0FURS0tLS0tCg== |base64 -d >client.crt
//生成key
echo LS0tLS1CRUdJTiBSU0EgUFJJVkFURSBLRVktLS0tLQpNSUlFb2dJQkFBS0NBUUVBeDdjNkpRTFdQaC90REtjUDQrc......
K3dGdUF2S29vRm9lRFZCS3I3NjdiTFA3ZTBibkVqS1ExRmNHVFFFQVU9Ci0tLS0tRU5EIFJTQSBQUklWQVRFIEtFWS0tLS0tCg== | base64 -d >client.key
生成Client P12认证文件cert.pfx并下载至本地
openssl pkcs12 -export -out cert.pfx -inkey client.key -in client.crt -certfile ca.crt
Enter Export Password:            //密码自定义jenkins导入证书的时候需要用到
Verifying - Enter Export Password:
在jenkins凭据菜单添加凭据
如上图所指示凭据类型为”Certiticate“password为生成证书时的设置的密码。
添加完成后如下图所示,点击连接测试
连接成功如下所示:
到此部署在非kubernetes集群内的Jenkins连接kubernetes配置就完成了。
部署在kubernetes集群内jenkins连接kubernetes配置
在前面介绍jenkins安装配置章节没有介绍如何在kubernetes集群中部署所以本小节的开头就先介绍一下如何在kubernetes集群中部署jenkins。
kubernetes系统中使用资源对象来描述某个系统的期望状态以及对象的基本信息。对于部署某个应用或者服务使用最多的资源对象就是deployment所以这里使用deployment来描述jenkins的配置并部署。
在kubernetes集群部署jenkins
首先需要创建一个服务账户ServiceAccount用来绑定对某一个命名空间的一系列kubernetes资源对象的操作权限。
如下jenkins-rbac.yaml文件
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: jenkins
namespace: kube-system
---
kind: Role
apiVersion: rbac.authorization.k8s.io/v1beta1
metadata:
name: jenkins
namespace: kube-system
rules:
- apiGroups: [""]
resources: ["pods"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: [""]
resources: ["pods/exec"]
verbs: ["create", "delete", "get", "list", "patch", "update", "watch"]
- apiGroups: [""]
resources: ["pods/log"]
verbs: ["get", "list", "watch"]
- apiGroups: [""]
resources: ["secrets"]
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1beta1
kind: RoleBinding
metadata:
name: jenkins
namespace: kube-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: jenkins
subjects:
- kind: ServiceAccount
name: jenkins
然后创建deployment资源对象文件来描述jenkins的一些基本配置信息比如开放的端口启动参数使用的镜像等。
如下jenkins-deployment.yaml文件
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: jenkins
labels:
app-name: jenkins
namespace: kube-system
spec:
replicas: 1
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
template:
metadata:
labels:
app-name: jenkins
spec:
serviceAccount: "jenkins"
containers:
- name: jenkins
image: docker.io/jenkins:latest
imagePullPolicy: IfNotPresent
ports:
- containerPort: 8080
name: web
protocol: TCP
- containerPort: 50000
name: agent
protocol: TCP
volumeMounts:
- name: jenkins-home
mountPath: /var/jenkins_home
env:
- name: JAVA_OPTS
value: "-Duser.timezone=Asia/Shanghai -XX:+UnlockExperimentalVMOptions -XX:+UseCGroupMemoryLimitForHeap -XX:MaxRAMFraction=1 -Dhudson.slaves.NodeProvisioner.MARGIN=50 -Dhudson.slaves.NodeProvisioner.MARGIN0=0.85"
volumes:
- name: jenkins-home
persistentVolumeClaim:
claimName: jenkins-public-pvc
在资源对象的定义中使用了pv和pvc做持久化volume所以还需要创建这两种资源对象定义的文件。
$ cat jenkins-pv.yaml
kind: PersistentVolume
apiVersion: v1
metadata:
labels:
name: jenkins-public-pv
name: jenkins-public-pv
namespace: kube-system
spec:
capacity:
storage: 100Gi
accessModes:
- ReadWriteMany
persistentVolumeReclaimPolicy: Retain
nfs:
path: /data/nfs/jenkins-data
server: 192.168.177.43
$ cat jenkins-pvc.yaml
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: jenkins-public-pvc
namespace: kube-system
spec:
accessModes:
- ReadWriteMany
resources:
requests:
storage: 100Gi
selector:
matchLabels:
name: jenkins-public-pv
使用nfs作为共享存储创建pvc使用pv在实际工作中一般使用storageclass替代pv作为动态存储。当然如果为了简单你也可以用挂载nfs目录的方式或者指定节点通过hostpath将工作目录挂在到宿主机方法比较多根据个人实际情况选择合适的存储即可。
部署service资源对象相当于给运行jenkins服务的pod加了一个代理。
如下jenkins-service.yaml
kind: Service
apiVersion: v1
metadata:
labels:
app-name: jenkins
name: jenkins
namespace: kube-system
spec:
ports:
- port: 8080
targetPort: 8080
name: web
- port: 50000
targetPort: 50000
name: agent
selector:
app-name: jenkins
部署ingress服务直接跳过service接管service代理的pod。
如下jenkins-ingress.yaml
apiVersion: extensions/v1beta1
kind: Ingress
metadata:
name: jenkins
namespace: kube-system
spec:
rules:
- host: k8s-jenkins.com
http:
paths:
- path: /
backend:
serviceName: jenkins
servicePort: 8080
创建好ingress后需要添加部署ingress controllernode节点的ip和ingress中定义的host到本地hosts文件做解析。如果没有部署ingress服务可以直接编辑上一步骤中创建service时的类型为nodePort这样就可以通过访问nodeIP:nodePort的方式访问jenkins了。
部署完成后如下所示:
$ kubectl get pods,ingress,svc,deploy -n kube-system |grep jenkins
pod/jenkins-78f658b5d6-25qgl 1/1 Running 0 163d
ingress.extensions/jenkins k8s-jenkins.com 80 10m
service/jenkins ClusterIP 10.254.179.172 <none> 8080/TCP,50000/TCP 198d
deployment.extensions/jenkins 1/1 1 1 198d
部署好后的其他配置与在前面章节安装配置jenkins的方法一样。有关在kubernetes集群中安装jenkins服务的方法就介绍到这里。
配置jenkins连接kubernetes
部署在kubernetes集群内的jenkins配置连接kubernetes相对较简单同样在系统配置页进行配置不用生成上面的一系列证书kubernetes的地址直接写kubernetes的FQDN即可。
如下所示:-
说明
Name 处默认为 kubernetes也可以修改为其他名称这里不做重复介绍。
Kubernetes URL 处可以填写 https://kubernetes.default 为Kubernetes Service 对应的 DNS 记录通过该DNS记录可以解析成该 Service 的 Cluster IP。注意这里也可以填写 https://kubernetes.default.svc.cluster.local 完整 DNS 记录,因为它要符合 service_name.namespace_name.svc.cluster_domain的命名方式。也可以直接填写外部 Kubernetes 的地址 https://ClusterIP:Ports (不推荐)。
Jenkins URL 处应该填写Jenkins的service地址和端口。比如我的http://jenkins.kube-system.svc.cluster.local:8080 也可以写成http://jenkins.kube-system:8080 表示jenkins的service名称和所在的namespace名称同kubernetes url设置类似也是使用Jenkins Service 对应的 DNS 记录以及端口。如果暴露服务的方式为nodeport也可以用http://NodeIP:NodePort本示例没配置的方式根据自己实际情况修改即可。
Credentials :同属于一个集群环境,对于集群认证的过程就不需要了,认证凭证也就不用填写了。
Jenkins虽然能连接kubernetes了但是通过kubernetes还不能生成动态的slave代理因为slave agentjnlp-agent在启动的时候会通过50000默认端口与jenkins master进行通信。Jenkins这个端口默认是关闭的所以还需要开启这个端口。
Manage Jenkins(系统管理)->Configure global Security全局安全配置-> Agents(代理)
说明:
这里指定的端口是jnlp-agent连接jenkins-master使用的端口。
如果Jenkins-master只是在Docker容器(没有使用容器编排系统)中启动的一定要记得将这个端口暴露到外部不然jenkins-master会不知道slave是否已经启动会反复去创建pod直到到超过重试次数。
该端口的默认值是50000如果要修改为其他端口需要修改在创建jenkins”云”时对应的Jenkins Tunnel(通道)参数的配置如果使用50000端口这里可以不用填写如果换成别的端口这里需要单独设定最好是jenkins_url:port的形式。
如果不开启代理端口Jenkins通过kubernetes动态生成slave节点的时候jenkins后台会报如下错误并且pod会不断的生成和删除。
到这里jenkins与kubernetes的集成就成功了。
默认情况下agent代理连Jenkins的接超时时间为100秒。在特殊情况下如果想设置一个不同的值可以将system属性设置org.csanchez.jenkins.plugins.kubernetes.PodTemplate.connectionTimeout为一个不同的值。但是100s的话其实够用了如果超过100s还没生成代理pod就需要根据jenkins日志去排查问题了。如果jenkins连接kubernetes配置成功了大多数情况下的错误一般就是配置podtemplate是什么后面章节会介绍到出现了问题。
示例
上面配置好了如何连接kubernetes下面用一个示例测试配置是否成功。
将下面示例放到pipeline脚本中。
def label = "mypod-${UUID.randomUUID().toString()}"
podTemplate(label: label, cloud: 'kubernetes') {
node(label) {
stage('Run shell') {
sh 'hostname'
sh 'echo hello world.'
}
}
}
说明:
PodTemplate中的label参数用于给Pod指定一个唯一的名称。
PodTemplate中cloud参数的值必须是在系统配置添加的云的名称。
node中直接使用”label”便可以引用上面定义的label变量表示在该pod名称中执行命令如果不指定的话默认还是在Jenkins服务所在主机上执行流水线脚本。
执行结果如下:
根据上面的执行结果可以看到Jenkins通过kubernetes调用的资源对象pod的yaml定义使用的镜像默认为jenkins/jnlp-slave:3.35-5-alpine镜像名称以及一些pod的环境变量等信息。
除了可以在pipeline类型的job中使用kubernetes插件外也可以在其他类型的job中使用但是此时需要在jenkins的系统配置中配置PodTemplate此处先不介绍在后面的实践章节会进行说明。
无论哪种类型的job在job执行的过程中都可以通过kubectl命令在kubernetes集群中查看pod运行的一些详细信息而关于该pod的资源定义又是怎么配置的是否可以自定义呢下节会通过学习kubernetes的基本语法来解答这个问题。

View File

@@ -0,0 +1,505 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
15.Jenkins Kubernetes Plugin介绍与语法详解
在上一节中介绍了jenkins使用不同的安装方式配置连接kubernetes集群的方法以及简单的示例根据kubernetes插件用途的不同对于插件语法的介绍也会区分开来本节主要介绍一下使用kubernetes集群动态生成Jenkins slave pod的插件Kubernetes plugin。
kubernetes plugin
编写pipeline脚本集成kubernetes插件生成动态slave时对于启动agent代理部分的代码编写根据pipeline语法类型的不同会有所差异。但无论有什么差异有一个核心方法不变那就是PodTemplate顾名思义就是pod的模板。
PodTemplate是使用kubernetes插件生成动态slave节点的核心方法。用于配置启动的agent代理Pod的资源对象定义。熟悉kubernetes的应该都知道kubernetes资源对象pod的定义严格来说主要包含两部分Pod各项配置定义和容器各项配置定义。而在Jenkins中则通过kubernetes插件的PodTemplate()方法,对这两部分进行定义,并启动代理。
下面会根据不同的语法类型分别介绍PodTemplate的配置方法。
基本语法
脚本式语法
首先看一个在脚本式pipeline中使用kubernetes插件的基本语法。
podTemplate(label: 'xxx',cloud:'',<pod_option1>,<pod_option2>,...,containers: [
<containerTemplate(name: 'c1',<container_option1>,<container_option2>...)>,
<containerTemplate(<name: 'c2',<container_option1>,<container_option2>...)>,
....
]){
node('xxx'){
stage('test'){
....
}
}
container('c1') {
stage('c1'){
....
}
}
container('c2') {
stage('c2'){
....
}
}
}
说明:
上面PodTemplate包含两部分
第一部分为对kubernetes资源对象Pod自身的一些定义包括pod的标签pod的名称使用的云cloud名称挂载的volume等。
第二部分container为对该pod下container的定义。container是一个列表可以包含一个或多个containerTemplate用于详细描述container的配置参数比如镜像地址容器名称镜像拉取策略等。
podTemplate()定义了一个label名称该lable用于在node()中引用用于生成一个唯一的pod名称。
containerTemplate()定义了该container的名称用于作为流水线执行的环境通过container('container_name')引用给容器。
声明式语法
相对于脚本式语法声明式的语法就显得相对比较混乱。是因为声明式最基础的配置是将kubernetes中资源对象pod的定义的内容通过yaml方式直接放到流水线脚本中如下所示(只展示了部分定义)。
pipeline {
agent {
kubernetes {
label <label_name>
yaml """
kind: Pod
metadata:
name: <pod_name>
spec:
containers:
- name: <container_name>
image: <image_name>
imagePullPolicy: IfNotPresent
command:
- xxxx
tty: true
volumeMounts:
....
restartPolicy: Never
volumes:
......
"""
}
}
stages {
stage('one') {
steps {
container('container_name') {
<command1>
}
container('container_name') {
<command2>
}
}
steps {
sh 'hostname'
}
}
}
说明
agent{}代理配置使用kubernetes关键字通过yaml指令将yaml文件内容直接放到PodTemplate中。
在不同的stage中引用容器的方式也是通过container('container_name')语法格式。
以上便是使用脚本式与声明式流水线集成kubernetes插件动态生成jenkins slave节点的基础语法。需要说明的是对于PodTemplage方法中Pod Template和Container Tempalte的配置既可以在常规的Jenkins UI中配置也可以在pipeline script中通过脚本定义配置信息。本节主要介绍在pipeline script中使用脚本的方式定义PodTemplate在Jenkins UI中配置PodTemplate的方法将在以后介绍并且如果会使用脚本定义PodTemplate后在Jenkins UI中配置相对会简单一些。
参数说明
PodTemplate()既然是方法那么肯定会涉及到参数配置所以接下来先了解一下PodTemplate方法的参数配置为了区分pod配置和container配置下面会将PodTemplate()总体配置拆分为Pod Template配置和container Template配置分别进行说明。
Pod Template 配置部分参数说明-
cloud 用来指定在jenkins的系统配置中设置的云名称默认为kubernetes。在上一节中对于该设置已经做个说明这里不在重复介绍。
name pod名称。
namespace pod运行的namespace命名空间
label pod的标签. 可以自己定义,也可以使用插件内置的 POD_LABEL 变量。
yaml Pod定义的yaml表现形式可参考kubernetes官网。
yamlMergeStrategy 包含merge() 和 override()属性。控制yaml定义是覆盖还是与从声明或者继承的pod模板的yaml定义合并。默认为override()。
containers Pod内容器模板的定义。
serviceAccount Pod使用的服务账户。
nodeSelector 生成的Pod绑定到的node节点以keyvalue的形式表现。
nodeUsageMode 包括NORMAL和EXCLUSIVE两个值它控制Jenkins在选择到这个代理的方式是只有指定该节点标签时使用这个节点还是尽可能多地使用该节点。
volumes 定义的数据卷用于pod和所有容器。
envVars应用于所有容器的环境变量。
envVar 可以理解为在pipeline内定义的环境变量。
secretEnvVar secret变量其值是从Kubernetes 的secret获取的。
imagePullSecrets 一个存放私有仓库认证信息的secret用于从私有仓库拉取镜像时对私有仓库认证。
annotationsPod的注解。
InheritFrom继承的一个或多个Pod模板的列表。
slaveConnectTimeout 代理pod连接jenkins的超时时间以秒为单位__。
podRetention 控制保留Pod的行为用户设置在agent执行完毕后是否保留该pod。有多个选项可以是never()onFailure()always()或default()。如果为空则默认为在经过activeDeadlineSeconds指定的时间之后删除pod 。
activeDeadlineSeconds :如果 podRetention 参数设置为never() 或者 onFailure()pod经过该参数设置的时间后会自动删除。
idleMinutes 允许Pod保持活动状态以供重用直到时间达到从开始执行到经过该值设置时间为止默认为分钟。
showRawYaml 启用或禁用原始yaml文件的输出。默认为true。
runAsUser 用于设置Pod中所有容器运行的用户ID。
runAsGroup 用于设置Pod中所有容器运行的组ID。
hostNetwork 使用宿主机网络类似docker中的network=host。
workspaceVolume 用于工作空间的卷的类型。也就是kubernetes中volume的挂载类型可以是
emptyDirWorkspaceVolume (default)-
dynamicPVC()-
hostPathWorkspaceVolume()-
nfsWorkspaceVolume()-
persistentVolumeClaimWorkspaceVolume()
container Template配置部分参数说明
name :容器名称。-
image :容器使用的镜像。-
envVars应用于容器的环境变量补充和覆盖在pod级别设置的envvar。-
envVar :同上。-
secretEnvVar :同上。-
command :容器要执行的命令。-
args :执行命令要传的参数。-
ttyEnabled 标记容器开启tty。-
livenessProbe :容器探针。-
ports :容器端口。-
alwaysPullImage :拉取镜像策略-
runAsUser 用于运行容器的用户ID。-
runAsGroup 用于运行容器的用户组ID。.
上面的参数对于脚本式和声明式pipeline都是适用的。对于声明式的语法也可以通过在前面介绍的片段生成器生成相应的PodTemplate语法片段。
有关Pod Template和Container Template的更多参数可以参考kubernetes中资源对象pod的yaml定义默认情况下Pod中支持的参数在pipeline中都是可以使用的。
了解了基本语法,下面通过一些基本的示例来加深一下理解。
语法示例
针对脚本式语法和声明式语法的不同,对于示例也会分开介绍。
脚本式语法
基础示例如下:
podTemplate {
node(POD_LABEL) {
stage('Run shell') {
sh 'echo hello world'
}
}
}
说明
对于脚本式语法node()块是被包含在PodTemplate方法里的。
该示例会启动一个kubernetes Pod并输出hello world。
node(POD_LABEL) POD_LABEL用于给pipeline的agent代理(kubernetes pod)指定一个标签确保pod名称的唯一性这个标签可以自定义通过podTemplate 的label参数指定也可以像上面示例一样使用POD_LABEL这个是在kubernetes plugin插件的1.17及以后版本添加的功能用于自动生成pod 标签,标签的格式为${JOB_NAME)-${BUILD_NUMBER}-hash_number。
通过上面的示例会发现PodTemplate既没有定义Pod参数配置也没有定义container的参数配置。在没有配置这些参数的情况下pipeline会使用默认的PodTemplate配置在执行上面的pipeline后Jenkins的构建日志中会列出该Pod的yaml定义可参考上面最初的示例包括镜像版本jenkins的环境变量等。由于是默认的配置对我们编写pipeline 持续交付脚本帮助不大我们需要自己定义Pod运行参数所以这里就不在重复介绍默认的pod定义。有兴趣的可以执行上面的示例结果会通过jenkins的console日志显示出来。
对于上面的基础示例可以添加container参数用来自定义pod内容器的启动参数比如
podTemplate(containers: […]) {
node(POD_LABEL) {
stage('Run shell') {
container('mycontainer') {
sh 'echo hello world'
}
}
}
}
该示例中container参数定义了启动容器用到的镜像配置模板在不填写container参数的情况默认的jnlp代理(container)的配置参数如下所示:
containerTemplate(name: 'jnlp', image: 'jenkins/jnlp-slave:3.35-5-alpine', args: '${computer.jnlpmac} ${computer.name}')
需要注意的是:
对于${computer.jnlpmac} ${computer.name}这两个参数是jnlp-agent该示例为jnlp-slave:3.35-5-alpine镜像运行必须的参数如果不写或少写一个都会导致slave pod生成失败。
另一个要说明的点是该contianer的名称实际工作中我们可能需要使用自己定义的一些镜像这些镜像如果是jnlp-agent类型启动时通过slave agent包启动并连接Jenkins master节点的镜像则容器名称就必须为jnlp否则有可能会导致pod生成失败至于原因将在下一章节进行说明。
对于默认的jnlp代理(container)相应的声明式语法如下:
apiVersion: v1
kind: Pod
spec:
containers:
- name: jnlp
image: 'jenkins/jnlp-slave:3.35-5-alpine'
args: ['\$(JENKINS_SECRET)', '\$(JENKINS_NAME)']
通过该示例可以知道上面脚本式语法中的两个参数是做什么用的了吧。如果还不知道可参考jenkins的docker-jnlp-slave
默认的container配置就只包含上面的三个参数nameimageargs除了这些参数外还有一些经常用到的比如用于分配伪终端的ttyEnabled参数、设置拉取镜像策略的alwaysPullImage参数、配置是否使用privileged特权模式的参数、设置容器启动内存和cpu等。
更多与容器相关的参数配置可参考如下示例:
containerTemplate(
name: 'mariadb',
image: 'mariadb:10.1',
ttyEnabled: true,
privileged: false,
alwaysPullImage: false,
workingDir: '/home/jenkins/agent',
resourceRequestCpu: '50m',
resourceLimitCpu: '100m',
resourceRequestMemory: '100Mi',
resourceLimitMemory: '200Mi',
envVars: [
envVar(key: 'MYSQL_ALLOW_EMPTY_PASSWORD', value: 'true'),
secretEnvVar(key: 'MYSQL_PASSWORD', secretName: 'mysql-secret', secretKey: 'password'),
...
],
ports: [portMapping(name: 'mysql', containerPort: 3306, hostPort: 3306)]
),
与pod Template相关的参数配置如下
podTemplate(cloud: 'kubernetes', label: 'my-defined', containers: [....
],
volumes: [
emptyDirVolume(mountPath: '/etc/mount1', memory: false),
secretVolume(mountPath: '/etc/mount2', secretName: 'my-secret'),
configMapVolume(mountPath: '/etc/mount3', configMapName: 'my-config'),
hostPathVolume(mountPath: '/etc/mount4', hostPath: '/mnt/my-mount'),
nfsVolume(mountPath: '/etc/mount5', serverAddress: '127.0.0.1', serverPath: '/', readOnly: true),
persistentVolumeClaim(mountPath: '/etc/mount6', claimName: 'myClaim', readOnly: true)
],
namesapce: default,
serviceaccount: default,
imagePullSecrets: [ 'pull-secret' ], //用于在启动容器时从私有仓库拉取镜像,而无需在宿主机对私有仓库进行认证登录
annotations: [
podAnnotation(key: "my-key", value: "my-value")
...
])
编写pipeline脚本时对于PodTemplate方法中的某个参数如果不知道如何定义都可以参考上面展示的模版范例或者通过在前面的介绍的片段生成器通过jenkins ui定义来生成语法片段。
podTemplate方法的配置除了直接使用key:value方式定义外也可以通过以yamlkubernetes中资源对象文件的默认后缀文件的方式定义比如下面示例
podTemplate(yaml: """
apiVersion: v1
kind: Pod
metadata:
labels:
some-label: some-label-value
spec:
containers:
- name: busybox
image: busybox
command:
- cat
tty: true
"""
) {
node(POD_LABEL) {
container('busybox') {
sh "hostname"
}
}
}
除了使用yaml关键字外也可以使用yamlFile关键字用于指定一个yaml文件该文件通常与jenkinsfile文件一起存在于源码仓库中。通过从源码仓库中拉取Jenkinsfile文件和该参数指定的文件下载后会自动执行。这种方式需要改变pipeline job中脚本定义的类型默认使用Pipeline script使用yamlFile的方式就该使用Pipeline script from SCM前面章节有介绍的方式了。
运行多个容器
对于不同的代码要是使用不同的构建环境怎么办在container参数中可以定义多个containerTemplate来满足此需求。也就是在一个pod中运行多个容器熟悉kubernetes的应该都知道同一个pod内的容器共享主机名、网络、vloume等信息所以在pod内运行一个或者多个容器它们之间并没有什么区别。如下示例
podTemplate(label:'test', containers: [
containerTemplate(name: 'maven', image: 'maven:3.3.9-jdk-8-alpine', ttyEnabled: true, command: 'cat'),
containerTemplate(name: 'golang', image: 'golang:1.8.0', ttyEnabled: true, command: 'cat')
]) {
node('test') {
stage('Get a Maven project') {
git 'https://github.com/jenkinsci/kubernetes-plugin.git'
container('maven') {
stage('Build a Maven project') {
sh 'echo build maven'
}
}
}
stage('Get a Golang project') {
git url: 'https://github.com/hashicorp/terraform.git'
container('golang') {
stage('Build a Go project') {
sh 'echo build go'
}
}
}
}
}
说明:
该示例定义多个容器在不同的stage通过定义的容器的名称来使用该容器作为流水线执行的环境。
声明式语法
在声明式语法中使用PodTemplate方法与在脚本式语法中使用该方法的方式基本一致。主要区别在于agent的定义方式上。
首先看一个在声明式脚本中使用PodTemplate的基础示例
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
some-label: some-label-value
spec:
containers:
- name: maven
image: maven:alpine
command:
- cat
tty: true
- name: busybox
image: busybox
command:
- cat
tty: true
"""
}
}
stages {
stage('Run maven') {
steps {
container('maven') {
sh 'mvn -version'
}
container('busybox') {
sh '/bin/busybox'
}
}
}
}
}
说明:
使用声明式脚本agent的type必须是”any, docker, dockerfile, kubernetes, label, none”中的一种本节为kubernetes插件的使用所以agent的type为kubernetes。
kubernetes使用yaml方式定义Pod和container模板配置信息在stage阶段使用container指令指定运行的容器提供流水线执行的环境。
该pod定义未指定pod工作的namespace命名空间这里需要说明一下的是如果yaml定义没有指定namespace则默认使用在jenkins系统配置中添加kubernetes云时指定的namespace如果这里也没设定namespace的名称则默认使用default namespace如果都指定了则默认使用在pipeline脚本中定义的namespace。
上面示例 stage阶段对容器的引用方式与脚本式流水线引用容器的方式一样。
有关yaml更全面的定义可以参考kubernetes中对资源对象pod的定义参考kubernetes官网
除了可以在脚本式语法中使用yamlFile指令外在声明式语法中也可以使用yamlFile指令。如下示例
pipeline {
agent {
kubernetes {
yamlFile 'KubernetesPod.yaml'
}
}
stages {
...
}
}
说明
示例中指定KubernetesPod.yaml文件与jenkinsfile文件放在同一源码仓库的同级目录下配置job pipeline时使用Pipeline script from SCM。如果指定的kubernetesPod.yaml文件与Jenkinsfile文件没在同一级目录指定该文件时需要加上相对路径。
使用多个容器
默认情况下声明式脚本的模板不会从父模板继承。所以即便在定义了全局agent代理的情况下也可以在指定的stage单独定义agent。
如下示例定义了全局agent也可以在stage定义agent互不影响。
pipeline {
agent {
kubernetes {
label 'parent-pod'
yaml """
spec:
containers:
- name: golang
image: golang:1.6.3-alpine
command:
- cat
tty: true
"""
}
}
stages {
stage('Run maven') {
agent {
kubernetes {
label 'nested-pod'
yaml """
spec:
containers:
- name: maven
image: maven:3.3.9-jdk-8-alpine
command:
- cat
tty: true
"""
}
}
steps {
container('maven') {
sh 'mvn -version'
}
}
}
stage('run go'){
steps {
container('golang') {
sh 'go --version'
}
}
}
}
}
以上就是使用kubernetes插件动态生成jenkins slave节点时用到的一些语法示例。在学会了前面jenkins与docker pipeline插件集成的语法及示例后对于本章节的内容学习应该相对简单很多。
有关在jenkins ui中定义PodTemplate以及自定义image镜像等内容会在下一章以实际案例的方式说明。

View File

@@ -0,0 +1,594 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
16.使用Kubernetes编排Jenkins Slave节点持续交付项目
在上一节的内容中简单介绍了一下使用kubernetes插件动态生成slave节点的基本语法和使用示例本节会根据上节介绍到的一些语法知识进行一个基础的实践并继续补充一些使用该插件时的一些配置细节。
为了方便读者理解本小节内容会分多个版本以由浅入深的方式尽量将之前的pipeline语法融会贯通。
基础版
本次版本说明:
1、 PodTemplate 配置以pipeline script的方式放到pipeline脚本里没有在Jenkins 系统配置里进行定义。
2、 Jenkins默认的Jenkins slave镜像为jnlp-slave:3.35-5-alpine这是一个Jnlp方式的agent镜像该镜像没有拉取/编译代码以及docker命令需要基于此镜像自定义一个新的镜像。
3、 构建应用镜像时的Dockerfile文件通过挂载nfs的方式引用。
4、 使用最基础的shell命令实现一个简单的持续交付脚本以方便了解该持续交付的流程。
基于上面条件用脚本式语法编写的pipeline脚本如下
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
serviceAccount: 'default', containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:latest",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: true),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
],
){
node('pre-CICD') {
stage('build') {
container('jnlp') {
stage('git-clone') {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
}
stage('Build a Maven project') {
sh 'cd base-nop/fw-base-nop && mvn clean install -DskipTests -Denv=beta'
}
stage('build docker image'){
sh 'cp /tmp/Dockerfile base-nop/fw-base-nop/target/'
sh '/usr/bin/docker build -t 192.168.176.155/library/fw-base-nop:xxxx --build-arg jar_name="fw-base-nop.jar" base-nop/fw-base-nop/target/'
}
stage('push registry') {
sh '''
docker login -u admin -p da88e43d88722c2c9ca09da644eeb015 192.168.176.155
docker push 192.168.176.155/library/fw-base-nop:xxxx
docker rmi 192.168.176.155/library/fw-base-nop:xxxx
'''
}
}
}
}
}
说明:
PodTemplate(….)配置的Pod模板配置参数以及相关说明在语法章节有过介绍不再过多赘述不过有一点需要注意的是containerTemplate 下的name参数和image参数的设定。
Jenkins的Agent大概分两种 一是基于SSH的由master主动连接slave/Agent节点 二是基于JNLP的使用的是HTTP协议由Agent/slave节点主动连接master节点每个Agent需要配置一个独特的secret。 可以参考官方说明。
1、 如果name参数配置的值为jnlp就表示使用基于Jnlp方式的jnlp-agent自启动连接Jenkins master。这就需要保证image参数指定的镜像里包含启动jnlp-agent的命令官方提供的镜像启动命令为jenkins-slave旧版本或者jenkins-agent新版本命令同时需要指定启动参数${computer.jnlpmac} ${computer.name如果指定的镜像里没有这些命令或者没有启动参数动态pod就会创建失败。
2、 如果name参数配置的值不是jnlp而image参数指定的镜像为jnlp agent镜像时动态pod也会生成失败。这是因为当该参数设置的值不是jnlp时动态生成的pod默认会启动两个容器使用的镜像一个是连接jenkins master默认的jnlp-agent镜像也就是jnlp-slave:3.35-5-alpine另一个就是通过image参数自定义的镜像。这两个jnlp-agent镜像启动时都会去连接jenkins master而基于jnlp方式使用agent时每个客户端只能有一个agent和一个secret所以同时启动多个jnlp-agent就会导致Pod启动失败。
3、 如果name参数配置的值不是jnlp并且image参数指定的镜像为非jnlp agent镜像时需要注意此时启动容器时的参数应该去掉同上面一样动态生成的pod包含两个容器一个名称是jnlp根据默认的jnlp agent镜像默认在后台启动一个名称是通过name参数设置的值同时两个容器都能作为pipeline脚本执行的环境使用时都是通过container('container_name')引用。
4、 如果Pod内有多个容器并且只要有一个容器为jnlp容器其他容器的名称和镜像使用没有限制也就是说容器名称可以不是jnlp但是镜像可以jnlp agent镜像。
5、上面示例里的image参数配置的镜像为根据默认的jnlp-agent镜像自定义的镜像dockerfile如下
FROM jenkins/jnlp-slave:3.35-5-alpine
USER root
RUN apk add maven git
COPY settings.xml /usr/share/java/maven-3/conf/settings.xml
COPY docker/docker /usr/bin/docker
其中settings.xml文件为maven配置文件这里需要根据实际情况考虑要不要添加。
执行结果如下:
声明式脚本
对于该pipeline脚本相应的声明式语法如下
pipeline {
agent {
kubernetes {
yaml """
apiVersion: v1
kind: Pod
metadata:
labels:
some-label: some-label-value
namespace: 'default'
spec:
containers:
- name: jnlp
image: 192.168.176.155/library/jenkins-slave:latest
args: ['\$(JENKINS_SECRET)', '\$(JENKINS_NAME)']
tty: true
privileged: true
alwaysPullImage: false
volumeMounts:
- name: mount-nfs
mountPath: /tmp
- name: mount-docker
mountPath: /var/run/docker.sock
volumes:
- name: mount-nfs
nfs:
path: /data/nfs
server: 192.168.177.43
- name: mount-docker
hostPath:
path: /var/run/docker.sock
"""
}
}
stages {
stage('Run maven') {
steps {
container('jnlp') {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])"
}
}
}
stage('build code') {
steps {
container('jnlp') {
sh 'cd base-nop/fw-base-nop && mvn clean install -DskipTests -Denv=beta'
}
}
}
stage('build image'){
steps{
container('jnlp') {
sh 'cp /tmp/Dockerfile base-nop/fw-base-nop/target/'
sh '/usr/bin/docker build -t 192.168.176.155/library/fw-base-nop:xxxx --build-arg jar_name="fw-base-nop.jar" base-nop/fw-base-nop/target/'
}
}
}
stage('push to registry') {
steps {
container('jnlp') {
sh '''
docker login -u admin -p da88e43d88722c2c9ca09da644eeb015 192.168.176.155
docker push 192.168.176.155/library/fw-base-nop:xxxx
docker rmi 192.168.176.155/library/fw-base-nop:xxxx
'''
}
}
}
}
}
说明:
声明式pipeline语法对于格式有严格要求所以如果遇到有语法错误的情况需要有耐心排查。
使用kubernetes插件时使用脚本式流水线语法相对于声明式语法要更加简单所以建议使用脚本式语法。
Config File Provider Plugin
关于maven配置文件settings.xml的使用还有另一种方式就是通过jenkins的Config File Provider Plugin插件。这个插件的作用就是在 Jenkins 中存储以properties、xml、json、Groovy结尾的文件以及Maven settings.xml内容。下面演示一下如何使用该插件。
点击”系统管理—>Managed files—>Add a new Config—>Global Maven settings.xml“将上面setting.xml文件的内容放到”Content”对应的输入框里如下所示
编辑好提交即可。
然后使用片段生成器configfileProvider:...根据配置的maven settings.xml文件生成语法片段比如
配置好后就可以编辑pipeline脚本了为了测试该插件的作用我直接使用docker hub官方的maven镜像脚本如下
podTemplate(cloud: 'kubernetes', label: 'pre-CICD', containers: [
containerTemplate(
name: 'maven',
image: "maven:latest",
command: 'cat',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false)
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
],
){
node('pre-CICD') {
stage('build') {
container('maven') {
stage('clone code') {
sh "git clone http://root:[email protected]/root/base-nop.git"
}
configFileProvider([configFile(fileId: 'f67fdaf1-4b17-4caa-86ad-e841f387ac7a', targetLocation: 'settings.xml')]) {
stage('Build project') {
sh 'cd base-nop/fw-base-nop && mvn clean install -DskipTests -Denv=beta --settings ${WORKSPACE}/settings.xml'
}
}
}
}
}
}
说明:
通过mvn命令编译代码时加上--settings参数用于指定该文件。上面示例可以看到在指定settings.xml文件时添加了该job的workspace路径这是因为在使用configFileProvider插件时该插件默认会将指定的settings.xml文件拷贝到job的workspace目录下如果不添加路径有可能会报该文件找不到的错误。
基础版的持续交付脚本就算是完成了。此版本的脚本只是为了让大家熟悉一下代码编译和镜像构建的一个基本流程,下面的版本将会该该版本进行一个简单的优化。继续往下看。
进阶版
说明(相对上一个版本):
1、 本次版本开始引入变量,以提高灵活性。需要注意的是,引用变量的时候要使用双引号(“”)。-
2、 将脚本内部分命令通过kubernetes插件和Docker插件内的相关方法实现。-
3、 挂载共享目录用于挂载maven的.m2仓库提高代码编译效率。
首先,用代码片段生成器生成部分命令的相关语法片段:
checkoutCheck out from version control生成拉取代码的片段前面有介绍过不在过多说明。
使用docker pipeline插件的withDockerRegistry片段生成器生成对docker Registry认证的语法片段如下所示
pipeline脚本如下
def project_name = 'fw-base-nop'
def registry_url = '192.168.176.155'
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
, containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:latest",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
nfsVolume(mountPath: '/root/.m2', serverAddress: '192.168.177.43', serverPath: '/data/nfs/.m2', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
],
nodeSelector: 'kubernetes.io/hostname=192.168.176.160',
){
node('pre-CICD') {
stage('build') {
container('jnlp') {
stage('clone code') {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
}
stage('Build a Maven project') {
sh "cd ${project_name} && mvn clean install -DskipTests -Denv=beta"
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/library/${project_name}-${BUILD_NUMBER}:${imageTag}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/library/${project_name}-${BUILD_NUMBER}:${imageTag}"
}
}
}
}
}
}
说明
volumes配置中挂载maven的.m2仓库到nfs server上以提高代码编译速度。
clone code阶段获取应用代码最后提交时的commit short id作为镜像构建时的tag。
使用docker pipeline语法进行镜像的构建和推送到仓库操作。
nodeSelector 用来选择一台固定的节点专门用来进行构建工作以防止在拉取基础镜像时生成pod过慢影响效率这里我使用了node节点默认自带的标签如果不想用这个标签也可以自定义一个标签。
该版本同样使用pipeline script的方式将脚本代码直接放到了job中。实际工作中我们也可以将上面的代码放到Jenkinsfile文件中并将该文件放到应用代码仓库的根目录也可以单独放置在配置pipeline job时使用pipeline script from SCM的方式配置该Jenkinsfile的仓库地址拉取后会自动进行构建。比如
执行后会自动拉取该文件并执行。需要说明的是如果该文件放到了应用代码仓库的根目录拉取该文件时并不会拉取代码所以在Jenkinsfile中拉取代码的操作是不能去掉的。
对应的声明式脚本只需要在版本一的基础上替换做了更改的部分并将用到的docker pipeline插件的方法加上script{}块即可。
在Cloud中定义PodTemplate
版本说明:
前两个版本的PodTemplate配置均通过在pipeline中使用代码的方式进行配置的本次版本将PodTemplate配置通过Jenkins UI放到了全局配置里。
编辑最开始“配置Jenkins连接kubernetes”时创建的cloud增加PodTemplate和container的配置。如下所示
-
上面配置的参数说明应该不用再重复介绍了有不懂的参数可以参考之前的文章即可。配置保存后修改pipeline只需要将PodTemplate方法配置的部分去掉即可。
pipeline如下
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
//node 这里依旧是填写podtemplate设置的标签的名称
node('pre-CICD') {
stage('build') {
container('jnlp') {
stage('git-clone') {
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
}
stage('Build a Maven project') {
sh "cd ${project_name} && mvn clean install -DskipTests -Denv=beta"
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/library/${project_name}-${BUILD_NUMBER}:${imageTag}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/library/${project_name}-${BUILD_NUMBER}:${imageTag}"
}
}
}
}
}
同版本2一样在配置pipeline类型的job时该脚本既可以通过Pipeline script的方式使用也可以通过Pipeline script from SCM的方式使用。
同时,在使用其他类型(比如 自由风格类型、maven类型的job时也能够使用该PodTemplate方法用于动态生成slave。比如创建一个自由风格类型的job配置job时勾选 Restrict where this project can be run 在显示的Label Expression 输入框里输入我们上边配置PodTemplate时设定的标签Labels 名称 pre-CICD即可。
构建job时就会自动启动这个代理如下所示
需要注意的是如果使用这种方式的话pipeline中的代码就不在适合此job需要通过编写shell脚本或者其他方式比如ansible实现持续交付。
sonarqube
既然是持续交付那么肯定少不了代码分析。上面一系列版本只是将代码进行了编译和构建正常流程还需要进行代码质量分析这就用到了之前搭建的sonarqube平台。关于sonarqube在前面的文章有详细介绍这里不重复说明接下来主要看一下如何在pod中使用sonar-scanner命令。
使用sonar-scanner命令既可以使用在Jenkins UI中配置的sonar-scanner命令工具也可以通过自定义sonar-scanner容器镜像的方式引用此命令。下面对这两种方法分别进行介绍。
首先看一下使用jenkins系统内配置的sonar-scanner工具PodTemplate配置同上实现代码质量分析。
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
node('pre-CICD') {
stage('build') {
stage('git-clone') {
container('jnlp'){
stage('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
}
stage('Build a Maven project') {
echo "${project_name}"
sh "cd ${project_name} && mvn clean install -DskipTests -Pproduct -U"
}
}
}
stage('sonar'){
def sonarqubeScannerHome = tool name: 'sonar-scanner-4.2.0'
withSonarQubeEnv(credentialsId: 'sonarqube') {
sh "${sonarqubeScannerHome}/bin/sonar-scanner -X "+
"-Dsonar.login=admin " +
"-Dsonar.language=java " +
"-Dsonar.projectKey=${JOB_NAME} " +
"-Dsonar.projectName=${JOB_NAME} " +
"-Dsonar.projectVersion=${BUILD_NUMBER} " +
"-Dsonar.sources=${WORKSPACE}/fw-base-nop " +
"-Dsonar.sourceEncoding=UTF-8 " +
"-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " +
"-Dsonar.password=admin "
}
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/library/${project_name}-${BUILD_NUMBER}:${imageTag}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/library/${project_name}-${BUILD_NUMBER}:${imageTag}"
}
}
}
}
该示例通过tool指令指定了在jenkins的global tool configuration里配置的sonar-scanner环境变量并赋给一个变量通过withSonarQubeEnv片段生成器对sonar认证并进行代码分析操作。有关sonar-scanner命令各参数的说明在”基础工具配置”章节有做介绍这里不在过多说明。
使用sonar-scanner自定义镜像
除了使用jenkins系统配置sonar-scanner工具也可以根据sonar-scanner工具自定义一个sonar-scanner镜像并使用该镜像启动一个容器来进行代码分析工作。
sonar-scanner镜像默认可以从docker hub上获取但是为了杜绝不必要的错误测试中曾经遇到过我选择了自定义。可以使用之前定义好的jnlp-agent镜像作为基础镜像这样在持续交付步骤使用一个镜像就可以完成所有工作有些人觉得这样会使镜像臃肿也可以使用一个包含jdk的小体积镜像作为基础镜像当然也可以根据自己的实际情况自定义。
无论使用哪种镜像,都需要修改${SONAR-SCANER_HOME}/bin/sonar-scanner文件下的use_embedded_jre参数的值默认为true需要改成fasle如下
use_embedded_jre=false
为什么要修改此参数呢是因为use_embedded_jre参数为true时sonar-scanner命令默认会使用自己提供的jre路径为$sonar_scanner_home/jre而不会使用系统环境下的jre所以需要将此参数改为false否则会报找不到java环境变量的错误如下所示
sonar-scanner:exec: line 73: xxx/sonar-scanner-4.2.0-linux/jre/bin/java: not foun
修改好后编辑dockerfile比如使用上面构建的jnlp-agent镜像作为基础镜像。
FROM 192.168.176.155/library/jenkins-slave:latest
COPY sonar-scanner-4.2.0/ /opt/sonar-scanner-4.2.0/
ENV SONAR_SCANNER_HOME /opt/sonar-scanner-4.2.0/
ENV SONAR_RUNNER_HOME ${SONAR_SCANNER_HOME}
ENV PATH $PATH:${SONAR_SCANNER_HOME}/bin
如果想使用openjdlk镜像作为基础镜像可以将基础镜像修改为openjdk:8-jre-alpine3.7,编辑好构建即可。
使用docker命令测试一下。
docker run -it --rm 192.168.176.155/library/jenkins-slave:sonar sonar-scanner
输出如下信息表示镜像构建成功 。
$ docker run -it --rm a9592584d82d sonar-scanner
INFO: Scanner configuration file: /opt/sonar-scanner-4.2.0/conf/sonar-scanner.properties
INFO: Project root configuration file: NONE
INFO: SonarQube Scanner 4.2.0.1873
INFO: Java 1.8.0_212 IcedTea (64-bit)
INFO: Linux 5.4.13-1.el7.elrepo.x86_64 amd64
INFO: User cache: /root/.sonar/cache
INFO: SonarQube server 6.7.5
......
构建好镜像对pipeline脚本进行简单修改为了区分显示又新添加一个containerTemplate配置用于定义sonar镜像同时在引用镜像时新增了一个container指令用于引用新镜像。如下示例
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
serviceAccount: 'default', containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:latest",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
containerTemplate(
name: 'sonar',
image: "192.168.176.155/library/jenkins-slave:sonar",
ttyEnabled: true,
privileged: true,
command: 'cat',
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
nfsVolume(mountPath: '/root/.m2', serverAddress: '192.168.177.43', serverPath: '/data/nfs/.m2', readOnly: false),
],
){
node('pre-CICD') {
stage('build') {
container('jnlp'){
stage('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
}
stage('Build a Maven project') {
echo "${project_name}"
sh "cd ${project_name} && mvn clean install -DskipTests -Pproduct -U"
}
}
container('sonar'){
stage('sonar test'){
withSonarQubeEnv(credentialsId: 'sonarqube') {
sh "sonar-scanner -X "+
"-Dsonar.login=admin " +
"-Dsonar.language=java " +
"-Dsonar.projectKey=${JOB_NAME} " +
"-Dsonar.projectName=${JOB_NAME} " +
"-Dsonar.projectVersion=${BUILD_NUMBER} " +
"-Dsonar.sources=${WORKSPACE}/fw-base-nop " +
"-Dsonar.sourceEncoding=UTF-8 " +
"-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " +
"-Dsonar.password=admin "
}
}
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/library/${project_name}:${imageTag}-${BUILD_NUMBER}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/library/${project_name}:${imageTag}-${BUILD_NUMBER}"
}
}
}
}
}
有关jenkins与kubernetes集成持续交付的配置就简单的介绍到这里下一章节内容介绍一下将代码持续部署到kubernetes集群中。

View File

@@ -0,0 +1,804 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
17.使用Kubernetes插件持续部署服务到Kubernetes集群
kubernetes系列插件的另一个用途就是将编译好的代码持续部署到kubernetes集群中用到的插件主要为
Kubernetes CLI。-
Kubernetes Continuous Deploykubernetes cd
下面分别介绍一下这两款插件。
Kubernetes CLI
该插件的主要功能是执行流水线脚本时通过在Jenkins系统中配置的连接kubernetes集群时创建的凭据使用kubectl命令与kubernetes集群交互。
withKubeConfig() 是该插件的核心方法,包含的参数如下(前两个参数为必须):
credentialsId凭据id还记得配置jenkins连接kubernetes时创建的凭据吗就是这个凭据的id。
serverUrlkubernetes集群的ApiServer的地址也就是配置jenkins集成kubernetes时配置的kubernetes的地址。
caCertificate用于验证API服务器的证书。如果未提供参数则跳过验证非必须。
clusterName生成的集群配置的名称默认为k8s。
namespace默认的上下文所在的命名空间。
contextName生成的上下文配置的名称默认值为k8s。
了解了上面列出的参数在来看一下withKubeConfig() 的语法,大致如下:
node {
stage('List pods') {
withKubeConfig([credentialsId: '<credential-id>',
caCertificate: '<ca-certificate>',
serverUrl: '<api-server-address>',
contextName: '<context-name>',
clusterName: '<cluster-name>',
namespace: '<namespace>'
]) {
sh 'kubectl get pods'
}
}
}
也可以在片段生成器中通过withKubeConfigSetup Kubernetes CLI生成相应的语法片段这里就不在演示了有兴趣的可以自己试一下。
该方法除了必须项参数外,其他参数用的都比较少,所以这里就不再通过过多的篇幅去介绍如何使用,主要介绍一下两个必须参数的使用用例。
下面以一个示例来演示一下:
node {
stage('get nodes') {
withKubeConfig([credentialsId: 'afffefbf-9216-41dc-b803-7791cdb87134', serverUrl: 'https://192.168.176.156:6443']) {
sh 'kubectl get nodes'
}
}
}
说明:
需要说明的是要使用该插件首先要确保执行pipeline的agent上有kubectl二进制可执行文件并具有执行权限。如果执行该pipeline时报如下错误
java.io.IOException: error=2, No such file or directory
at java.lang.UNIXProcess.forkAndExec(Native Method)
......
Caused: java.io.IOException: Cannot run program "kubectl": error=2, No such file or directory
......
Finished: FAILURE
该错误表示没有kubectl命令或者有该命令但是从全局环境变量中没有找到该命令。
解决方法是修改/etc/profile文件的export PATH=参数添加存放常用的可执行文件的路径或者将kubectl二进制文件放到该参数指定的路径下比如
export PATH=/root/local/bin:/usr/bin:/usr/sbin:$PATH
表示kubectl放到这些路径下是可以被找到的修改完该文件执行source /etc/profile命令使修改生效。
在jenkins中使用kubectl与kubernetes集群交互的实际工作场景并不多见大多数场景是使用该命令根据定义的资源对象文件去部署代码。
使用kubectl容器
使用kubectl命令的另一种方式就是使用kubectl容器可以根据一个小体积的镜像自定义一个可以与kubernetes集群交互的镜像然后每次部署时就使用该镜像完成部署操作。比如下面的dockerfile。
FROM alpine
USER root
COPY kubectl /usr/bin/kubectl
RUN chmod +x /usr/bin/kubectl && mkdir -p /root/.kube/
COPY config /root/.kube/config
说明:
kubectl 文件为与集群版本对应的二进制可执行文件。
config 文件为kubectl客户端命令与 kubernetes集群交互的配置认证文件默认位置为/root/.kube/config。
根据这个dockerfile构建镜像。
docker build -t alpine-kubectl:1.14 -f dockerfile .
我的kubernetes集群版本为1.14所以就打个1.14的tag。
镜像构建好以后可通过命令查看该镜像是否有效。
$ docker run -it --rm alpine-kubectl:1.14 kubectl get nodes
NAME STATUS ROLES AGE VERSION
192.168.176.151 Ready <none> 171d v1.14.1
192.168.176.152 Ready <none> 171d v1.14.1
......
看到命令成功执行说明自定义镜像构建成功接下来就可以使用此镜像操作kubernetes集群了。
如下示例:
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'TEST-CD',
containers: [
containerTemplate(
name: 'test-cd',
image: "192.168.176.155/library/alpine-kubectl:1.14",
ttyEnabled: true,
privileged: true,
alwaysPullImage: false)
],
){
node('TEST-CD'){
stage('test-cd'){
container('test-cd'){
sh 'kubectl get nodes'
}
}
}
}
该示例仅做参考,在实际工作中可以根据实际情况定义要如何实现持续部署到集群。
Kubernetes Continuous Deploy
简称kubernetes cd该插件用于将kubernetes中的资源对象部署到kubernetes集群中。
它提供以下功能:
通过SSH从主节点获取cluster credentials ,也可以手动配置它。
资源配置的变量替换,使您可以进行动态资源部署。
提供登录私有Docker registry的登录凭证管理。
无需在Jenkins agent节点上安装kubectl工具。
支持的kubernetes资源对象如下
下面列出了支持的资源对象类型以及所支持的扩展组信息。
ConfigMap (v1)
Daemon Set (apps/v1、extensions/v1beta1、apps/v1beta2)
Deployment (apps/v1、apps/v1beta1、extensions/v1beta1、apps/v1beta2)
Ingress (extensions/v1beta1、 networking.k8s.io/v1beta1)
Job (batch/v1)
Namespace (v1)
Pod (v1)
Replica Set (apps/v1、extensions/v1beta1、apps/v1beta2)
Replication Controller (v1) - 不支持滚动升级,如果需要,请使用 Deployment.
Secret (v1) - 同样支持secret配置
Service (v1)
Stateful Set (apps/v1、apps/v1beta1、apps/v1beta2) apps/v1
Cron Job (batch/v1beta1、batch/v2alpha1)
Horizontal Pod Autoscaler(autoscaling/v1、autoscaling/v2beta1、autoscaling/v2beta2)
Network Policy (networking.k8s.io/v1)
Persistent Volume (v1)
Persistent Volume Claim (v1)
ClusterRole (rbac.authorization.k8s.io/v1)
ClusterRoleBinding (rbac.authorization.k8s.io/v1)
Role (rbac.authorization.k8s.io/v1)
RoleBinding (rbac.authorization.k8s.io/v1)
ServiceAccount (v1)
上面列出的资源对象熟悉kubernetes的同学应该都知道是做什么用的。对于使用该插件持续部署应用代码来说了解Deployment、service、configmap、ingress这些资源对象就足够了几乎所有的持续部署使用的资源对象都是Deployment而对于其他类型的资源对象一般不会通过jenkins去间接操作。本系列文章不是kubernetes相关的课程所以有关上面列出的资源对象的内容就不过多介绍。
要使用cd插件与kubernetes集群交互就需要添加一个凭证Jenkins使用该凭证可以通过kubectl命令对kubernetes集群有操作权限与配置jenkins集成kubernetes时创建的凭证不同的是该凭证的类型为Kubernetes configuration (kubeconfig)是直接使用kubectl命令与kubernetes集群交互时使用的kubeconfig文件默认路径/root/.kube/config而不用根据文件内容去生成相应的认证证书。
在jenkins中创建使用kubeconfig文件凭证的方式也分多种
直接输入kubeconfig内容。
在Jenkins master上设置kubeconfig的路径。
从远程SSH服务器获取kubeconfig文件。
下面以直接使用kubeconfig文件内容为例演示如何创建该凭证。
点击”Credentials(凭据)-—-> system(系统)-—> Global Credentials(全局凭据)-—> Add Credentials(添加凭据)” ,如下图所示:
直接将kubeconfig文件的内容粘贴到Content框里然后设置一个ID名称即可在编写pipeline脚本时使用创建的这个凭据的ID名称来对其引用。全部配置好保存即可。
创建好了凭据就可以使用这个插件了。kubernetes cd插件既可以在普通类型的Jenkins Job中配置使用也支持在pipeline脚本中使用。
在普通类型比如自由风格类型、maven类型的Job中配置Job时使用”Add build step添加构建步骤”选项从列出的选项列表中选择”Deploy to kubernetes“。
比如我在一个自由风格类型的job中配置在kubernetes集群中创建一个namespace如下所示
说明:
kubeconfig这里应该选择上面创建的凭证即可。
Config Files为定义的kubernetes资源对象文件这里的路径默认为jenkins项目的${WORKSPACE}路径,如果有多个资源对象文件,要使用英文逗号”,“分开。需要注意的是,该文件的路径不支持绝对路径的写法,比如/root/ns.yaml但是支持基于workspace的相对路径比如job/ns.yaml此时的文件路径为${WORKSPACE}/job/ns.yaml更多该文件匹配的相关写法可参考这里
ns.yaml 是一个namespace类型的资源对象文件内容比较简单如下
apiVersion: v1
kind: Namespace
metadata:
name: test-deploy-plugin
执行结果这里就不贴图了,想看效果的可以自己去试一下。
在pipeline类型的job中使用cd插件就要编写pipeline脚本了开始之前还是要先了解一下该插件的语法 。
kubernetes-cd插件提供了kubernetesDeploy方法支持对pipeline脚本的编写。可以用kubernetesDeploy: Deploy to kubernetes片段生成器生成一个该方法的语法片段如下所示
美化一下语法。
kubernetesDeploy(kubeconfigId: 'k8s_auth', //必须项
configs: 'xx-deploy.yaml,xx-service.yaml,xx-ingress.yaml', // 必须项
enableConfigSubstitution: false,
secretNamespace: '<secret-namespace>',
secretName: '<secret-name>',
dockerCredentials: [
[credentialsId: '<credentials-id-for-docker-hub>'],
[credentialsId: 'auth_harbor', url: 'http://192.168.176.154'],
]
)
说明:
kubeconfigId存储在Jenkins中的kubeconfig的凭据ID必须。
configs相关的资源对象文件多个文件用逗号”,“隔开,必须。
enableConfigSubstitution在配置中启用变量替换非必须。
secretNamespace对docker私有仓库认证的secret所在的命名空间通常用于从私有仓库拉取镜像非必须。
secretName对docker私有仓库认证的secret名称该secret的内容包含了私有仓库的地址信息以及登录私有仓库的用户名和密码。
使用资源对象deployment部署pod的时候通常要从Docker私有仓库拉取应用服务的镜像对于需要认证的镜像仓库拉取应用镜像时有两种认证方式一种是部署pod的宿主机已经做了认证此时不需要对现有资源对象文件进行额外定义就能直接拉取镜像另一种是宿主机没有做认证就需要在资源对象pod中定义imagepullsecret参数这样拉取镜像时就会使用该secret指定的用户名和密码对仓库地址进行认证并拉取镜像但是有一点需要注意无论宿主机有没有做认证都需要在宿主机上的docker配置文件中使用insecure-registries参数指定私有仓库的地址。
如果该参数存在该名称将以KUBERNETES_SECRET_NAME作为环境变量名称显示在资源对象deployment中引用时就可以写该变量名如下所示代码
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: sample-k8s-deployment
spec:
replicas: 1
template:
metadata:
labels:
app: sample-k8s-app
spec:
containers:
- name: sample-k8s-app-container
image: <username or registry URL>/<image_name>:<tag(maybe $BUILD_NUMBER)>
ports:
- containerPort: 8080
imagePullSecrets:
- name: $KUBERNETES_SECRET_NAME
至于该secret如何创建将在下面实践小节演示。
dockerCredentialsdocker私有仓库认证的方法通常用于从私有仓库拉取镜像非必须。
有关使用kubernetes插件将代码持续部署到kubernetes集群的使用方法就介绍到这里下面根据前面章节针对这两种插件进行一个简单实践。
持续部署示例
使用kubernetesDeploy插件
在上一节持续交付的基础下添加新stage。
stage('deploy to k8s') {
sh """
cd /tmp/k8s_yaml/${project_name}/
sed -i 's#fw-base-nop:.*-*[0-9]\$#fw-base-nop:${imageTag}-${BUILD_NUMBER}#' deployment.yaml
cp * ${WORKSPACE}/
"""
kubernetesDeploy (
kubeconfigId:"k8s_auth",
configs: "*.yaml",
)
}
说明:
该流水线脚本使用shell命令修改deployment资源对象中image参数的值为新构建镜像的的镜像名称然后将所有的资源对象拷贝到workspace目录下然后执行流水线时会自动查找匹配的yaml文件。
上面是一个基础的示例旨在演示一下流程。实际工作中可以将这些资源对象文件放到源代码仓库指定的文件夹下比如放到项目根路径下kubernetes-deploy目录里上面pipeline脚本可以修改如下
stage('deploy to k8s') {
sh """
sed -i 's#fw-base-nop:.*-*[0-9]\$#fw-base-nop:${imageTag}-${BUILD_NUMBER}#' kubernetes-deploy/deployment-${project_name}.yaml
cp * ${WORKSPACE}/
"""
kubernetesDeploy (
kubeconfigId:"k8s_auth",
configs: "kubernetes-deploy/*.yaml",
)
}
需要注意的是:
kubernetes-cd插件默认会自动解析kubernetes资源对象文件如果遇到语法错误的设置会报错并退出。
configs: “*.yaml”这里既可以指定单个文件也可以指定多个文件默认目录为job的workspace目录。
资源对象文件定义需要注意的是deployment.yml 文件中如果做了滚动更新的配置目前只发现此处需要做改动maxSurge和maxUnavailable需要写成百分比的形式。
strategy:
rollingUpdate:
maxSurge: 100%
maxUnavailable: 0%
如果写成int类型的数字jenkins插件识别的过程中会报如下错误
在前面的章节有提到过异常处理在整个成熟的流水线脚本中默认会出现错误的步骤一般发生在代码编译过程所以还需要在此处加一个异常处理操作。对于脚本式语法依然使用try/catch/finally如下所示
try{
container('jnlp'){
stage('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
}
stage('Build a Maven project') {
ecoho "${project_name}"
sh "cd ${project_name} && mvn clean install -DskipTests -Pproduct -U"
currentBuild.result = 'SUCCESS'
}
}
}catch(e){
currentBuild.result = 'FAILURE'
}
说明
try指令包含的内容可以是任何一个或者多个stage无论包含哪个stagecatch指令都要紧跟try指令。
我这里演示的示例try指令包含的stage为拉取和编译代码的过程可以将try指令放到任何你想要捕获异常的步骤根据个人实际情况修改。
需要注意的是catch能捕获的异常通常是命令执行失败或者插件的某个方法配置不当这种类型的错误对于jenkins内部返回的异常比如没有某个属性或者某个指令不适用于该流水线脚本是不会捕获到的此时如果先前做了job执行状态判断即便job失败了发送邮件操作也不会执行。
完整的代码示例如下:
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
serviceAccount: 'default', containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:latest",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
containerTemplate(
name: 'sonar',
image: "192.168.176.155/library/jenkins-slave:sonar",
ttyEnabled: true,
privileged: true,
command: 'cat',
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
nfsVolume(mountPath: '/root/.m2', serverAddress: '192.168.177.43', serverPath: '/data/nfs/.m2', readOnly: false),
],
){
node('pre-CICD') {
stage('build') {
try{
container('jnlp'){
stage('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
}
stage('Build a Maven project') {
sh "cd ${project_name} && mvna clean install -DskipTests -Pproduct -U"
currentBuild.result = 'SUCCESS'
}
}
}catch(e){
currentBuild.result = 'FAILURE'
}
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
container('sonar'){
stage('sonar test'){
withSonarQubeEnv(credentialsId: 'sonarqube') {
sh "sonar-scanner -X "+
"-Dsonar.login=admin " +
"-Dsonar.language=java " +
"-Dsonar.projectKey=${JOB_NAME} " +
"-Dsonar.projectName=${JOB_NAME} " +
"-Dsonar.projectVersion=${BUILD_NUMBER} " +
"-Dsonar.sources=${WORKSPACE}/fw-base-nop " +
"-Dsonar.sourceEncoding=UTF-8 " +
"-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " +
"-Dsonar.password=admin "
}
}
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/base/${project_name}-${imageTag}:${BUILD_NUMBER}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/base/${project_name}:${imageTag}-${BUILD_NUMBER}"
}
}
}else {
echo "---currentBuild.result is:${currentBuild.result}"
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建异常",
body: """
详情:\n
failure: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] \n
状态:${env.JOB_NAME} jenkins 构建异常 \n
URL ${env.BUILD_URL} \n
项目名称 ${env.JOB_NAME} \n
项目构建id${env.BUILD_NUMBER} \n
信息: 代码编译失败
""",
to: "[email protected]",
recipientProviders: [[$class: 'DevelopersRecipientProvider']]
)
}
stage('deploy'){
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
sh """
cd /tmp/k8s_yaml/${project_group}/${project_name}
sed -i 's#fw-base-nop:.*-*[0-9]\$#fw-base-nop:${imageTag}-${BUILD_NUMBER}#' deployment.yaml
cp * ${WORKSPACE}/
"""
kubernetesDeploy (
kubeconfigId:"k8s_auth",
configs: "*.yaml",
)
}
}
}
}
}
imagePullSecrets
imagepullsecrets通过指定的secret名称用于执行创建资源对象操作后在拉取容器镜像时对docker私有仓库进行认证。
在kubernetes系列插件中只有在kubernetes-plugin插件配置PodTemplate方法时有提到过imagepullsecrets只不过此时的secret只是用于拉取提供pipeline脚本执行环境的容器镜像不能用于在kubernetes集群部署应用容器在拉取镜像时对私有仓库进行认证。
前面有提到过拉取应用镜像时有两种认证方式一种宿主机对私有仓库已经做了认证此时直接部署即可另一种是宿主机没有做认证就需要在资源对象pod中定义imagepullsecret参数这样拉取镜像时就会使用该secret指定的用户名和密码对仓库地址进行认证并拉取镜像。
那么这个secret是如何创建的呢比较简单使用一条命令即可。
kubectl create secret docker-registry <name> --docker-server=DOCKER_REGISTRY_SERVER --docker-username=DOCKER_USER --docker-password=DOCKER_PASSWORD --docker-email=DOCKER_EMAIL
其中:
DOCKER_REGISTRY_SERVERdocker私有仓库服务的地址。
DOCKER_USER向docker私有仓库认证的用户名。
DOCKER_PASSWORD向docker私有仓库认证的密码。
默认如果没有加-n参数该secret被创建在default命名空间所以如果要将pod部署在指定的namespace里同时需要在该namespace里创建secret。
创建好secret后编辑资源对象文件通常为deployment.yaml在template.spec下添加。
imagePullSecrets:
- name: <secret_name>
这样部署pod时会自动对私有仓库进行认证而不在关心宿主机是否对私有仓库进行了认证。只不过这样就需要在每一个要部署应用容器的namespace中创建imagepullsecret。实际工作中虽然可以这样做但是如果在项目组和namespace数量很大的的情况下需要在每一个namespace中创建该secret每次新建namepace都要创建一边该secret显然不是很好的解决方案。好在kubedeploy提供了更简单的方式来实现此功能就是使用kubernetesDeploy方法中的secret等参数下面详细介绍一下。
kubernetesDeploy方法的dockerCredentials参数用于对docker私有仓库认证如果secretName参数的值没有指定则生成一个唯一的secret名称并以KUBERNETES_SECRET_NAME作为环境变量名被资源对象引用如果设置了secretName则更新这个secret然后通过开启变量替换就可以被在secretNamespace指定的命名空间内创建的pod引用。
具体配置为:
kubernetesDeploy (
kubeconfigId:"k8s_auth",
configs: "*.yaml",
enableConfigSubstitution: true,
secretNamespace: 'base',
secretName: 'test-mysecret',
dockerCredentials: [
[credentialsId: 'auth_harbor', url: 'http://192.168.176.155'],
]
)
其中:
secretName最好不要与指定的namespace内存在的secret名称相同。
dockerCredentials 里credentialsId为创建的对私有仓库认证的凭据IDurl为私有仓库的url。
此时的资源对象文件imagepullsecret配置应该为
imagePullSecrets:
- name: $KUBERNETES_SECRET_NAME
配置好后kubernetesDeploy在解析资源对象文件的时候会自动对KUBERNETES_SECRET_NAME变量进行替换将在kubernetesDeploy方法中设定的secretName参数定义的值替换该变量并部署。这样对于任何namespace下的容器部署都不需要在单独的配置镜像仓库的认证只需要在配置kubernetesDeploy方法下secretNamespace参数的值即可而无需在每一个namespace下都创建secret。
完整的stage如下。
stage('deploy'){
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
sh """
cd /tmp/k8s_yaml/${project_group}/${project_name}
sed -i 's#fw-base-nop:.*-*[0-9]\$#fw-base-nop:${imageTag}-${BUILD_NUMBER}#' deployment.yaml
cp * ${WORKSPACE}/
"""
kubernetesDeploy (
kubeconfigId:"k8s_auth",
configs: "*.yaml",
enableConfigSubstitution: true,
secretNamespace: 'base',
secretName: 'test-mysecret',
dockerCredentials: [
[credentialsId: 'auth_harbor', url: 'http://192.168.176.155'],
]
)
}
}
执行时在jenkins console中可以看到该值进行了替换如下所示-
使用kubernetes cli插件
该插件如何使用上面已经做了介绍我这里的示例直接定义一个新的镜像配置根据自己实际情况配置使用前面创建的kubectl容器在持续部署步骤使用withKubeConfig方法用apply命令直接更新服务。
下面直接看一下pipeline脚本。
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
def project_group = 'base'
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
serviceAccount: 'default', containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:sonar",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
containerTemplate(
name: 'kubectl',
image: "192.168.176.155/library/alpine-kubectl:1.14",
ttyEnabled: true,
privileged: true,
command: 'cat',
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
nfsVolume(mountPath: '/root/.m2', serverAddress: '192.168.177.43', serverPath: '/data/nfs/.m2', readOnly: false),
],
){
node('pre-CICD') {
stage('build') {
try{
container('jnlp'){
stage('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
currentBuild.result == 'SUCCESS'
}
stage('Build a Maven project') {
sh "cd ${project_name} && mvn clean install -DskipTests -Pproduct -U"
currentBuild.result = 'SUCCESS'
}
}
}catch(e){
currentBuild.result = 'FAILURE'
}
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
container('jnlp'){
stage('sonar test'){
withSonarQubeEnv(credentialsId: 'sonarqube') {
sh "sonar-scanner -X "+
"-Dsonar.login=admin " +
"-Dsonar.language=java " +
"-Dsonar.projectKey=${JOB_NAME} " +
"-Dsonar.projectName=${JOB_NAME} " +
"-Dsonar.projectVersion=${BUILD_NUMBER} " +
"-Dsonar.sources=${WORKSPACE}/fw-base-nop " +
"-Dsonar.sourceEncoding=UTF-8 " +
"-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " +
"-Dsonar.password=admin "
}
}
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/library/${project_name}:${imageTag}-${BUILD_NUMBER}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/library/${project_name}:${imageTag}-${BUILD_NUMBER}"
}
}
}else {
echo "---currentBuild.result is:${currentBuild.result}"
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建异常",
body: """
详情:<br>
failure: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] <br>
状态:${env.JOB_NAME} jenkins 构建异常 <br>
URL ${env.BUILD_URL} <br>
项目名称 ${env.JOB_NAME} <br>
项目构建id${env.BUILD_NUMBER} <br>
信息: 代码编译失败
""",
to: "[email protected]",
recipientProviders: [[$class: 'DevelopersRecipientProvider']]
)
}
stage('deploy'){
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
container('kubectl'){
withKubeConfig(credentialsId: 'afffefbf-9216-41dc-b803-7791cdb87134', serverUrl:'https://192.168.176.156:6443') {
sh """
cd /tmp/k8s_yaml/${project_group}/${project_name}
sed -i 's#fw-base-nop:.*-*[0-9]\$#${project_name}:${imageTag}-${BUILD_NUMBER}#' deployment.yaml
cp * ${WORKSPACE}/
kubectl apply -f .
"""
}
}
}
}
}
}
}
执行结果如下:
上面的脚本依然可以放到应用代码的源码库中通过配置Pipeline Script from SCM的方式从代码仓库拉取Jenkinsfile并执行如何配置在前面的章节有过介绍就不在重复说明。根据实际情况选择部署方式即可。
在文章的最后给出一个deployment资源对象的定义做一个参考。
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: fw-base-nop
namespace: base
spec:
replicas: 1
strategy:
rollingUpdate:
maxSurge: 100%
maxUnavailable: 0%
template:
metadata:
labels:
name: fw-base-nop
spec:
terminationGracePeriodSeconds: 60
#imagePullSecrets:
#- name: $KUBERNETES_SECRET_NAME
containers:
- name: fw-base-nop
image: 192.168.176.155/base/fw-base-nop:bc29b5e-37
livenessProbe:
httpGet:
path: /monitor/info
port: 18082
scheme: HTTP
initialDelaySeconds: 300
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
readinessProbe:
httpGet:
path: /monitor/info
port: 18082
scheme: HTTP
initialDelaySeconds: 300
timeoutSeconds: 10
successThreshold: 1
failureThreshold: 5
env:
- name: TZ
value: "Asia/Shanghai"
- name: JAVA_OPTS
value: "-Xms1228m -Xmx1228m"
- name: _JAVA_OPTIONS
value: "-Xms1228m -Xmx1228m"
ports:
- containerPort: 8082
resources:
limits:
cpu: 1024m
memory: 2048Mi
requests:
cpu: 1024m
memory: 2048Mi
volumeMounts:
- name: fw-base-nop-logs
mountPath: /data/logs/fw-base-nop
- name: host-resolv
mountPath: /etc/resolv.conf
- name: tz-config
mountPath: /etc/localtime
volumes:
- name: host-resolv
hostPath:
path: /etc/resolv.conf
- name: tz-config
hostPath:
path: /etc/localtime
- name: fw-base-nop-logs
emptyDir: {}
有关使用kubernetes插件持续部署的内容到此结束本节内容只是给大家提供了一个选择的方案。在实际工作中还需要做好调研在考虑要如何去实现持续部署操作。

View File

@@ -0,0 +1,886 @@
因收到Google相关通知网站将会择期关闭。相关通知内容
18.Ansible Plugin插件语法详解与持续部署服务到kubernetes集群
在实际工作中使用容器编排系统对容器编排已经成为使用容器技术的常态。而在前面的Ansible章节中仅仅介绍了使用Ansible部署服务到docker容器中对于容器的编排系统没有说明并且只是介绍了在Jenkins UI中使用bash shell命令和插件配置来使用Ansible。基于此本节就来介绍一下如何在Pipeline中使用Ansible并通过示例来进行演示。
本节主要介绍两方面的内容:
在pipeline脚本中使用ansible插件的语法介绍以及使用示例-
将构建好的镜像持续部署到容器编排系统kubernetes中。
为什么要将本章节放到课程的最后,是因为该文主要讲的是持续部署。对于持续交付操作,在前面的章节中都已经介绍过,所以将前面的持续交付操作直接拿来用就可以了。
Ansible plugin 语法详解
在Pipeline类型的job中可以通过片段生成器生成使用ansible的语法片段也可以直接在pipeline中执行shell命令。下面主要介绍一下如何使用ansible语法片段。
进入流水线语法界面点击”片段生成器菜单“跳转的页面中steps下拉框的第一个就是我们要使用的片段生成器参数:ansiblePlaybook Invoke and ansible playbook。
如下所示
其中:
Ansible tool该选项参数是一个下拉框用来选择ansible的命令列表我这里只配置了ansible和ansible-playbook命令需要注意的是该选项下拉框中的工具列表需要事先在Jenkins的”全局工具配置“菜单中定义ansible命令的名称。
Playbook file path in workspace该参数用来指定要执行的playbook文件该文件默认从当前的工作空间$WORKSPACE路径查找。需要注意的是该参数只能指定具体的文件名称不能通过正则匹配也不能指定多个文件。
Inventory file path in workspace该参数用来指定主机清单文件默认的路径同样是当前工作空间目录。如果该参数指定的是一个目录则会使用该目录下所有文件中定义的主机。
SSH connection credentials用来指定连接服务器的凭证与在前面章节publish over ssh插件用到的凭证一样。如果执行ansible-playbook命令的机器已经做好了服务器免密认证此处可以不用填写并且如果要使用该参数时要确保ansible-playbook命令所在主机存在sshpass命令。
Vault credentials对于使用了ansible valut命令加密过后的playbook文件可以添加该参数用于指定一个凭证在执行playbook时自动解密。
Use become用于决定是否启用become username。
Become username与Use become参数一起使用指定运行任务的用户 。
Host subsethost的子集如果在playbook中定义的inventoryhosts清单文件是一个组并且该组下有多个主机ip或域名通过该参数可以设置在该组中指定的主机上执行playbook任务如果有多个主机则每个主机之间需要用逗号”,“)隔开。
Tagsplaybook中任务的标签。如果设置了该参数则只会执行playbook中与该参数值匹配的任务。
Tags to skip跳过要执行的标签。如果设置了该参数则不会执行playbook中与该参数值匹配的任务。
Task to start at执行playbook时用于从指定的任务开始执行该参数的值用于匹配playbook中定义的任务的名称。
Number of parallel processes to use对应ansible中的-f参数指定并发线程数。
Disable the host SSH key check使用ssh连接主机时对于首次连接的主机ssh会检测本地known_hosts文件中是否存在该主机的host记录如果不存在则需要手动确认对于在控制台通过ssh命令连接可以通过手工确认而对于使用ansible命令时则需要在ansible.cfg配置文件中设置host_key_checking = False在jenkins中可以通过勾选此参数框来关闭检查。
Colorized output在控制台中是否切换彩色编码并无多大用处。
Extra parameters用于指定要使用的参数变量需要加上”-e”参数。
介绍完ansible插件的基本参数以后下面来看一下该插件的使用方法。
通过上面的片段生成器生成的语法示例如下:
ansiblePlaybook(
become: true,
colorized: true,
credentialsId: '',
disableHostKeyChecking: true,
extras: '..',
installation: 'ansible-playbook',
inventory: '',
limit: '',
playbook: '',
skippedTags: '',
startAtTask: '',
sudo: true,
tags: '',
vaultCredentialsId: ''
)
该语法块在声明式语法和脚本式语法中使用的方式相同。实际工作中可能不会用到所有的参数,根据自己需求选择参数即可。
下面以一个简单的示例演示一下例如我要执行test.yaml下的任务只限制在192.168.176.149和192.168.176.148主机上执行。
代码如下:
pipeline{
agent { node { label 'master' } }
stages{
stage('Test'){
steps{
sh "cp -R /data/nfs/ansible $WORKSPACE/"
ansiblePlaybook (
become: true,
disableHostKeyChecking: true,
limit: '192.168.176.149,192.168.176.148',
installation: 'ansible-playbook',
inventory: './ansible/hosts',
playbook: './ansible/test.yaml',
extras: ''
)
}
}
}
}
说明:
cp -R /data/nfs/ansible $WORKSPACE/为什么要执行该命令因为在使用docker pipeline插件或者kubernetes插件的时候对于动态生成的容器默认是没有这些文件以及目录的这时候就需要使用挂载共享存储的方式将所需的目录挂载到容器内。这里模拟的就是在容器内执行playbook当然如果将使用的文件放到了源码仓库中可以忽略这一步。
limit 参数就是用来限制执行playbook任务的节点该示例表示在192.168.176.149,192.168.176.148连个节点上执行任务。
installation 代表使用ansible-playbook命令。
inventory和playbook 分别使用相对路径指定主机清单文件和playbook文件。
hosts和test.yaml文件内容分别如下
$ cat hosts
[k8s_master]
192.168.176.148
192.168.176.149
192.168.176.150
$ cat test.yaml
---
- hosts: k8s_master
gather_facts: no
tasks:
- name: test
shell: hostname
register: host_name
- name: debug
debug:
msg: "{{ host_name.stdout }}"
执行结果如下:
从执行结果可以看到在指定的主机上执行了命令。
通过上面的示例对于如何使用ansible插件应该有了初步的了解下面介绍一下将该插件的语法块集成到前面章节的pipeline的脚本中去使用。
使用Ansible Plugin
根据前面章节的示例汇总针对不同类型agent代理对于该插件的使用有如下情况-
agent代理为虚拟机。-
agent代理为容器。-
agent代理通过kubernetes编排。
当agent为虚拟机时使用ansible插件
当agent代理为虚拟机时无论是master节点还是node节点都需要保证节点上已经安装好ansible工具。
在最初的在pipeline实践章节中已经完成了代码的编译以及镜像的构建和推送到私有仓库操作我们只需要在此基础上对在”ansible持续交付与部署“章节”部署服务到容器“小节使用的playbook脚本稍作修改只保留对容器和镜像的相关操作即可。
修改完的playbook脚本如下
$ cat deploy.yaml
- hosts: "{{ target }}"
remote_user: root
gather_facts: False
vars:
dest_dict: "/data/{{ project_name }}/"
tasks:
- name: 判断目录是否存在
shell: ls {{ dest_dict }}
register: dict_exist
ignore_errors: true
- name: 创建相关目录
file: dest="{{ item }}" state=directory mode=755
with_items:
- "{{ dest_dict }}"
- /data/logs/{{ project_name }}
when: dict_exist is failure
- name: 查看容器是否存在
shell: "docker ps -a --filter name={{ project_name }} |grep -v COMMAND"
ignore_errors: true
register: container_exists
- name: 查看容器状态
shell: "docker ps -a --filter name={{ project_name }} --format '{{ '{{' }} .Status {{ '}}' }}'"
ignore_errors: true
register: container_state
when: container_exists.rc == 0
- name: 关闭容器
shell: "docker stop {{ project_name }}"
when: "('Up' in container_state.stdout)"
ignore_errors: true
- name: 删除容器
shell: "docker rm {{ project_name }}"
when: container_exists.rc == 0
ignore_errors: true
- name: 查看镜像是否存在
command: "docker images --filter reference={{ registry_url }}/{{ project_group }}/{{ project_name }}* --format '{{ '{{' }} .Repository {{ '}}' }}:{{ '{{' }}.Tag {{ '}}' }}'"
register: image_exists
ignore_errors: true
- name: 删除镜像
shell: "docker rmi -f {{ item }}"
loop: "{{ image_exists.stdout_lines }}"
ignore_errors: true
when: image_exists.rc == 0
- name: 启动容器
shell: 'docker run {{ run_option }} --name {{ project_name }} {{ image_name }}'
主要修改了”查看镜像是否存在“任务获取镜像列表的命令添加了两个匹配镜像url的变量此处需要根据你的实际情况修改在执行playbook的时候需要添加这两个变量。
对于执行playbook时用到的变量我们直接在pipeline中定义即可。
首先看一下在pipeline中使用ansible-playbook命令部署。
stage('部署镜像'){
environment {
container_run_option="--network host -e TZ='Asia/Shanghai' -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/${project_name}:/data/logs/${project_name}"
}
steps{
sh """
cp -R /data/nfs/ansible $WORKSPACE/
echo $container_run_option
ansible-playbook -i $WORKSPACE/ansible/hosts $WORKSPACE/ansible/deploy.yaml -e "workspace='${WORKSPACE}' registry_url=$registry_url project_group=$project_group target='192.168.176.160' project_name='$project_name' run_option='$container_run_option' image_name='${registry_url}/${project_group}/${project_name}:${BUILD_ID}'"
"""
}
}
注意我这里执行ansible-playbook命令的主机对目标主机默认做好了免密认证。如果对ansible主机对目标主机没有做免密认证可以通过添加credentialsId参数指定jenkins凭证的方式向远程服务器认证。
既然介绍了ansible的插件的语法为什么还要使用ansible-playbook命令呢是为了测试playbook脚本是否能够正常执行传入的变量是否有效以及整个流水线是否能够执行成功等。毕竟在执行playbook的时候传入了许多变量如果直接使用ansible插件可能会遇到意想不到的坑有时候一个不起眼的双引号或者单引号都能导致playbook执行失败。
如果换成使用ansible插件pipeline语法快如下
stage('部署镜像'){
environment {
container_run_option="\'--network host -e TZ=Asia/Shanghai -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/${project_name}:/data/logs/${project_name}\' "
}
steps{
sh """
cp -R /data/nfs/ansible $WORKSPACE/
echo $container_run_option
"""
ansiblePlaybook (
become: true,
disableHostKeyChecking: true,
installation: 'ansible-playbook',
inventory: './ansible/hosts',
playbook: './ansible/deploy.yaml',
extras: ' -e "workspace=${WORKSPACE} target="192.168.176.160" registry_url=$registry_url project_group=$project_group project_name=$project_name run_option=$container_run_option image_name=${registry_url}/${project_group}/${project_name}:${BUILD_ID}"'
)
}
}
使用ansible插件的时候要注意变量的引用。变量通过extras参数传入使用该参数时请务必加上”-e”选项。对于docker运行时的参数需要使用单引号(” “)引起来,并添加转义(可不做),防止在转化成实际变量值时出错。
比如对于上面的container_run_option运行参数如果不加单引号不做转义实际传递到extras参数的时候是这样的。
run_option= --network host -e TZ=Asia/Shanghai -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/fw-base-nop:/data/logs/fw-base-nop
传入到playbook中对应的docker启动命令如下
docker run --network --name fw-base-nop 192.168.176.155/base/fw-base-nop:289
此时就会导致容器启动失败。
而使用了引号与转义后是这样的。
run_option='--network host -e TZ=Asia/Shanghai -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/fw-base-nop:/data/logs/fw-base-nop'
这才是正确的docker启动时的运行参数。
该示例只是展示了使用ansible部署容器相关的操作对于代码的编译和镜像的构建还是通过pipeline语法实现的如果你不想要将这部分内容使用pipeline脚本实现也可以将这部分操作通过playbook脚本实现如何编写这里就不在细说了。
当agent为容器时使用ansible插件
当agent为容器时分为两种情况经过前几章的学习你应该能够猜到了没错这两种情况分别是使用docker pipeline插件在虚拟机上生成容器和使用kubernetes 插件编排容器。使用docker pipeline插件时可以选择在容器内使用ansible插件也可以选择在虚拟机上使用ansible插件在使用kubernetes插件时就只能在容器内使用ansible插件了无论哪种情况都要保证在agent代理上有ansible-playbook命令。
在虚拟机上使用ansible插件在上一小节已经做过介绍下面主要介绍一下如何在容器中使用该插件。
想要在容器中使用ansible插件需要使用一个单独的ansible镜像而不能通过在容器中挂载可执行文件的方式使用。这与使用docker可执行文件的方式相似可执行文件的使用均依赖于当前的系统环境。并且ansible运行时还依赖python环境以及需要用到的各种模块单独拷贝二进制文件到镜像的方式同样也不适用与ansible。好在docker hub上已经有了各种版本的ansible镜像我们只需要直接拿过来用即可。
首先使用镜像做一个简单的测试还是使用上一小节的示例只不过此次使用容器作为agent代理完成playbook的执行。
代码如下所示:
pipeline {
agent {
docker{
image 'gableroux/ansible:2.7.10'
args '-v /data/:/data/ -v /root/.ssh:/root/.ssh'
}
}
stages('build') {
stage('test-ansilbe'){
steps{
sh "ansible-playbook -i /data/nfs/ansible/hosts /data/nfs/ansible/test.yaml "
}
}
}
}
该示例会自动在某一个节点上启动容器。
这里需要注意的是:
1、 在容器内执行ansible-playbook任务时需要连接到远程主机此时的ansible主机容器是没有对远程服务器经过免密认证的除了通过在主机清单文件hosts中添加主机参数以外也可以通过挂载宿主机ssh目录的方式我这里使用的是root用户这样在容器中连接远程服务器时不需要单独认证即可完成连接。不过此种方式需要确保宿主机可以通过免秘钥认证连接目标服务器。
2、 除此之外在使用ansible插件的ansiblePlaybook方法时也可以通过”credentialsId“参数指定jenkins凭据来向远程服务器认证。此时有两个问题需要我们考虑到
如果不使用挂载.ssh目录的方式或者通过在清单文件中添加ansible用户和密码参数的方式那么还需要配置ansible的配置文件ansible.cfg因为每启动一个动态容器都相当于一台新的机器去连接远程主机时会检查本地known_hosts文件有没有远程主机的fingerprint key串如果没有需要手动输入yes确认而使用ansible执行任务时默认是不能手动输入的所以还需要在ansible配置文件中配置host_key_checking = False来解决此问题当然你也可以在使用ansible插件时通过disableHostKeyChecking参数来解决此问题。
如果不使用.ssh目录的方式使用credentialsId参数时ansiblePlaybook方法连接远程主机时使用的sshpass命令对远程主机进行认证通过jenkins执行任务时的控制台日志可以看到默认使用的镜像是没有该命令的所以我们还需要安装此命令。
自定义ansible镜像
在上面使用的ansible镜像的基础上在不使用ansible 插件的情况下定制的Dockerfile内容如下
FROM gableroux/ansible:2.7.10
RUN echo "http://mirrors.aliyun.com/alpine/latest-stable/main/" > /etc/apk/repositories \
&& echo "http://mirrors.aliyun.com/alpine/latest-stable/community/" >> /etc/apk/repositories \
&& apk add --update --no-cache openssh sshpass \
&& echo "[defaults] \n" > ~/.ansible.cfg \
&& echo "host_key_checking = False" >> ~/.ansible.cfg
注意:
如果不想使用ansible.cfg配置文件来定义host_key_checking = False也可以通过在执行ansible时将该配置通过变量的方式传入例如
ansible-playbook xxx.yaml -e "host_key_checking = False"
构建镜像。
docker build -t 192.168.176.155/library/ansible:v2.7.10 .
如果想要在指定的节点上执行,代码如下:
pipeline {
agent { node { label 'slave-43' } }
stages('build') {
stage('test-ansilbe'){
steps{
script{
sh "hostname"
docker.image('gableroux/ansible:2.7.10').inside('-v /data/:/data/ -v /root/.ssh:/root/.ssh '){
sh "ansible-playbook -i /data/nfs/ansible/hosts /data/nfs/ansible/test.yaml "
}
}
}
}
}
}
该示例会在slave-43节点上启动容器执行playbook。
基础的演示完成了,放到实际脚本中去应该简单多了吧。
方法比较多下面给出一种例如使用多个agent示例。
pipeline {
agent { node { label 'master' } }
environment {
project_name = 'fw-base-nop'
jar_name = 'fw-base-nop.jar'
registry_url = '192.168.176.155'
project_group = 'base'
}
stages('build') {
stage('代码拉取并编译'){
steps {
script {
docker.image('alpine-cicd:latest').inside('-v /root/.m2:/root/.m2'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, extensions: [], submoduleCfg: [], userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
echo "开始编译 "
sh "cd $project_name && mvn clean install -DskipTests -Denv=beta"
}
}
}
}
stage('构建镜像'){
steps {
script{
jar_file=sh(returnStdout: true, script: "find ${WORKSPACE} ./ -name $jar_name |head -1").trim()
docker.image('alpine-cicd:latest').inside('-v /root/.m2:/root/.m2 -v /data/:/data/'){
sh "cp $jar_file /data/$project_group/$project_name/"
}
}
}
}
stage('上传镜像'){
steps {
script {
docker.withRegistry('http://192.168.176.155', 'auth_harbor') {
docker.image('alpine-cicd:latest').inside('-v /data/:/data/ -v /var/run/docker.sock:/var/run/docker.sock'){
def customImage=docker.build("${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}","/data/${project_group}/${project_name}/")
customImage.push()
}
}
sh "docker rmi -f ${registry_url}/${project_group}/${project_name}:${env.BUILD_ID}"
}
}
}
stage('部署镜像'){
environment{
container_run_option="--network host -e TZ='Asia/Shanghai' -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/${project_name}:/data/logs/${project_name}"
}
steps{
script{
docker.image('gableroux/ansible:2.7.10').inside('-v /data/:/data/ -v /root/.ssh:/root/.ssh'){
sh """
ansible-playbook -i /data/nfs/ansible/hosts /data/nfs/ansible/deploy.yaml -e "workspace='${WORKSPACE}' registry_url=$registry_url project_group=$project_group target_host='192.168.176.160' project_name='$project_name' run_option='$container_run_option' image_name='${registry_url}/${project_group}/${project_name}:${BUILD_ID}'"
"""
}
}
}
}
}
}
如果你想要定义一个全局agent代理需要你使用的镜像中包含ansible命令这就需要你使用上面自定义的镜像了。
使用ansible插件的方式与上一小节中的一样将ansible-playbook命令换成ansiblePlaybook()即可,代码如下所示:
stage('部署镜像'){
environment {
container_run_option="\'--network host -e TZ=Asia/Shanghai -d -v /etc/localtime:/etc/localtime:ro -m 2G --cpu-shares 512 -v /data/logs/${project_name}:/data/logs/${project_name}\'"
}
steps{
script{
docker.image('gableroux/ansible:2.7.10').inside('-v /data/:/data/ -v /root/.ssh:/root/.ssh'){
ansiblePlaybook (
become: true,
disableHostKeyChecking: true,
installation: '/usr/local/bin/ansible-playbook',
inventory: '/data/nfs/ansible/hosts',
playbook: '/data/nfs/ansible/deploy.yaml',
extras: ' -e "workspace=${WORKSPACE} target="192.168.176.160" registry_url=$registry_url project_group=$project_group project_name=$project_name run_option=$container_run_option image_name=${registry_url}/${project_group}/${project_name}:${BUILD_ID}"'
)
}
}
}
}
注意:
在使用ansible容器作为agent节点时一定要注意ansible-playbook命令的位置。比如我在jenkins系统中配置的命令路径为/usr/bin/ansible-playbook那么通过片段生成器生成的语法片段中installation参数默认指定的ansible-playbook命令的路径为/usr/bin而我使用的ansible镜像gableroux/ansible:2.7.10ansible-playbook命令的默认路径为/usr/local/bin/ansible-playbook这时候如果installation参数的值还是ansibl-playbook在运行容器执行ansible-playbook命令时就会提示命令找不到。所以这里应该设置成/usr/local/bin/ansible-playbook。
如果不想使用宿主机下的/root/.ssh文件可以通过添加credentialsId参数用于指定登录远程服务器的jenkins凭证如下所示
docker.image('192.168.176.155/library/ansible:v2.7.10').inside('-v /data/:/data/'){
ansiblePlaybook (
become: true,
disableHostKeyChecking: true,
installation: '/usr/local/bin/ansible-playbook',
inventory: '/data/nfs/ansible/hosts',
credentialsId: '160-ssh',
playbook: '/data/nfs/ansible/deploy.yaml',
extras: ' -e "workspace=${WORKSPACE} target_host="192.168.176.160" registry_url=$registry_url project_group=$project_group project_name=$project_name run_option=$container_run_option image_name=${registry_url}/${project_group}/${project_name}:35"'
)
}
有关使用容器作为agent代理的内容就介绍到这里。
当agent代理通过kubernetes编排
当agent代理使用kubernetes系统编排时具体的部署流程与上面使用docker容器作为agent代理流程是一样的。只不过既然使用kubernetes系统那么应用容器的部署肯定也不是只单独部署了需要将容器部署到kubernetes集群中。所以我们需要从新编写ansible-playbook脚本。
我们只需要将在上一节中用的k8s的资源对象文件deployment.ymal拷贝到k8s master主机上指定的目录下apply即可。
同前两节一样在开始之前还是使用ansible-playbook命令测试一下流程是成功
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
def project_group = 'base'
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
serviceAccount: 'default', containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:sonar",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
containerTemplate(
name: 'ansible',
image: "192.168.176.155/library/ansible:v2.7.10",
ttyEnabled: true,
privileged: true,
command: 'cat',
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
nfsVolume(mountPath: '/root/.m2', serverAddress: '192.168.177.43', serverPath: '/data/nfs/.m2', readOnly: false),
],
){
node('pre-CICD') {
stage('deploy'){
container('ansible'){
sh """
ansible-playbook -i /tmp/ansible/hosts /tmp/ansible/deploy-kubernetes.yaml -e "project_group=$project_group project_name=$project_name"
"""
}
}
}
}
说明:
在stage(deploy)阶段简单测试一下使用ansible-playbook命令实现部署到kubernetes集群。其中deploy-kubernetes.yaml内容如下
---
- hosts: k8s_master
gather_facts: False
tasks:
- name: copy deployment
copy: src=/tmp/k8s_yaml/{{ project_group }}/{{ project_name }}/deployment.yaml dest=/data/{{ project_group }}/{{ project_name }}/
- name: exec
shell: kubectl apply -f /data/{{ project_group }}/{{ project_name }}/deployment.yaml
执行结果如下:
使用ansible命令可以完成部署那么证明playbook文件和ansible-playbook命令都是没问题的下面使用ansible插件测试一下。
因为是使用的脚本式语法pipeline代码如下
stage('deploy'){
container('ansible'){
ansiblePlaybook (
become: true,
disableHostKeyChecking: true,
installation: '/usr/local/bin/ansible-playbook',
inventory: '/tmp/ansible/hosts',
credentialsId: '160-ssh',
limit: '192.168.176.148',
playbook: '/tmp/ansible/deploy-kubernetes.yaml',
extras: " -e 'project_group=$project_group project_name=$project_name'"
)
}
}
基本上与在docker中的配置一样。
完整的代码如下:
def project_name = 'fw-base-nop' //项目名称
def registry_url = '192.168.176.155' //镜像仓库地址
def project_group = 'base'
podTemplate(cloud: 'kubernetes',namespace: 'default', label: 'pre-CICD',
serviceAccount: 'default', containers: [
containerTemplate(
name: 'jnlp',
image: "192.168.176.155/library/jenkins-slave:sonar",
args: '${computer.jnlpmac} ${computer.name}',
ttyEnabled: true,
privileged: true,
alwaysPullImage: false,
),
containerTemplate(
name: 'ansible',
image: "192.168.176.155/library/ansible:v2.7.10",
ttyEnabled: true,
privileged: true,
command: 'cat',
alwaysPullImage: false,
),
],
volumes: [
nfsVolume(mountPath: '/tmp', serverAddress: '192.168.177.43', serverPath: '/data/nfs', readOnly: false),
hostPathVolume(mountPath: '/var/run/docker.sock', hostPath: '/var/run/docker.sock'),
nfsVolume(mountPath: '/root/.m2', serverAddress: '192.168.177.43', serverPath: '/data/nfs/.m2', readOnly: false),
],
){
node('pre-CICD') {
stage('build') {
try{
container('jnlp'){
stage('clone code'){
checkout([$class: 'GitSCM', branches: [[name: '*/master']], doGenerateSubmoduleConfigurations: false, userRemoteConfigs: [[credentialsId: 'c33d60bd-67c6-4182-b52c-d7aeebfab772', url: 'http://192.168.176.154/root/base-nop.git']]])
script {
imageTag = sh(returnStdout: true, script: 'git rev-parse --short HEAD').trim()
}
echo "${imageTag}"
currentBuild.result == 'SUCCESS'
}
stage('Build a Maven project') {
sh "cd ${project_name} && mvn clean install -DskipTests -Pproduct -U"
currentBuild.result = 'SUCCESS'
}
}
}catch(e){
currentBuild.result = 'FAILURE'
}
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
container('jnlp'){
stage('sonar test'){
withSonarQubeEnv(credentialsId: 'sonarqube') {
sh "sonar-scanner -X "+
"-Dsonar.login=admin " +
"-Dsonar.language=java " +
"-Dsonar.projectKey=${JOB_NAME} " +
"-Dsonar.projectName=${JOB_NAME} " +
"-Dsonar.projectVersion=${BUILD_NUMBER} " +
"-Dsonar.sources=${WORKSPACE}/fw-base-nop " +
"-Dsonar.sourceEncoding=UTF-8 " +
"-Dsonar.java.binaries=${WORKSPACE}/fw-base-nop/target/classes " +
"-Dsonar.password=admin "
}
}
}
withDockerRegistry(credentialsId: 'auth_harbor', url: 'http://192.168.176.155') {
stage('build and push docker image') {
sh "cp /tmp/Dockerfile ${project_name}/target/"
def customImage = docker.build("${registry_url}/library/${project_name}:${imageTag}-${BUILD_NUMBER}","--build-arg jar_name=${project_name}.jar ${project_name}/target/")
echo "推送镜像"
customImage.push()
}
stage('delete image') {
echo "删除本地镜像"
sh "docker rmi -f ${registry_url}/library/${project_name}:${imageTag}-${BUILD_NUMBER}"
}
}
}else {
echo "---currentBuild.result is:${currentBuild.result}"
emailext (
subject: "'${env.JOB_NAME} [${env.BUILD_NUMBER}]' 构建异常",
body: """
详情:<br>
failure: Job ${env.JOB_NAME} [${env.BUILD_NUMBER}] <br>
状态:${env.JOB_NAME} jenkins 构建异常 <br>
URL ${env.BUILD_URL} <br>
项目名称 ${env.JOB_NAME} <br>
项目构建id${env.BUILD_NUMBER} <br>
信息: 代码编译失败
""",
to: "[email protected]",
recipientProviders: [[$class: 'DevelopersRecipientProvider']]
)
}
stage('deploy'){
if (currentBuild.result == null || currentBuild.result == 'SUCCESS') {
sh 'sed -i 's#fw-base-nop:.*-*[0-9]\$#${project_name}:${imageTag}-${BUILD_NUMBER}#' /tmp/${project_group}/${project_name}/deployment.yaml'
container('ansible'){
ansiblePlaybook (
become: true,
disableHostKeyChecking: true,
installation: '/usr/local/bin/ansible-playbook',
inventory: '/tmp/ansible/hosts',
credentialsId: '160-ssh',
limit: '192.168.176.148',
playbook: '/tmp/ansible/deploy-kubernetes.yaml',
extras: " -e 'project_group=$project_group project_name=$project_name'"
)
}
}
}
}
}
}
说明:
有一点需要注意的是对于修改资源对象文件deployment.yaml中image参数的值可以通过sed命令直接修改共享存储中的源文件该参数的值前提是在共享存储挂载到pod时权限可写也可以通过在ansible playbook脚本文件deployment-kubernetes.yaml中添加任务去修改比如使用lineinfile模块这里就不在演示了。
这样便完成了使用ansible插件去部署容器到kubernetes集群的操作。而在实际工作中由于部署的复杂性和多样性我们在更新应用的时候可能不只要更新服务我们还需要更新应用的所使用的资源大小应用的端口日志路径等任何可能修改信息此时只是简单的使用单个playbook文件已经无法满足要求了这时我们在之前介绍到的ansible 相关的知识就派上用场了。
使用ansible-playbook最常用的的场景就是使用role定义一系列角色去完成设定的任务。在使用role编写playbook部署容器到kubernetes集群时根据需求的不同对于所需的kubernetes资源对象的定义也会有所差异。我这里就简单的介绍一种基本部署的流程。
通过编写一个playbook文件作为整个部署脚本的执行入口如下所示
$ cat dev-common.yml
- hosts: DEV-TEST-k8s
remote_user: root
gather_facts: False
roles:
- dev-k8s
通过上面的入口文件然后看一下该roles目录都包含哪些role以及playbook脚本文件。
tree roles/
roles/
└── dev-k8s
├── tasks
│ └── main.yml
└── templates
├── deployments.yaml.j2
roles目录包含一个dev-k8 role
role下的tasks目录用于存放执行任务的playbook。-
templates目录用于存放kubernetes资源对象的模板文件。-
资源对象文件只配置了一个做示例实际工作中可能会涉及到更多比如configmapingressservice等本例仅作参考。
然后看一下这个tasks下playbook的内容命令都比较简单并且定义了一系列变量这些变量都可以在pipeline脚本中定义。
$ cat tasks/main.yml
---
- name: 判断目录是否存在
shell: ls {{ yaml_dir }}/{{ns}}/dev-{{ server_name }}
register: dict_exist
ignore_errors: true
- name: Create the k8s-config directory
file: path="{{ yaml_dir }}/{{ns}}/dev-{{ server_name }}" state=directory mode=0755
when: dict_exist is failure
- name: Create project file
template: "src={{ item }}.j2 dest={{ yaml_dir }}/{{ns}}/dev-{{ server_name }}/dev-{{server_name}}-{{ item }}"
with_items:
- deployments.yaml
- name: 部署
shell: "cd {{ yaml_dir }}/{{ns}}/dev-{{ server_name }} && {{bin_dir}}/kubectl apply -f . --record"
说明:
变量yaml_dir和bin_dir分别用于存放kubernetes资源对象文件和kubectl命令这两个变量放到了inventory清单文件中没有通过外部变量传入根据自己实际情况修改。
其他变量参考下面template模板文件。
然后看一下template下的资源对象模板文件。
$ cat deployments.yaml.j2
---
apiVersion: extensions/v1beta1
kind: Deployment
metadata:
name: dev-{{server_name}}
namespace: {{ns}}
spec:
replicas: 2
strategy:
rollingUpdate:
maxSurge: 1
maxUnavailable: 1
revisionHistoryLimit: 10
template:
metadata:
labels:
name: dev-{{server_name}}
spec:
hostNetwork: true
terminationGracePeriodSeconds: 60
imagePullSecrets:
- name: mysecret
containers:
- name: dev-{{server_name}}
image: 192.168.176.155/base/{{server_name}}:{{ tag }}
imagePullPolicy: Always
lifecycle:
preStop:
exec:
command: ["rm","-r","-f","/data/logs/{{server_name}}/"]
env:
- name: TZ
value: "Asia/Shanghai"
{% if java_opts %}
- name: JAVA_OPTS
value: {{java_opts}}
- name: _JAVA_OPTIONS
value: {{java_opts}}
{% else %}
- name: JAVA_OPTS
value: "-Xms1024m -Xmx1024m"
- name: _JAVA_OPTIONS
value: "-Xms1024m -Xmx1024m"
{% endif %}
ports:
- containerPort: {{port}}
resources:
limits:
cpu: {{ cpu|int }}m
memory: {{ mem|int }}Mi
requests:
cpu: {{ cpu|int }}m
memory: {{ mem|int }}Mi
volumeMounts:
{% if server_name == "test-enter" %}
- name: dev-{{server_name}}-logs
mountPath: /data/logs/test-enter
{% else %}
- name: dev-{{server_name}}-logs
mountPath: /data/logs/{{server_name}}
{% endif %}
- name: host-resolv
mountPath: /etc/resolv.conf
- name: tz-config
mountPath: /etc/localtime
volumes:
- name: host-resolv
hostPath:
path: /etc/resolv.conf
- name: tz-config
hostPath:
path: /usr/share/zoneinfo/Asia/Shanghai
- name: dev-{{server_name}}-logs
emptyDir: {}
说明:
该资源对象填充了一些变量,其中:-
server_name表示项目名称对应持续交付步骤时的${project_name}。-
ns表示该资源对象部署到的namespace对应持续交付步骤的${project_group}。-
tag表示镜像的tag对应持续交付步骤的${imageTag}-${BUILD_NUMBER}。-
java_opts表示jvm的配置变量值由外部传入。-
cpu/memcpu和内存设置由外部变量传入。-
port服务端口由外部变量传入。
执行命令为:
values="server_name=${project_name} port=8083 cpu=1024 mem=2048 java_opts=${java_opts} ns=$project_group tag=${imageTag}-${BUILD_NUMBER}"
ansible-playbook /etc/ansible/dev-common.yaml -e "$values"
此示例模板仅做流程参考,在工作中可以根据自己的实际情况进行修改配置。
持续交付和持续部署的实现方式具有多样性,最重要的是要学会将这些工具链结合起来使用,从中找到适合自己的方式方法。