Jenkins快速尝鲜指南
Jenkins作为CI/CD方面的流行技术,同时还相对(k8s之流)易于安装,不自己整一整实在是可惜。
本文介绍jenkins从零到部署一个简单java web应用的过程,力求简单优雅。如果想要跟随本文操作学习,准备如下:
- 一个linux环境(虚拟机或者云服务器,windows当然也可以,不过windows有什么好玩的啊)
- 安装好docker、git、java、maven、nginx(可选)
本文最终希望实现的效果如下:
- jenkinsfile(pipeline定义文件)和dockerfile(docker构建镜像的文件)都在git代码仓库中
- jenkins构建帮助完成如下步骤:
- 服务器(docker的宿主机)拉取git代码
- 在服务器(docker的宿主机)上完成编译打包
- 基于仓库中拉取的dockerfile构建docker镜像
- 使用上一步的镜像启动一个docker容器
这样从推送代码到服务更新,中间只有在jenkins中点击构建这一个操作(这个操作甚至也可以不用,可以配置jenkins监控仓库的代码变更,自动完成构建)
安装Jenkins
Jenkins官网主要介绍了三种方式来安装Jenkins
- 包管理工具安装
- war包安装
- docker安装
包管理器的安装方式虽然方便,但是活都让别人干了,不同系统的安装也可能不太一样,有点隔靴挠痒的感觉,故没有采用。
笔者一开始尝试docker安装,但是后来发现docker方式有诸多麻烦,首先docker中安装的Jenkins需要操作docker。第一种方式docker out of docker,Jenkins在一个docker容器中,需要操作宿主机的docker,设置起来颇为麻烦,也没有找到完整的教程,撇下不谈。
另一种方式是使用docker in docker(Dind),Jenkins官网介绍了这种方式,就是在宿主机的docker中再以(Dind镜像)启动一个docker,这样宿主的docker容器列表里有一个提供Jenkins的容器,还有一个提供docker的容器,再通过socket挂载的方式,让Jenkins容器中的Jenkins可以使用兄弟容器中的docker。
此时宿主机上执行docker ps 效果如下,一个docker:dind镜像启动的容器,直译就是docker中的docker,一个jenkinsci/blueocean镜像启动的容器,里面是jenkins服务。
笔者最终没有使用这种方式,一是因为这样就有了内外两个docker,他们的镜像缓存是不共享的,内部docker启动后,由于jenkins操作的是内部docker,最终的应用是在docker中的docker中的容器中运行的,性能损耗很大,需要的基础镜像和打包所需的jar包都需要重新下载,相当麻烦,最终没有使用这种方式。
最终选择的是war包的方式安装,首先下载Jenkins的war包,然后很简单,按照官方:
java -jar jenkins.war
或者使用自己写的更复杂一点的,笔者的供参考,部分参数请自行去掉或替换:
nohup java -Xms${opt_xms} -Xmx${opt_xmx} -Xmx${opt_xmx} -XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/tmp/jenkins.hprof -jar ${jar_dir}/${jar_name} --httpPort=${opt_port} --prefix=/jenkins >> ${log_path}/${project_name}.log &
然后访问 http://localhost:8080就可以解锁Jenkins了。
这里因为jenkins使用了hudson,所以这个war包既可以java -jar的方式启动,也可以放到web容器中启动
解锁jenkins需要一个密码,密码打印在启动jenkins的控制台输出里了,或者参照官网文档找找。我的文件是在这里的:
cat /home/username/.jenkins/secrets/initialAdminPassword
username换成你的linux用户名。
配置Jenkins
配置访问路径和端口号
如果希望通过 http://localhost:9090访问Jenkins:
java -jar jenkins.war --prefix=/jenkins --httpPort=9090
如果修改了jenkins的路径,需要在系统管理->系统配置中修改如下配置:
这个配置项里需要加上路径配置,端口也是需要的,图中因为我使用了nginx转发至80端口,所以省略了端口号,nginx转发相关,稍后再聊
配置Jenkins用户
通过初始化密码登录后第一件事就是(系统管理->管理用户)创建一个用户,然后(系统管理->全局安全配置)设置授权策略,登录以后才可以操作jenkins,这里我还选择了匿名用户可读。
配置全局工具
前往(系统管理->全局工具配置)配置git、maven、docker,笔者配置简单供参考下
配置插件
前往(系统管理->插件管理)安装以下插件
- 必要插件:
* Git Parameter
构建时可以使用git分支作为参数
* Pipeline Maven Integration
在pipeline中使用mvn指令
* Docker Pipeline
在pipeline中使用docker指令 - 推荐插件:
* Localization: Chinese (Simplified)
中文插件,推荐安装,因为我使用了这个插件,所以文章中的名词都取自中文版
配置nginx转发
因为jenkins不建议root运行,再加上服务器上可能有多个服务,所以jenkins可能难以直接使用80端口,这样访问jenkins还需要带上端口号(太low了吧),所以需要nginx转发。官方有一篇文章介绍了这方面内容。
又因为笔者服务器的域名备案过期,所以多个服务不能使用二级域名来区分,只能都挂在80端口的default_server下,然后根据不同的路径来定位不同的服务。也就是我期望的jenkins访问路径是这样的:http://111.231.101.154/jenkins/所以上面文章中的方案并不能直接使用。笔者贴出自己的nginx配置文件关键部分供参考:
upstream jenkins {
keepalive 32; # keepalive connections
server 127.0.0.1:9090; # jenkins ip and port
}
server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
location ~ "^jenkins/static/[0-9a-fA-F]{8}\/(.*)$" {
#rewrite all static files into requests to the root
#E.g /static/12345678/css/something.css will become /css/something.css
rewrite "^jenkins/static/[0-9a-fA-F]{8}\/(.*)" /$1 last;
}
location /jenkins/userContent {
#have nginx handle all the static requests to the userContent folder files
#note : This is the $JENKINS_HOME dir
root /var/lib/jenkins/;
if (!-f $request_filename){
#this file does not exist, might be a directory or a /**view** url
rewrite (.*) /$1 last;
break;
}
sendfile on;
}
location /jenkins {
proxy_pass http://jenkins;
proxy_redirect default;
proxy_http_version 1.1;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_max_temp_file_size 0;
#this is the maximum upload size
client_max_body_size 10m;
client_body_buffer_size 128k;
proxy_connect_timeout 90;
proxy_send_timeout 90;
proxy_read_timeout 90;
proxy_buffering off;
proxy_request_buffering off; # Required for HTTP CLI commands in Jenkins > 2.54
proxy_set_header Connection ""; # Clear for keepalive
}
}
新建构建任务
直接上图:
笔者仓库地址:https://github.com/javaisgood/urltest.git
保存后自动跳转至任务主页,点击立即构建
稍等之后,构建完成:
这时候服务已经启动成功,pipeline的执行步骤也和Jenkinsfile中定义的一样。现在访问http://{{host}}:9000/url/hello_jenkins应该可以看到响应结果,结果就是打印请求的url,具体实现可查看仓库中代码。
如笔者的http://111.231.101.154/url/hello_jenkins
这里因为笔者使用了nginx,将80端口的/url路径请求转发至9000,所以省略了端口号。
参数化构建
使用参数
其实在之前的构建完成后,仔细看图就会发现原来的立即构建变成了“Build with Parameters”参数化构建:
点开下面的配置,也可以在配置页找到如下内容:
此时点击“Build with Parameters”可以看到构建不会直接开始,而是加入了新的步骤:
填写AppName,这其实是因为在代码仓库的Jenkinsfile中,定义了一个参数,这个参数会被直接应用于这个任务的配置中(第一次取的默认值直接构建了,之后会加入填写参数步骤)。
可以看到在Jenkinsfile文件中的parameters 部分定义了参数和默认值
pipeline {
agent any
parameters {
string(name: 'AppName', defaultValue: 'jswdwsx/url', description: 'this is the app name')
}
...省略...
}
指定分支构建
参数化构建的常见应用就是用来选择git分支了,指定分支构建依赖上面提到的Git Parameter插件,安装好这个插件后,再次配置任务
在参数化构建过程里添加一个参数,类型选择Git参数(没装插件没这个选项),填写内容如下
名称:branch
描述:随便写
参数类型:分支或标签
默认值:origin/master
再往下,到流水线定义部分:
选择构建分支这里,原来填的是master分支,现在要选择分支构建,分支当然需要取自上面的git参数,引用参数的写法我试了很久就发现一种写法可以:
指定分支:refs/remotes/$branch
$branch表示引用之前定义的参数的值。
关于这个写法为什么要这么写,我是根据报错的日志倒推出来的。开始试了很多种写法,头都大了,所以弄完专门水了一篇,参考此文
保存以后就可以重新尝试构建了:
可以看到新增了一个参数,和之前的设置也是一一对应,如此一来点击选择分支,点击开始构建,就能开始构建指定的分支了!
原理简析
Jenkinsfile文件
我们需要在jenkins中的配置操作,前面基本已经说完了。接下来,每次构建,jenkins就会拉取pull代码,然后读取其中的Jenkinsfile文件,按照文件的内容来执行一系列操作。
Jenkinsfile定义了一个流水线(Pipeline),其中stages部分定义了jenkins一次构建需要执行哪些操作,一起来看看:
...省略...
stages {
stage('Build jar') {
steps {
sh 'mvn -B -DskipTests clean package'
sh 'echo Build jar done!'
}
}
stage('Remove image') {
steps {
script {
try {
sh "docker ps -a | grep ${params.AppName} | awk '{print \$1}'| xargs docker rm -f"
sh "docker rmi ${params.AppName}"
sh 'echo Remove image done!'
}
catch (exc) {
echo 'image do not exist!'
}
finally {
sh 'echo Remove image finally done!'
}
}
}
}
stage('Build image') {
steps {
sh 'mvn docker:build'
sh 'echo Build image done!'
}
}
stage('Deploy') {
steps {
sh "docker run -p 9000:8080 -d ${params.AppName}"
}
}
}
...省略...
5个stage分别完成以下事情:
- Build jar
mvn完成java web应用的打包 - Remove image
先删除上一次启动的容器,再删除上一次生成的镜像,如果报错(之前容器或镜像不存在的时候,比如第一次构建时),只打印提示信息,不能停止后续构建操作。 - Build image
使用mvn的docker插件build一个docker镜像,构建细节在dockerfile中定义 - Deploy
使用上一步构建的镜像,启动一个容器。
这样就定义了一次完整的构建的过程了。
构建的阶段视图中可以看到,每个阶段(stage)都会分开展示,点击绿色方块可以查看对应stage的日志
dockerfile文件
上面说到build镜像的时候,步骤定义在dockerfile中,再来看看dockerfile吧
FROM openjdk:8u265-slim
RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime
RUN echo 'Asia/Shanghai' >/etc/timezone
ADD target/url-1.0.jar /usr/share/jswdwsx/url.jar
CMD ["/bin/bash", "-c", "java -jar -server /usr/share/jswdwsx/url.jar"]
EXPOSE 8080
大概就是如下内容:
- 指定一个基础镜像
- 设置时区
- 添加jar包
- 设置启动命令
- 暴露端口
这里注意docker镜像定义的端口号是8080,但是Jenkinsfile里又将容器的8080端口映射到宿主机的9000端口了,所以访问服务的端口最终是9000,如果你像我一样还使用了nginx转发,那么就是80端口了。
总结
至此,我们完成了jenkins发布java web应用的体验。
我们比较了jenkins几种安装方式的特点,说明了我们选择war包安装的理由。
我们定义了一个流水线来完成构建,另一种定义构建的方式是直接让jenkins执行一段shell脚本,相对来说流水线是更优雅的,有着更清晰的语法和结构,以及更直观的构建过程展示。
我们使用了git分支参数化构建,可以指定分支进行构建。
我们将Jenkinsfile和dockerfile都纳入版本管理,这样这两个文件可以方便地修改,也可以追踪其变化。
我们使用了一些插件,包括jenkins插件和mvn插件,有效简化了整个过程。
我们还使用了nginx进行转发,更贴近真实生产环境的情况。