在前面的jenkins基础实践章节中简单介绍了使用jenkins内置插件实现应用代码的发布,通过上一节Ansible基础知识的介绍,本节来使用Ansible对jenkins基础实践章节的内容继续进行一下重构和提优化。
在前面的章节中已经对ansible做过基础的介绍,这里不多说,首先看一下Jenkins与Ansible的集成如何使用。
Jenkins与Ansible的集成使用主要有三种方式:
下面分别介绍一下前两种的使用方法,以一个freestyle类型的job为例。
使用”Exec shell“,在输入框中输入:
ansible localhost -m shell -a "hostname"
说明:
该命令用来在本机执行hostname命令。- localhost是ansible内置的一个组,表示ansible本机。
除了可以使用ansible Ad-hoc命令意外,还可以使用ansible-playbook命令执行playbook,有兴趣的可以自己尝试一下。
在”构建“步骤中,点击”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部署服务。
以在”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容器中去,至于部署到容器编排系统中,将在课程末尾介绍。
© 2019 - 2023 Liangliang Lee. Powered by gin and hexo-theme-book.