learn-tech/专栏/Jenkins持续交付和持续部署/08.Jenkins集成Ansible持续部署服务到Docker容器.md
2024-10-16 06:37:41 +08:00

18 KiB
Raw Blame History

                        因收到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 jenkins989: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容器中去至于部署到容器编排系统中将在课程末尾介绍。