jenkins基于Ansible自动发布/回滚/管理

   看着似乎用jenkins基于ansible发布spring boot/cloud类的jar包程序,或者tomcat下的war包的需求挺多的,闲来无事,也说说自己做过的jenkins基于ansible的发布方法。

 

规范与标准

    无规矩不成方圆,要做好后期的自动化,标准化是少不了的,下面是我们这边规划的一些标准(非强制,根据自己实际情况调整)

  • 应用名称:{应用类型}-{端口}-{应用名称} or {类型}-{应用名称}, 例如:web-8080-gateway, app-jobs-platform
  • 主机名称:{地区}-{机房}-{应用类型}-{代码语言}-{项目分组}-{ip结尾}, 例如:sz-rjy-service-java-foo-14-81
  • 日志路径:/log/web,例如:/log/web/web-8080-gateway
  • 应用路径:/data/,例如:/data/web-8080-gateway

   不难看出,这里应用名称前缀使用的是主机名称的第三个字段(看起来挺麻烦的,不过没办法,谁让公司是这么规定的呢

环境配置

  1). 软件版本描述

  • Ansible: 2.7.1
  • Python: 2.7.5
  • CentOS: 7.2
  • Java: 1.8.0_73
  • Jenkins: 2.121.1

  

  2).环境/软件安装

        略...自己玩去

 

Ansible Role

 1).目录结构

 

  1. playbooks/deploy.yml        # 入口文件
  2. roles/deploy
  3. ├── defaults
  4.    └── main.yml            # 默认参数
  5. ├── tasks
  6.    ├── backup.yml          # 备份应用
  7.    ├── common.yml
  8.    ├── gray_deploy.yml     # 发布应用
  9.    ├── main.yml            # 主配置
  10. └── templates
  11.     ├── service.sh.j2       # 服务管理模板
  12.     └── systemd.service.j2  # systemd模板

 

2).role配置

  playbooks/deploy.yml(入口文件)

  1. ---
  2. - hosts: "{{ TARGET }}"
  3.   remote_user: "{{ REMOTE_USER }}"
  4.   any_errors_fatal: true
  5.  
  6.   roles:
  7.    - deploy


defaults/main.yml

  1. ---
  2. # defaults file for deploy
  3. # 获取项目名称,JOB_NAME来自jenkins内建参数,可见下文jenkins配置页说明
  4. PROJECT: "{{ JOB_NAME.split('_')[-1] }}"
  5.  
  6. # 项目路径
  7. PROJECT_DIR: "/data"
  8.  
  9. # JAVA参数配置项,JAVA_OPTIONS来自jenkins参数化构建,可见下文jenkins配置页说明
  10. JAVA_OPTS: "{{ JAVA_OPTIONS | default('-Xmx256m -Xms256m') }}"
  11.  
  12. # systemd配置路径
  13. SYSTEMD_PATH: "/etc/systemd/system"
  14.  
  15. # 应用日志路径
  16. LOGPATH: "/log/web"
  17.  
  18. # 备份目录
  19. BACKUP: "/data/backup/{{ PROJECT }}"
  20.  
  21. # jdk version
  22. # 配和include_role: jdk使用
  23. jdk:
  24.   version: "{{ version | default('1.8.0_73') }}"

 

tasks/main.yml(主配置)

  1. ---
  2. # 本想引用jdk的role做到预配置jdk环境,但是become下遇到了些bug,如果是秘钥或者直连,应该也是没问题的
  3. #- include_role: name=jdk
  4.  
  5. - include_tasks: common.yml
  6.  
  7. - include_tasks: backup.yml
  8.  
  9. # 这里使用loop循环play_hosts(当前执行的主机),是为了实现一个蓝绿,当然,这是主机少的情况,
  10. # 如果一个应用有N台主机,这效率就很低了,这样的话,可以考虑设置全局serial来控制每次发布的比例
  11. # PS: 如果不需要可以把run_once与loop去掉, ab_deploy.yml里的delegate_to去掉
  12. - include_tasks: ab_deploy.yml
  13.   loop: "{{ play_hosts }}"
  14.   run_once: true
  15.   become: yes

 [\d+\.]{3,}\d+

