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)都是類似的,各位可以根據自己實際情況去做出一定的調整。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM