zoukankan      html  css  js  c++  java
  • 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)都是类似的,各位可以根据自己实际情况去做出一定的调整。

  • 相关阅读:
    No module named _tkinter
    Camera2与TextureView使用
    Collections常用方法总结
    Android插件化框架
    《战狼2》观后感——民族荣耀
    《茶马古道》观后感——朝圣之路
    点击查看大图Activity
    图片压缩代码
    《天那边》观后感——对一些现象的反思
    recyclerView的使用
  • 原文地址:https://www.cnblogs.com/lvcisco/p/12121762.html
Copyright © 2011-2022 走看看