tasks/common.yml(公共配置,预配置环境,创建目录等)

  1. ---
  2. - set_fact:
  3.     BASENAME: "{{ ansible_hostname.split('-')[2] }}-{{ SERVER_PORT }}-{{ PROJECT }}"
  4.   when: (SERVER_PORT is defined) and (SERVER_PORT != "")
  5.  
  6. - set_fact:
  7.     BASENAME: "{{ ansible_hostname.split('-')[2] }}-{{ PROJECT }}"
  8.   when: (SERVER_PORT is not defined) or (SERVER_PORT == "")
  9.  
  10. - set_fact:
  11.     WORKPATH: "{{ PROJECT_DIR }}/{{ BASENAME }}"
  12.  
  13. - name: 检查 {{ WORKPATH }} 工作路径
  14.   stat: path={{ WORKPATH }}
  15.   register: work
  16.  
  17. - name: 检查systemd
  18.   stat: path={{ SYSTEMD_PATH }}/{{ PROJECT }}.service
  19.   register: systemd
  20.  
  21. - block:
  22.    - name: 创建 {{ WORKPATH }}
  23.      file: path={{ item }} state=directory owner={{ REMOTE_USER }} group={{ REMOTE_USER }} recurse=yes
  24.      with_items:
  25.        - "{{ WORKPATH }}"
  26.        - "{{ LOGPATH }}"
  27.  
  28.    - name: 推送syetmed模板
  29.      template: src=systemd.service.j2 dest={{ SYSTEMD_PATH }}/{{ PROJECT }}.service
  30.   become: yes
  31.  
  32. - name: Local | find package
  33.   find: paths={{ WORKSPACE }} patterns=".*{{ PROJECT.split('-')[0] }}.*\.jar$" age=-60 age_stamp=mtime recurse=yes use_regex=yes
  34.   delegate_to: localhost
  35.   register: target_file
  36.  
  37. - assert:
  38.     that:
  39.       - "target_file.files.0.path is defined"
  40.     msg: "未找到构建文件,请检查构建过程"
  41.  
  42. - set_fact:
  43.     package: "{{ target_file.files.0.path }}"
  44.     
  45. # 推送管理脚本
  46. - name: Push script
  47.   template: src=service.sh.j2 dest={{ WORKPATH }}/{{ PROJECT }}.sh mode=0750

 

