看着似乎用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).目錄結構
- playbooks/deploy.yml # 入口文件
- roles/deploy
- ├── defaults
- │ └── main.yml # 默認參數
- ├── tasks
- │ ├── backup.yml # 備份應用
- │ ├── common.yml
- │ ├── gray_deploy.yml # 發布應用
- │ ├── main.yml # 主配置
- └── templates
- ├── service.sh.j2 # 服務管理模板
- └── systemd.service.j2 # systemd模板
2).role配置
playbooks/deploy.yml(入口文件)
- ---
- - hosts: "{{ TARGET }}"
- remote_user: "{{ REMOTE_USER }}"
- any_errors_fatal: true
- roles:
- - deploy
defaults/main.yml
- ---
- # defaults file for deploy
- # 獲取項目名稱,JOB_NAME來自jenkins內建參數,可見下文jenkins配置頁說明
- PROJECT: "{{ JOB_NAME.split('_')[-1] }}"
- # 項目路徑
- PROJECT_DIR: "/data"
- # JAVA參數配置項,JAVA_OPTIONS來自jenkins參數化構建,可見下文jenkins配置頁說明
- JAVA_OPTS: "{{ JAVA_OPTIONS | default('-Xmx256m -Xms256m') }}"
- # systemd配置路徑
- SYSTEMD_PATH: "/etc/systemd/system"
- # 應用日志路徑
- LOGPATH: "/log/web"
- # 備份目錄
- BACKUP: "/data/backup/{{ PROJECT }}"
- # jdk version
- # 配和include_role: jdk使用
- jdk:
- version: "{{ version | default('1.8.0_73') }}"
tasks/main.yml(主配置)
- ---
- # 本想引用jdk的role做到預配置jdk環境,但是become下遇到了些bug,如果是秘鑰或者直連,應該也是沒問題的
- #- include_role: name=jdk
- - include_tasks: common.yml
- - include_tasks: backup.yml
- # 這里使用loop循環play_hosts(當前執行的主機),是為了實現一個藍綠,當然,這是主機少的情況,
- # 如果一個應用有N台主機,這效率就很低了,這樣的話,可以考慮設置全局serial來控制每次發布的比例
- # PS: 如果不需要可以把run_once與loop去掉, ab_deploy.yml里的delegate_to去掉
- - include_tasks: ab_deploy.yml
- loop: "{{ play_hosts }}"
- run_once: true
- become: yes
[\d+\.]{3,}\d+
tasks/common.yml(公共配置,預配置環境,創建目錄等)
- ---
- - set_fact:
- BASENAME: "{{ ansible_hostname.split('-')[2] }}-{{ SERVER_PORT }}-{{ PROJECT }}"
- when: (SERVER_PORT is defined) and (SERVER_PORT != "")
- - set_fact:
- BASENAME: "{{ ansible_hostname.split('-')[2] }}-{{ PROJECT }}"
- when: (SERVER_PORT is not defined) or (SERVER_PORT == "")
- - set_fact:
- WORKPATH: "{{ PROJECT_DIR }}/{{ BASENAME }}"
- - name: 檢查 {{ WORKPATH }} 工作路徑
- stat: path={{ WORKPATH }}
- register: work
- - name: 檢查systemd
- stat: path={{ SYSTEMD_PATH }}/{{ PROJECT }}.service
- register: systemd
- - block:
- - name: 創建 {{ WORKPATH }}
- file: path={{ item }} state=directory owner={{ REMOTE_USER }} group={{ REMOTE_USER }} recurse=yes
- with_items:
- - "{{ WORKPATH }}"
- - "{{ LOGPATH }}"
- - name: 推送syetmed模板
- template: src=systemd.service.j2 dest={{ SYSTEMD_PATH }}/{{ PROJECT }}.service
- become: yes
- - name: Local | find package
- find: paths={{ WORKSPACE }} patterns=".*{{ PROJECT.split('-')[0] }}.*\.jar$" age=-60 age_stamp=mtime recurse=yes use_regex=yes
- delegate_to: localhost
- register: target_file
- - assert:
- that:
- - "target_file.files.0.path is defined"
- msg: "未找到構建文件,請檢查構建過程"
- - set_fact:
- package: "{{ target_file.files.0.path }}"
- # 推送管理腳本
- - name: Push script
- template: src=service.sh.j2 dest={{ WORKPATH }}/{{ PROJECT }}.sh mode=0750
tasks/backup.yml(備份應用)
- ---
- - name: 獲取遠程文件信息
- stat:
- path: "{{ WORKPATH }}/{{ PROJECT }}.jar"
- register: history_pkg
- # 獲取一個時間點
- - set_fact: backup_time={{ '%Y%m%d_%H%M' | strftime }}
- - block:
- # 在控制端創建了一個空的目錄?(至於為什么要建一個空的目錄,后面回滾會用到)
- - name: Create local flag
- file: path={{ BACKUP }}/{{ backup_time }} state=directory recurse=yes
- delegate_to: localhost
- run_once: true
- # 在遠程主機創建備份目錄
- - name: Create backup directory
- file:
- path: "{{ BACKUP }}/{{ backup_time }}"
- state: directory
- owner: "{{ REMOTE_USER }}"
- group: "{{ REMOTE_USER }}"
- recurse: yes
- become: yes
- # 備份到遠程主機的本地路徑
- - name: Backup {{ PROJECT }}
- shell: |
- \cp -ra {{ WORKPATH }}/* {{ BACKUP }}/{{ backup_time }}/
- # 遠程文件存在才備份
- when: history_pkg.stat.exists
tasks/ab_deploy.yml(逐台推送打包文件,重啟應用)
- ---
- # 有人可能會問delegate_to為何不寫外層的include_tasks,其實這似乎是不支持或者是bug,
- # include_tasks獲取的執行對象居然是同一個,導致delegate_to+loop只在一台機器上生效
- # 不過我們item寫在block里獲取是正常的,有興趣的童鞋可以試試。
- # PS:不需要ab發布可以去掉delegate_to
- - block:
- # 推送編譯好的jar包
- - name: Push {{ package }} --> {{ WORKPATH }}/{{ PROJECT }}.jar
- copy: src={{ package }} dest={{ WORKPATH }}/{{ PROJECT }}.jar mode=0640
- - name: Restart Service
- systemd: name={{ PROJECT }} state=restarted enabled=yes daemon_reload=yes
- become: yes
- # 等待服務打開端口提供服務,超時30s,注意到,這里只有定義了SERVER_PORT才執行
- # 相對的,你可以走接口或者頁面檢查頁面的狀態碼或者返回內容來做一樣的判斷,
- # 參考模塊: shell,uri,until
- # PS: 不需要ab發布可以去掉delegate_to
- - name: Wait for {{ SERVER_PORT }} available
- wait_for:
- host: "{{ ansible_default_ipv4.address }}"
- port: "{{ SERVER_PORT }}"
- delay: 5
- timeout: 30
- when: SERVER_PORT is defined
- delegate_to: "{{ item }}"
- # 上面的wait失敗后執行的任務,(非必要,要么真的慢,要么是真的沒起來)
- # 這里也可以放其他任務,比如直接fail模塊失敗消息,或者失敗的回滾策略?
- rescue:
- - debug:
- msg: "{{ PROJECT }} {{ SERVER_PORT }} timeout more the 30s!"
templates/service.sh.j2
- #!/bin/bash
- # public func
- source /etc/init.d/functions
- # Env
- source /etc/profile
- # program
- program="{{ PROJECT }}.jar"
- # work path
- work_path={{ WORKPATH }}
- # check --no-daemonize option
- args=$2
- # other args
- {# 這里可以設置很多jinja2的判斷,根據不同模塊,業務配置不同的一些需要的參數 #}
- {# eureka賬戶密碼,非必要,根據自己的業務來吧 #}
- {% if ENV == 'STG' %}
- # eureka賬戶名
- export EUREKA_USER='abc'
- # eureka密碼
- export EUREKA_PASS='123'
- {% endif %}
- # jmx,按需吧。
- # 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"
- # JAVA OPTIONS
- {# 這些參數其實是從jenkins的參數化構建傳入到jenkins的ansible插件里的高級選項里的extra vars傳入的 #}
- {# 這里我們的eureka配置項是環境變量傳入的,各位的方式可能不同,自己斟酌 #}
- {# 判斷是否包含eureka_conf選項,有則為注冊中心配置(eureka_conf也是從jenkins傳入) #}
- {% if eureka_conf is defined %}
- JAVA_OPTS="{{ JAVA_OPTS }} -Dspring.profiles.active={{ eureka_conf }} $JMX_OPTS"
- {% else %}
- JAVA_OPTS="{{ JAVA_OPTS }} $JMX_OPTS"
- {% endif %}
- get_pid() {
- ps -eo pid,cmd | grep java | grep "${program}" | grep -Ev "grep|python" | awk '{print $1}'
- }
- run_program() {
- cd ${work_path}
- if [[ "${args}"x == "--no-daemonize"x ]];then
- # 至於為什么要放在前台執行,是為了將程序交給systemd管理
- java -jar ${JAVA_OPTS} ${program}
- else
- nohup java -jar ${JAVA_OPTS} ${program} &> /dev/null &
- fi
- [[ $? -eq 0 ]] && return 0 || return 1
- }
- run_or_not() {
- pid=$(get_pid)
- if [[ ! ${pid} ]];then
- return 1
- else
- return 0
- fi
- }
- start() {
- run_or_not
- if [[ $? -eq 0 ]];then
- success;echo -e "${program} is running..."
- else
- cd ${work_path} && run_program
- if [[ $? -eq 0 ]];then
- sleep 1
- pid=$(get_pid)
- if [[ "$pid"x != x ]];then
- flag=success
- else
- flag=failure
- $flag;echo -e "Success Start ${program}, but it exists.!"
- exit 1
- fi
- else
- flag=failure
- fi
- $flag;echo -e "[$pid] Start ${program}"
- fi
- }
- stop() {
- run_or_not
- if [[ $? -eq 0 ]];then
- pid=$(get_pid)
- kill -9 $pid
- if [[ $? -eq 0 ]];then
- flag=success
- else
- flag=failure
- fi
- $flag;echo -e "Stop $program"
- else
- $flag;echo -e "$program is not running."
- fi
- }
- status() {
- run_or_not
- if [[ $? -eq 0 ]];then
- pid=$(get_pid)
- success;echo -e "[$pid] $program is running..."
- exit 0
- else
- failure;echo -e "$program not running."
- exit 1
- fi
- }
- case $1 in
- start)
- start
- ;;
- stop)
- stop
- ;;
- status)
- status
- ;;
- restart)
- stop
- sleep 1
- start
- ;;
- *)
- echo "Usage: $0 {start [--no-daemoize]|stop|status|reload|restart}"
- exit 1
- esac
templates/systemd.service.j2
- [Unit]
- Description={{ PROJECT }}
- After=network.target
- [Service]
- {# --no-daemonize這里放在前台啟動了 #}
- ExecStart={{ WORKPATH }}/{{ PROJECT }}.sh start --no-daemonize
- ExecStop={{ WORKPATH }}/{{ PROJECT }}.sh stop
- WorkingDirectory={{ WORKPATH }}
- {# 按需配置重啟策略吧 #}
- #Restart=on-failure
- #RestartSec=30
- User={{ REMOTE_USER }}
- Group={{ REMOTE_USER }}
- RuntimeDirectory={{ PROJECT }}
- RuntimeDirectoryMode=0755
- [Install]
- WantedBy=multi-user.target
注:為何要注冊到systemd呢?一個是統一管理維護(業務環境復雜多樣,go/python/java/.net core有腳本啟動的,有中間件管理啟動的,管理方式層出不窮),以及結合journalctl捕獲控制台的輸出日志
Jenkins結合Ansible的自動發布
1)jenkins依賴插件描述
- Ansible plugin: 執行Ansible所需插件。
- AnsiColor:彩色輸出,非必須
- Build With Parameters:參數化構建需要
- Git plugin:git需要
- JDK Parameter Plugin:自己按需吧
- Mask Passwords Plugin:參數化構建,加密密碼類參數,非必須
- Readonly Parameter plugin:參數化構建里的,只讀參數選項
- Active Choices Plug-in: 動態參數插件,發布不會用到,后面會介紹,有奇用
- Run Selector Plugin:參數化構建,選擇插件
- Git Parameter Plug-In:git分支選擇插件,非必要
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點,應用名稱跟回滾的主機哪里可以獲取到呢?答案是jenkins job里。
我們來看下JENKINS_HOME下對應的job是什么樣的。
- # 進去你的jenkins家目錄
- cd /data/jenkins/
- ls STG_AIO_config/
- builds config.xml lastStable lastSuccessful modules nextBuildNumber
我們通過拆分目錄名STG_AIO_config
可以得到{環境}
,{項目分組}
,{應用名稱}
等信息,而主機信息就在config.xml
的content
字段內!
- # 以下是config.xml的部分配置片段
- <playbook>/etc/ansible/playbooks/deploy.yml</playbook>
- <inventory class="org.jenkinsci.plugins.ansible.InventoryContent">
- <content>[target]
- 10.20.24.81
- 10.20.24.82
- [target:vars]
- ansible_password=${STG_USER_PASS}
- ansible_become_user=root
- ansible_become_method=su
- ansible_become_pass=${ROOT_PASS}
- CFG_SVR_USER=${CFG_SVR_USER}
- CFG_SVR_PASS=${CFG_SVR_PASS}
- CFG_SVR_KEY_PASS=${CFG_SVR_KEY_PASS}
- CFG_SVR_KEY_SECRET=${CFG_SVR_KEY_SECRET}</content>
- <dynamic>false</dynamic>
- </inventory>
- <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能獲取到什么呢?我們看下
- #為了演示效果我新建的PROD,DEV的目錄
- [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/jenkins/jobs && ls |grep -Po '^(?!PRD|APP)[A-Z]+(?=_)' | sort -n | uniq
- DEV
- PROD
- STG
可以知道,我們這里是獲取了多個{環境}
接下來,我們添加第二個參數Active Choices Reactive Parameter
(要引用其他參數)==> GROUP
,Groovy Script里多了一個env=ENV
是為了引入前面獲取的ENV,Choice Type里繼續勾選單選框,
不同的是下面多了一個Referenced parameter
,這里填寫ENV(這個是因為要關聯上面的ENV)
我們代入ENV,看下shell下能獲取到什么呢?
- [root@sz-rjy-ops-ansible-config-23-222 config]# env=STG
- [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/jenkins/jobs && ls -d $env* | grep -Po '(?<=_)[A-Z0-9]+(?=_)' | sort -n | uniq
- AIO
- J
獲取到了{項目分組}
接下來,我們添加第三個動態參數Active Choices Reactive Parameter
(要引用其他參數)==>SERVICE,Groovy Script里多了一個env=ENV,group=GROUP
是為了引入前面獲取的ENV跟GROUP,Choice Type里繼續勾選單選框,
不同的是下面多了一個Referenced parameter
,這里填寫ENV,GROUP(這個是因為要關聯上面的ENV,GROUOP)
我們看下這個shell又能獲取到什么
- [root@sz-rjy-ops-ansible-config-23-222 config]# env=STG
- [root@sz-rjy-ops-ansible-config-23-222 config]# group=AIO
- [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/jenkins/jobs/ && ls -d ${env}_${group}_* | grep -Po "(?<=_)[a-z0-9-].*" | sort
- basic-data
- bootadmin
- ce-system
- config
- gateway
- jobs-acc-executor
- loan-batch
- monitor
- eureka
- zipkin
- 。。。 。。。
這里我們獲取到了{應用名}
最后,我們再配置一個Active Choices Reactive Parameter=
=>HISTORY,用於獲取歷史版本,選擇單選框,關聯SERVICE
參數
我們執行shell試下
- [root@sz-rjy-ops-ansible-config-23-222 config]# service=config
- [root@sz-rjy-ops-ansible-config-23-222 config]# cd /data/backup/$service/ && ls | sort -nr | head -n10
- 20181113_1505
- 20181113_1502
- 20181113_1437
- 20181113_1434
- 20181113_1432
- 20181113_1425
- 20181113_1415
- 20181112_1118
- 20181112_1110
- 20181112_1105
獲取到了歷史的備份點。
作為可選項,我們還可以加個Active Choices Reactive Parameter=
=>INFO的動態參數構建,用於獲取config.xml里的job描述,這里要關聯三個參數ENV,GROUP,SERVICE
shell內執行效果如下:
- [root@sz-rjy-ops-ansible-config-23-222 config]# env=STG
- [root@sz-rjy-ops-ansible-config-23-222 config]# group=AIO
- [root@sz-rjy-ops-ansible-config-23-222 config]# service=config
- [root@sz-rjy-ops-ansible-config-23-222 config]# grep -Po '(?<=<description>).*(?=<)' /data/jenkins/jobs/${env}_${group}_${service}/config.xml | head -n 1
- 配置中心
正確獲取到了描述信息。
我們正確獲取到了{環境},{項目分組},{應用名}
以及{歷史備份點}
等信息,但是還沒有關聯的inventory,既然inventory信息在config.xml中已有,我們可以通過聲明ENV,GROUP,SERVICE
環境變量,再通過腳本獲取這幾個值來拼接出config.xml
所在位置,再通過解析xml來獲取主機host,得到一個ansible動態inventory
,(不單單是host,我們可以在xml里獲取各種我們定義的值來作為inventory vars變量為我們所用!)
我們在jenkins下拉到構建
選項,添加一個Executor shell
我們將ENV,GROUP,SERVICE聲明到了執行的環境變量中,
我們再看看inventory這個腳本是如何獲取的。
- cat /data/script/python/inventory.py
- #!/usr/bin/python
- # -- encoding: utf-8 --
- ## pip install xmltodict ##
- import xmltodict
- import json
- import re
- import os
- # 從環境變量獲取參數
- # 賬號密碼你做了信任就不需要,自己看着辦
- options = {
- 'ENV': os.getenv('ENV'),
- 'GROUP': os.getenv('GROUP'),
- 'SERVICE': os.getenv('SERVICE'),
- 'ACTION': os.getenv('ACTION'),
- 'ansible_user': 'stguser',
- 'ansible_password': 'abc',
- 'ansible_become_pass': '123',
- 'ansible_become_method': 'su',
- 'ansible_become_user': 'root',
- 'ansible_become': True
- }
- def getXml(env,group,service):
- ''' 拼接對應項目的jenkinx config.xml路徑'''
- file = '/data/jenkins/jobs/{}_{}_{}/config.xml'.format(env,group,service)
- return file
- def getData(file):
- data = dict()
- xml = open(file)
- try:
- xmldata = xml.read()
- finally:
- xml.close()
- convertedDict = xmltodict.parse(xmldata)
- # maven2 項目模板數據提取
- if convertedDict.has_key('maven2-moduleset'):
- name = convertedDict['maven2-moduleset']['rootModule']['artifactId']
- _ansi_obj = convertedDict['maven2-moduleset']['postbuilders']['org.jenkinsci.plugins.ansible.AnsiblePlaybookBuilder']
- # 可能有多個playbbok,只要是inventory一樣就無所謂取哪一個(這里取第一個,如果多個不一樣,自己想辦法合並)
- if isinstance(_ansi_obj,list):
- host_obj = _ansi_obj[0]['inventory']['content']
- else:
- host_obj = _ansi_obj['inventory']['content']
- data['hosts'] = re.findall('[\d+\.]{3,}\d+',host_obj)
- # 如果設置了參數化構建,把只讀參數作為ansible參數
- if convertedDict['maven2-moduleset']['properties'].has_key('hudson.model.ParametersDefinitionProperty'):
- parameter_data = convertedDict['maven2-moduleset']['properties']['hudson.model.ParametersDefinitionProperty']['parameterDefinitions']['com.wangyin.ams.cms.abs.ParaReadOnly.WReadonlyStringParameterDefinition']
- # 這里使用的自由風格模板模板,數據結構與maven2不一樣,需要拆開判斷
- if convertedDict.has_key('project'):
- host_obj = convertedDict['project']['builders']['org.jenkinsci.plugins.ansible.AnsiblePlaybookBuilder']['inventory']['content']
- data['hosts'] = re.findall('[\d+\.]{3,}\d+',host_obj)
- # 如果設置了參數化構建,把只讀參數作為ansible參數
- if convertedDict['project']['properties'].has_key('hudson.model.ParametersDefinitionProperty'):
- parameter_data = convertedDict['project']['properties']['hudson.model.ParametersDefinitionProperty']['parameterDefinitions']['com.wangyin.ams.cms.abs.ParaReadOnly.WReadonlyStringParameterDefinition']
- # 插入參數化構建參數(我這里是只讀字符串參數)
- try:
- for parameter in parameter_data:
- data[parameter['name']] = parameter['defaultValue']
- except:
- pass
- #print(json.dumps(convertedDict,indent=4))
- return data
- def returnInventory(xmldata,**options):
- ''' 合並提取的數據,返回inventory的json'''
- inventory = dict()
- inventory['_meta'] = dict()
- inventory['_meta']['hostvars'] = dict()
- inventory[options['SERVICE']] = dict()
- inventory[options['SERVICE']]['vars'] = dict()
- # 合並xmldata提取的數據
- for para_key,para_value in xmldata.items():
- # 單獨把hosts列表提取出來,其他的都丟vars里
- if para_key == 'hosts':
- inventory[options['SERVICE']][para_key] = para_value
- else:
- inventory[options['SERVICE']]['vars'][para_key] = para_value
- # 合並options里的所有東西到vars里
- for opt_key,opt_value in options.items():
- inventory[options['SERVICE']]['vars'][opt_key] = opt_value
- return inventory
- if __name__ == "__main__":
- xmldata = getData(getXml(options['ENV'],options['GROUP'],options['SERVICE']))
- print(json.dumps(returnInventory(xmldata,**options),indent=4))
我們看看執行結果
- [root@sz-rjy-ops-ansible-config-23-222 config]# export ENV=STG GROUP=AIO SERVICE=config
- [root@sz-rjy-ops-ansible-config-23-222 config]# /data/script/python/inventory.py
- {
- "config": {
- "hosts": [
- "10.20.24.81",
- "10.20.24.82"
- ],
- "vars": {
- "ansible_become_method": "su",
- "GROUP": "AIO",
- "SERVER_PORT": "8888",
- "SERVICE": "config",
- "ansible_become_user": "root",
- "ansible_become": true,
- "ansible_user": "stguser",
- "ENV": "STG",
- "ansible_become_pass": "abc",
- "ACTION": null,
- "ansible_password": "123"
- }
- },
- "_meta": {
- "hostvars": {}
- }
- }
可以看到能正常的獲取到一個動態inventory的json了
最后我們看下jenkins里的ansible配置,inventory執行了python腳本,並傳入了一個extra vars 的HISTORY參數
2)Ansible role
目錄結構
- playbooks/spring-rollback.yml # 入口文件
- roles/spring-rollback
- ├── defaults
- │ └── main.yml # 默認參數
- ├── README.md
- └── tasks
- ├── common.yml # 公共配置
- ├── main.yml # 主配置
- ├── rollback.yml # 回滾任務
playbooks/spring-rollback.yml
- ---
- - hosts: all
- pre_tasks:
- - assert:
- that:
- - "HISTORY != ''"
- fail_msg: '請選擇一個正確的歷史版本!'
- roles:
- - spring-rollback
defaults/main.yml
- ---
- # defaults file for rollback
- # 備份路徑
- BACKUP: "/data/backup/{{ SERVICE }}/{{ HISTORY }}"
- OWNER: stguser
tasks/main.yml
- ---
- # tasks file for rollback
- - include_tasks: common.yml
- - include_tasks: rollback.yml
- loop: "{{ play_hosts }}"
- run_once: true
- become: yes
tasks/common.yml
- ---
- - shell: "ls -d /data/*{{ SERVICE }}"
- register: result
- - set_fact:
- src_package: "{{ BACKUP }}"
- dest_package: "{{ result.stdout }}"
tasks/rollback.yml
- ---
- - block:
- - name: 回滾{{ SERVICE }}至{{ HISTORY }}歷史版本
- shell: |
- [[ -d {{ dest_package }} ]] && rm -rf {{ dest_package }}/*
- \cp -ra {{ src_package }}/* {{ dest_package }}/
- - name: Restart Service
- systemd: name={{ SERVICE }} state=restarted enabled=yes daemon_reload=yes
- become: yes
- - name: Wait for {{ SERVER_PORT }} available
- wait_for:
- host: "{{ ansible_default_ipv4.address }}"
- port: "{{ SERVER_PORT }}"
- delay: 5
- timeout: 30
- when: (SERVER_PORT is defined) or (SERVICE_PORT != '')
- delegate_to: "{{ item }}"
3)回滾演示
回滾前,我們先看看源文件的MD5
- ## 以下為應用服務器
- # md5sum /data/service-8888-config/config.jar
- aedebf60226bfa213e256c3602c59669 config.jar
- # md5sum /data/backup/config/20181113_1505/config.jar
- 6de7651f725133bd74f66873c025aafd /data/backup/config/20181113_1505/config.jar
執行回滾操作。
再次對比MD5
- [stguser@sz-rjy-service-java-aio-24-81 service-8888-config]$ md5sum config.jar
- 6de7651f725133bd74f66873c025aafd config.jar
可以發現,服務以及回滾到了我們指定的版本
服務管理
相同的,我們可以根據前面這個方式,配置一個管理服務的job,用於服務的啟停(服務都是注冊的systemd)
怎么實現這里就不再闡述了,對於其他的項目(tomcat/nginx)都是類似的,各位可以根據自己實際情況去做出一定的調整。