tasks/backup.yml(备份应用)

  1. ---
  2. - name: 获取远程文件信息
  3.   stat:
  4.     path: "{{ WORKPATH }}/{{ PROJECT }}.jar"
  5.   register: history_pkg
  6.  
  7. # 获取一个时间点
  8. - set_fact: backup_time={{ '%Y%m%d_%H%M' | strftime }}
  9.  
  10. - block:
  11.   # 在控制端创建了一个空的目录?(至于为什么要建一个空的目录,后面回滚会用到)
  12.   - name: Create local flag
  13.     file: path={{ BACKUP }}/{{ backup_time }} state=directory recurse=yes
  14.     delegate_to: localhost
  15.     run_once: true
  16.  
  17.   # 在远程主机创建备份目录
  18.   - name: Create backup directory
  19.     file:
  20.       path: "{{ BACKUP }}/{{ backup_time }}"
  21.       state: directory
  22.       owner: "{{ REMOTE_USER }}"
  23.       group: "{{ REMOTE_USER }}"
  24.       recurse: yes
  25.     become: yes
  26.  
  27.   # 备份到远程主机的本地路径
  28.   - name: Backup {{ PROJECT }}
  29.     shell: |
  30.       \cp -ra {{ WORKPATH }}/* {{ BACKUP }}/{{ backup_time }}/
  31.  
  32.   # 远程文件存在才备份
  33.   when: history_pkg.stat.exists

 

tasks/ab_deploy.yml(逐台推送打包文件,重启应用)

  1. ---
  2. # 有人可能会问delegate_to为何不写外层的include_tasks,其实这似乎是不支持或者是bug,
  3. # include_tasks获取的执行对象居然是同一个,导致delegate_to+loop只在一台机器上生效
  4. # 不过我们item写在block里获取是正常的,有兴趣的童鞋可以试试。
  5. # PS:不需要ab发布可以去掉delegate_to
  6. - block:
  7.     # 推送编译好的jar包
  8.     - name: Push {{ package }} --> {{ WORKPATH }}/{{ PROJECT }}.jar
  9.       copy: src={{ package }} dest={{ WORKPATH }}/{{ PROJECT }}.jar mode=0640
  10.       
  11.     - name: Restart Service
  12.       systemd: name={{ PROJECT }} state=restarted enabled=yes daemon_reload=yes
  13.       become: yes
  14.  
  15.     # 等待服务打开端口提供服务,超时30s,注意到,这里只有定义了SERVER_PORT才执行
  16.     # 相对的,你可以走接口或者页面检查页面的状态码或者返回内容来做一样的判断,
  17.     # 参考模块: shell,uri,until
  18.     # PS: 不需要ab发布可以去掉delegate_to
  19.     - name: Wait for {{ SERVER_PORT }} available
  20.       wait_for:
  21.         host: "{{ ansible_default_ipv4.address }}"
  22.         port: "{{ SERVER_PORT }}"
  23.         delay: 5
  24.         timeout: 30
  25.       when: SERVER_PORT is defined
  26.   delegate_to: "{{ item }}"
  27.   
  28.   # 上面的wait失败后执行的任务,(非必要,要么真的慢,要么是真的没起来)
  29.   # 这里也可以放其他任务,比如直接fail模块失败消息,或者失败的回滚策略?
  30.   rescue:
  31.     - debug:
  32.         msg: "{{ PROJECT }} {{ SERVER_PORT }} timeout more the 30s!"

 

 

templates/service.sh.j2

  1. #!/bin/bash
  2.  
  3. # public func
  4. source /etc/init.d/functions
  5.  
  6. # Env
  7. source /etc/profile
  8.  
  9. # program
  10. program="{{ PROJECT }}.jar"
  11.  
  12. # work path
  13. work_path={{ WORKPATH }}
  14.  
  15. # check --no-daemonize option
  16. args=$2
  17.  
  18. # other args
  19. {# 这里可以设置很多jinja2的判断,根据不同模块,业务配置不同的一些需要的参数 #}
  20. {# eureka账户密码,非必要,根据自己的业务来吧 #}
  21. {% if ENV == 'STG' %}
  22. # eureka账户名
  23. export EUREKA_USER='abc'
  24. # eureka密码
  25. export EUREKA_PASS='123'
  26. {% endif %}
  27.  
  28.  
  29. # jmx,按需吧。
  30. # JMX_OPTS="-Djava.rmi.server.hostname={{ ansible_default_ipv4.address }} -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=1{{ SERVER_PORT | default('9990') }} -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false"
  31.  
  32. # JAVA OPTIONS
  33. {# 这些参数其实是从jenkins的参数化构建传入到jenkinsansible插件里的高级选项里的extra vars传入的 #}
  34. {# 这里我们的eureka配置项是环境变量传入的,各位的方式可能不同,自己斟酌 #}
  35. {# 判断是否包含eureka_conf选项,有则为注册中心配置(eureka_conf也是从jenkins传入) #}
  36. {% if eureka_conf is defined %}
  37. JAVA_OPTS="{{ JAVA_OPTS }} -Dspring.profiles.active={{ eureka_conf }} $JMX_OPTS"
  38. {% else %}
  39. JAVA_OPTS="{{ JAVA_OPTS }} $JMX_OPTS"
  40. {% endif %}
  41.  
  42. get_pid() {
  43.      ps -eo pid,cmd | grep java | grep "${program}" | grep -Ev "grep|python" | awk '{print $1}'
  44. }
  45.  
  46. run_program() {
  47.     cd ${work_path}
  48.     if [[ "${args}"== "--no-daemonize"]];then
  49.         # 至于为什么要放在前台执行,是为了将程序交给systemd管理
  50.         java -jar ${JAVA_OPTS} ${program} 
  51.     else
  52.         nohup java -jar ${JAVA_OPTS} ${program} &> /dev/null &
  53.     fi
  54.     [[ $? -eq 0 ]] && return 0 || return 1
  55. }
  56.  
  57. run_or_not() {
  58.     pid=$(get_pid)
  59.     if [[ ! ${pid} ]];then
  60.         return 1
  61.     else
  62.     return 0
  63.     fi
  64. }
  65.  
  66. start() {
  67.     run_or_not
  68.     if [[ $? -eq 0 ]];then
  69.         success;echo -"${program} is running..."
  70.     else
  71.         cd ${work_path} && run_program
  72.         if [[ $? -eq 0 ]];then
  73.             sleep 1
  74.             pid=$(get_pid)
  75.             if [[ "$pid"!= x ]];then
  76.                 flag=success
  77.             else
  78.                 flag=failure
  79.                 $flag;echo -"Success Start ${program}, but it exists.!"
  80.                 exit 1
  81.             fi
  82.         else
  83.             flag=failure
  84.         fi
  85.         $flag;echo -"[$pid] Start ${program}"
  86.     fi
  87. }
  88.  
  89. stop() {
  90.     run_or_not
  91.     if [[ $? -eq 0 ]];then
  92.         pid=$(get_pid)
  93. kill -9 $pid
  94.         if [[ $? -eq 0 ]];then
  95.         flag=success
  96.     else
  97.             flag=failure
  98.         fi
  99.     $flag;echo -"Stop $program"
  100.     else
  101.         $flag;echo -"$program is not running."
  102.     fi
  103. }
  104.  
  105. status() {
  106.     run_or_not
  107.     if [[ $? -eq 0 ]];then
  108.     pid=$(get_pid)
  109.     success;echo -"[$pid] $program is running..."
  110.     exit 0
  111.     else
  112.     failure;echo -"$program not running."
  113.     exit 1
  114.     fi
  115. }
  116.  
  117. case $1 in
  118.     start)
  119.     start
  120.     ;;
  121.     stop)
  122.     stop
  123.     ;;
  124.     status)
  125.     status
  126.     ;;
  127.     restart)
  128.     stop
  129.     sleep 1
  130.     start
  131.     ;;
  132.     *)
  133.         echo "Usage: $0 {start [--no-daemoize]|stop|status|reload|restart}"
  134.         exit 1
  135. esac

 

templates/systemd.service.j2

  1. [Unit]
  2. Description={{ PROJECT }}
  3. After=network.target
  4.  
  5. [Service]
  6. {# --no-daemonize这里放在前台启动了 #}
  7. ExecStart={{ WORKPATH }}/{{ PROJECT }}.sh start --no-daemonize
  8. ExecStop={{ WORKPATH }}/{{ PROJECT }}.sh stop
  9. WorkingDirectory={{ WORKPATH }}
  10. {# 按需配置重启策略吧 #}
  11. #Restart=on-failure
  12. #RestartSec=30
  13. User={{ REMOTE_USER }}
  14. Group={{ REMOTE_USER }}
  15. RuntimeDirectory={{ PROJECT }}
  16. RuntimeDirectoryMode=0755
  17.  
  18. [Install]
  19. WantedBy=multi-user.target

注:为何要注册到systemd呢?一个是统一管理维护(业务环境复杂多样,go/python/java/.net core有脚本启动的,有中间件管理启动的,管理方式层出不穷),以及结合journalctl捕获控制台的输出日志

 

Jenkins结合Ansible的自动发布

1)jenkins依赖插件描述

 

2)jenkins项目命名规范

{环境}_{项目组}_{应用名},如:STG_AIO_gateway, STG_AIO_basic-data

ps: 不一定按照这个规则写,根据自己需求改动上下文吧。

 

3)jenkins项目配置(以配置中心为例)

这里的STG_USER_PASS,ROOT_PASS是远程服务的登录密码,传递给ansible作为登录依据,当然你做了互信或者其他方式也可以的

这里描述了业务环境,服务对外提供端口,以及java的配置参数

Git的配置

 

 

构建的配置

 

接下来是最关键的ansible配置

这里我们把主机inventory信息跟账户验证都写在了页面文本中(这种方式的好处是什么?是你可以随时新增可用主机来添加实例,而不需要调整其他)

不难看出这里的content就是inventory,我们也很容易的通过content的vars配置各种差异和传值,十分的方便,比如,我们配置的eureka实例,要配置主从复制,启动的参数是有差异的,从前面的service.sh.j2我们也看到了eureka的一些jinja2判断,那么它判断参数的来源就是这个content的东西,如下图的eureka配置示例所示:

 

 

 

接下来,我们继续点开下方的高级选项,配置extra vars来传入前面的参数化构建的参数以及一些内置参数

 

至此,关键的配置已经完成了,如果有需要,各位还可以配置一些邮件通知什么的。

 

4)发布动图演示

 

发布后的回滚

        由于各种各样的原因,发布的代码可能会出现异常,这时候可能需要紧急回滚代码,庆幸的是,我们前面有做备份策略,我们可以手动去回滚备份的代码,但是缺点也很明显,当主机实例过多时,手动回滚明显是不再明智的,所以我们想办法结合Jenkins+Ansible这两者来做到一个通用的服务回滚策略,首先我们先分析下我们回滚代码需要用到什么?

  1. 代码的历史备份
  2. 回滚应用名称
  3. 需要回滚的主机

首先看下第1点,我们发布过程是是有对代码做备份的。再看第2,3点,应用名称跟回滚的主机哪里可以获取到呢?答案是jenkins job里。

我们来看下JENKINS_HOME下对应的job是什么样的。

  1. # 进去你的jenkins家目录
  2. cd /data/jenkins/
  3. ls STG_AIO_config/
  4. builds  config.xml  lastStable  lastSuccessful  modules  nextBuildNumber

我们通过拆分目录名STG_AIO_config可以得到{环境}{项目分组}{应用名称}等信息,而主机信息就在config.xmlcontent字段内!

  1. # 以下是config.xml的部分配置片段
  2.       <playbook>/etc/ansible/playbooks/deploy.yml</playbook>
  3.       <inventory class="org.jenkinsci.plugins.ansible.InventoryContent">
  4.         <content>[target]
  5. 10.20.24.81
  6. 10.20.24.82
  7. [target:vars]
  8. ansible_password=${STG_USER_PASS}
  9. ansible_become_user=root
  10. ansible_become_method=su
  11. ansible_become_pass=${ROOT_PASS}
  12. CFG_SVR_USER=${CFG_SVR_USER}
  13. CFG_SVR_PASS=${CFG_SVR_PASS}
  14. CFG_SVR_KEY_PASS=${CFG_SVR_KEY_PASS}
  15. CFG_SVR_KEY_SECRET=${CFG_SVR_KEY_SECRET}</content>
  16.         <dynamic>false</dynamic>
  17.       </inventory>
  18.       <ansibleName>ansible-playbook</ansibleName>

 

首先我们看一下成品的一个截图效果。

 

接下来,我们看看jenkins里是如何做到的。

 

1)创建回滚任务的jenkins job

    我们新建了一个常规job,叫服务回滚(自己随便叫啥),通过参数化构建插件<Active Choices Plug-in>动态的去通过shell命令去获取JENKINS_HOME/job下的所有任务,拆分JOB名称得到{环境}{项目分组}{应用名称},再通过shell命令获取config.xml里的主机信息,又通过{应用名称}获取备份目录下(还记得在前面的发布环节我们在Jenkins本机创建的空目录么,当然你也可以写文件里读取)备份目录名称得到历史还原点,于是得到了前面需要的3个点。

    现在我们来看看怎么做。新建的job叫服务回滚

 

下拉选择参数化构建,选择动态参数,新建一个Avctive Choices Parameter ==>ENV,选择Groovy script,里面用groovy套了一个shell去执行(当然,你groovy熟悉不套shell能获取一个列表也行),

Choice Type选择Single Select,即单选框

这个shell能获取到什么呢?我们看下

  1. #为了演示效果我新建的PROD,DEV的目录
  2. [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/jenkins/jobs && ls |grep -Po '^(?!PRD|APP)[A-Z]+(?=_)'  | sort -| uniq
  3. DEV
  4. PROD
  5. STG 

可以知道,我们这里是获取了多个{环境}

 

接下来,我们添加第二个参数Active Choices Reactive Parameter(要引用其他参数)==> GROUP,Groovy Script里多了一个env=ENV是为了引入前面获取的ENV,Choice Type里继续勾选单选框,

不同的是下面多了一个Referenced parameter,这里填写ENV(这个是因为要关联上面的ENV)

我们代入ENV,看下shell下能获取到什么呢?

  1. [root@sz-rjy-ops-ansible-config-23-222 config]# env=STG
  2. [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/jenkins/jobs && ls -d $env* | grep -Po '(?<=_)[A-Z0-9]+(?=_)' | sort -| uniq
  3. AIO
  4. J

获取到了{项目分组}

 

接下来,我们添加第三个动态参数Active Choices Reactive Parameter(要引用其他参数)==>SERVICE,Groovy Script里多了一个env=ENV,group=GROUP是为了引入前面获取的ENV跟GROUP,Choice Type里继续勾选单选框,

不同的是下面多了一个Referenced parameter,这里填写ENV,GROUP(这个是因为要关联上面的ENV,GROUOP)

我们看下这个shell又能获取到什么

  1. [root@sz-rjy-ops-ansible-config-23-222 config]# env=STG
  2. [root@sz-rjy-ops-ansible-config-23-222 config]# group=AIO
  3. [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/jenkins/jobs/ && ls -d ${env}_${group}_* | grep -Po "(?<=_)[a-z0-9-].*" | sort
  4. basic-data
  5. bootadmin
  6. ce-system
  7. config
  8. gateway
  9. jobs-acc-executor
  10. loan-batch
  11. monitor
  12. eureka
  13. zipkin
  14. 。。。 。。。

这里我们获取到了{应用名}

 

最后,我们再配置一个Active Choices Reactive Parameter==>HISTORY,用于获取历史版本,选择单选框,关联SERVICE参数

我们执行shell试下

  1. [root@sz-rjy-ops-ansible-config-23-222 config]# service=config
  2. [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/backup/$service/ && ls  | sort -nr | head -n10
  3. 20181113_1505
  4. 20181113_1502
  5. 20181113_1437
  6. 20181113_1434
  7. 20181113_1432
  8. 20181113_1425
  9. 20181113_1415
  10. 20181112_1118
  11. 20181112_1110
  12. 20181112_1105

获取到了历史的备份点。

 

作为可选项,我们还可以加个Active Choices Reactive Parameter==>INFO的动态参数构建,用于获取config.xml里的job描述,这里要关联三个参数ENV,GROUP,SERVICE

shell内执行效果如下:

  1. [root@sz-rjy-ops-ansible-config-23-222 config]# env=STG
  2. [root@sz-rjy-ops-ansible-config-23-222 config]# group=AIO
  3. [root@sz-rjy-ops-ansible-config-23-222 config]# service=config
  4. [root@sz-rjy-ops-ansible-config-23-222 config]# grep -Po '(?<=<description>).*(?=<)' /data/jenkins/jobs/${env}_${group}_${service}/config.xml | head -1
  5. 配置中心

正确获取到了描述信息。

 

我们正确获取到了{环境},{项目分组},{应用名}以及{历史备份点}等信息,但是还没有关联的inventory,既然inventory信息在config.xml中已有,我们可以通过声明ENV,GROUP,SERVICE环境变量,再通过脚本获取这几个值来拼接出config.xml所在位置,再通过解析xml来获取主机host,得到一个ansible动态inventory,(不单单是host,我们可以在xml里获取各种我们定义的值来作为inventory vars变量为我们所用!)

 

我们在jenkins下拉到构建选项,添加一个Executor shell

我们将ENV,GROUP,SERVICE声明到了执行的环境变量中,

我们再看看inventory这个脚本是如何获取的。

  1. cat /data/script/python/inventory.py
  2. #!/usr/bin/python
  3. # -- encoding: utf-8 --
  4. ## pip install xmltodict ##
  5. import xmltodict
  6. import json
  7. import re
  8. import os
  9.  
  10. # 从环境变量获取参数
  11. # 账号密码你做了信任就不需要,自己看着办
  12. options = {
  13.     'ENV': os.getenv('ENV'),
  14.     'GROUP': os.getenv('GROUP'),
  15.     'SERVICE': os.getenv('SERVICE'),
  16.     'ACTION': os.getenv('ACTION'),
  17.     'ansible_user': 'stguser',
  18.     'ansible_password': 'abc',
  19.     'ansible_become_pass': '123',
  20.     'ansible_become_method': 'su',
  21.     'ansible_become_user': 'root',
  22.     'ansible_become': True
  23. }
  24.  
  25. def getXml(env,group,service):
  26.     ''' 拼接对应项目的jenkinx config.xml路径'''
  27.     file = '/data/jenkins/jobs/{}_{}_{}/config.xml'.format(env,group,service)
  28.     return file
  29.  
  30. def getData(file): 
  31.     data = dict()
  32.     xml = open(file)
  33.     try:  
  34.         xmldata = xml.read()  
  35.     finally:  
  36.         xml.close()
  37.     convertedDict = xmltodict.parse(xmldata)
  38.  
  39.     # maven2 项目模板数据提取
  40.     if convertedDict.has_key('maven2-moduleset'):
  41.         name = convertedDict['maven2-moduleset']['rootModule']['artifactId']
  42.         _ansi_obj = convertedDict['maven2-moduleset']['postbuilders']['org.jenkinsci.plugins.ansible.AnsiblePlaybookBuilder']
  43.  
  44.         # 可能有多个playbbok,只要是inventory一样就无所谓取哪一个(这里取第一个,如果多个不一样,自己想办法合并)
  45.         if isinstance(_ansi_obj,list):
  46.             host_obj = _ansi_obj[0]['inventory']['content']
  47.         else:
  48.             host_obj = _ansi_obj['inventory']['content']
  49.  
  50.         data['hosts'] = re.findall('[\d+\.]{3,}\d+',host_obj)
  51.  
  52.         # 如果设置了参数化构建,把只读参数作为ansible参数
  53.         if convertedDict['maven2-moduleset']['properties'].has_key('hudson.model.ParametersDefinitionProperty'):
  54.             parameter_data = convertedDict['maven2-moduleset']['properties']['hudson.model.ParametersDefinitionProperty']['parameterDefinitions']['com.wangyin.ams.cms.abs.ParaReadOnly.WReadonlyStringParameterDefinition']
  55.     
  56.     # 这里使用的自由风格模板模板,数据结构与maven2不一样,需要拆开判断
  57.     if convertedDict.has_key('project'):
  58.         host_obj = convertedDict['project']['builders']['org.jenkinsci.plugins.ansible.AnsiblePlaybookBuilder']['inventory']['content']
  59.  
  60.         data['hosts'] = re.findall('[\d+\.]{3,}\d+',host_obj)
  61.         
  62.         # 如果设置了参数化构建,把只读参数作为ansible参数
  63.         if convertedDict['project']['properties'].has_key('hudson.model.ParametersDefinitionProperty'):
  64.             parameter_data = convertedDict['project']['properties']['hudson.model.ParametersDefinitionProperty']['parameterDefinitions']['com.wangyin.ams.cms.abs.ParaReadOnly.WReadonlyStringParameterDefinition']
  65.     
  66.     # 插入参数化构建参数(我这里是只读字符串参数)
  67.     try:       
  68.         for parameter in parameter_data:
  69.             data[parameter['name']] = parameter['defaultValue']
  70.     except:
  71.         pass
  72.  
  73.     #print(json.dumps(convertedDict,indent=4))
  74.     return data
  75.  
  76. def returnInventory(xmldata,**options):
  77.     ''' 合并提取的数据,返回inventory的json'''
  78.  
  79.     inventory = dict()
  80.     inventory['_meta'] = dict()
  81.     inventory['_meta']['hostvars'] = dict()
  82.     inventory[options['SERVICE']] = dict()
  83.     inventory[options['SERVICE']]['vars'] = dict()
  84.  
  85.     # 合并xmldata提取的数据
  86.     for para_key,para_value in xmldata.items():
  87.         # 单独把hosts列表提取出来,其他的都丢vars里
  88.         if para_key == 'hosts':
  89.             inventory[options['SERVICE']][para_key] = para_value
  90.         else:
  91.             inventory[options['SERVICE']]['vars'][para_key] = para_value
  92.     # 合并options里的所有东西到vars里
  93.     for opt_key,opt_value in options.items():
  94.         inventory[options['SERVICE']]['vars'][opt_key] = opt_value
  95.     return inventory
  96.   
  97. if __name__ == "__main__":
  98.     xmldata = getData(getXml(options['ENV'],options['GROUP'],options['SERVICE']))
  99.     print(json.dumps(returnInventory(xmldata,**options),indent=4))

 

我们看看执行结果

  1. [root@sz-rjy-ops-ansible-config-23-222 config]# export ENV=STG GROUP=AIO SERVICE=config
  2. [root@sz-rjy-ops-ansible-config-23-222 config]# /data/script/python/inventory.py
  3. {
  4.     "config": {
  5.         "hosts": [
  6.             "10.20.24.81", 
  7.             "10.20.24.82"
  8.         ], 
  9.         "vars": {
  10.             "ansible_become_method": "su", 
  11.             "GROUP": "AIO", 
  12.             "SERVER_PORT": "8888", 
  13.             "SERVICE": "config", 
  14.             "ansible_become_user": "root", 
  15.             "ansible_become": true, 
  16.             "ansible_user": "stguser", 
  17.             "ENV": "STG", 
  18.             "ansible_become_pass": "abc", 
  19.             "ACTION": null, 
  20.             "ansible_password": "123"
  21.         }
  22.     }, 
  23.     "_meta": {
  24.         "hostvars": {}
  25.     }
  26. }

可以看到能正常的获取到一个动态inventory的json了

 

最后我们看下jenkins里的ansible配置,inventory执行了python脚本,并传入了一个extra vars 的HISTORY参数

 

 

2)Ansible role

目录结构

  1. playbooks/spring-rollback.yml      # 入口文件
  2. roles/spring-rollback
  3.             ├── defaults
  4.                └── main.yml       # 默认参数
  5.             ├── README.md
  6.             └── tasks
  7.                 ├── common.yml     # 公共配置
  8.                 ├── main.yml       # 主配置
  9.                 ├── rollback.yml   # 回滚任务

playbooks/spring-rollback.yml

  1. ---
  2. - hosts: all
  3.  
  4.   pre_tasks:
  5.    - assert:
  6.        that:
  7.        - "HISTORY != ''"
  8.        fail_msg: '请选择一个正确的历史版本!'
  9.  
  10.   roles:
  11.    - spring-rollback

defaults/main.yml

  1. ---
  2. # defaults file for rollback
  3. # 备份路径
  4. BACKUP: "/data/backup/{{ SERVICE }}/{{ HISTORY }}"
  5. OWNER: stguser

tasks/main.yml

  1. ---
  2. # tasks file for rollback
  3. - include_tasks: common.yml
  4.  
  5. - include_tasks: rollback.yml
  6.   loop: "{{ play_hosts }}"
  7.   run_once: true
  8.   become: yes

tasks/common.yml

  1. ---
  2. - shell: "ls -d /data/*{{ SERVICE }}"
  3.   register: result
  4.  
  5. - set_fact:
  6.     src_package: "{{ BACKUP }}"
  7.     dest_package: "{{ result.stdout }}"

tasks/rollback.yml

  1. ---
  2. - block:
  3.   - name: 回滚{{ SERVICE }}至{{ HISTORY }}历史版本
  4.     shell: |
  5.       [[ -{{ dest_package }} ]] && rm -rf {{ dest_package }}/*
  6.       \cp -ra {{ src_package }}/* {{ dest_package }}/
  7.  
  8.   - name: Restart Service
  9.     systemd: name={{ SERVICE }} state=restarted enabled=yes daemon_reload=yes
  10.     become: yes
  11.  
  12.   - name: Wait for {{ SERVER_PORT }} available
  13.     wait_for:
  14.       host: "{{ ansible_default_ipv4.address }}"
  15.       port: "{{ SERVER_PORT }}"
  16.       delay: 5
  17.       timeout: 30
  18.     when: (SERVER_PORT is defined) or (SERVICE_PORT != '')
  19.   delegate_to: "{{ item }}"

 

3)回滚演示

回滚前,我们先看看源文件的MD5

  1. ## 以下为应用服务器
  2. # md5sum /data/service-8888-config/config.jar 
  3. aedebf60226bfa213e256c3602c59669  config.jar
  4.  
  5. # md5sum /data/backup/config/20181113_1505/config.jar 
  6. 6de7651f725133bd74f66873c025aafd  /data/backup/config/20181113_1505/config.jar

执行回滚操作。

再次对比MD5

  1. [stguser@sz-rjy-service-java-aio-24-81 service-8888-config]$ md5sum config.jar 
  2. 6de7651f725133bd74f66873c025aafd  config.jar

可以发现,服务以及回滚到了我们指定的版本

 

服务管理

相同的,我们可以根据前面这个方式,配置一个管理服务的job,用于服务的启停(服务都是注册的systemd)

怎么实现这里就不再阐述了,对于其他的项目(tomcat/nginx)都是类似的,各位可以根据自己实际情况去做出一定的调整。

posted @ 2019-12-30 21:12  lvelvis  阅读(2330)  评论(0编辑  收藏  举报
#####