概述
在CI/CD流程中,项目的可持续迭代,版本回档都是十分重要的环节。而网络资源中看着似乎基于jenkins+ansible发布spring boot/cloud类的jar包程序,或者tomcat下的war包的需求
发布流程

规范与标准
无规矩不成方圆,要做好后期的自动化,标准化是少不了的,下面是我们这边规划的一些标准(非强制,根据自己实际情况调整)
- 应用名称:
{应用类型}-{端口}-{应用名称}
or{类型}-{应用名称}
, 例如:web-8080-gateway
,app-jobs-platform
- 主机名称:
{地区}-{机房}-{应用类型}-{代码语言}-{项目分组}-{ip结尾}
, 例如:sz-rjy-service-java-foo-14-81
- 日志路径:
/log/web,
例如:/log/w
版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作! eb/web-8080-gateway - 代码路径:
/data/,
例如:/data/web-8080-gateway
- Tomcat安装路径:
/usr/local/tomcat
- 实例路径:
/usr/local/tomcat/{应用名称}
- Jenkins job命名: {
环境
}_{项目分组
}_{应用名称
},如:TEST_GROUP1_web-8080-nc
应用配置:存放于git中,建立了各个环境相应的目录,环境目录下对应应用名称的同名文件,如:
[email protected]:devops-conf/conf.git
|
|__UAT
| |__web-8080-nc
| |__web-8081-wechat
|
|__STG
| |__web-8080-nc
| |__web-8081-wechat
... ...
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分支选择插件>非必须
- PostBuildScript Plugin : 可在构建后根据构建结果运行多个可配置的操作>非必须
环境配置
1). 软件版本
- Ansible: 2.7.1
- Python: 2.7.5
- CentOS: 7.2
- Java: 1.8.0_73
- Jenkins: 2.121.1
2).环境/软件安装
发布任务配置
1. Jenkins Job配置

SVN拉取配置

maven构建配置

Ansible插件配置

2. Ansible Role配置
Role结构
1)代码结构
playbooks
└──test-tomcat-deploy.yml # 入口文件
roles
└──tomcat-deploy-test
├── defaults
│ └── main.yml # 默认变量
├── files
│ ├── conf # 实例配置文件
│ │ ├── catalina.policy
│ │ ├── catalina.properties
│ │ ├── context.xml
│ │ ├── logging.properties
│ │ ├── tomcat-users.xml
│ │ ├── tomcat-users.xsd
│ │ └── web.xml
│ └── tomcat.tar.gz # tomcat安装文件(没做任何调整,就是改名成tomcat后压缩了一下)
├── handlers
│ └── main.yml # handlers任务
├── tasks
│ ├── backup.yml # 备份任务
│ ├── commons.yml # 一般任务
│ ├── deploy.yml # 部署任务
│ ├── init.yml # 初始化任务
│ └── main.yml # 主文件
└── templates
├── catalina.sh.j2 # 启动脚本
├── env.j2 # 对于实例的描述文件,非必要
├── server.xml.j2 # 实例的server.xml配置
└── systemd.service.j2 # systemd服务脚本
2)Role配置文件说明
① .playbooks/test-tomcat-deploy.yml
---
- hosts: target
roles:
- { role: tomcat-deploy-test, tags: deploy }
② .defaults/main.yml
---
# base info
# JOB_NAME等信息直接从环境变量读取,而不是之前那样通过exra vars导入,少了很多不必要的参数填写
JOB_NAME: "{{ lookup('env','JOB_NAME') }}"
TOMCAT_HOME: "/usr/share/tomcat"
# 将JOB_NAME切割出各个信息
PROJECT: "{{ JOB_NAME.split('_')[-1] }}"
CODE_DEPTH_PATH: "{{ code_depth_path | default('') }}"
PROJECT_BASE: "/data/{{ PROJECT }}"
ENV: "{{ JOB_NAME.split('_')[0] }}"
# project info
project:
public:
JAVA_HOME: "{{ java_home | default('') }}"
GIT_CONF_REPO: "[email protected]:devops/conf.git"
JAVA_OPTS: "{{ lookup('env','JAVA_OPTS') | default('-server -Xms2048M -Xmx2048M') }}"
local:
# 备份信息日志路径
deploy_release_log: "/data/deploy_release_log/{{ ENV }}/{{ PROJECT }}"
# 构建后的代码路径
CODE_PATH: "{{ local_code_path | default('') }}"
target:
# 实例相关配置
CATALINA_HOME: "{{ TOMCAT_HOME }}"
CATALINA_BASE: "/usr/local/tomcat/{{ PROJECT }}"
CATALINA_LOGBASE: "/usr/local/tomcat/{{ PROJECT }}/logs"
#tomcat虚拟路径,默认为空
TOMCAT_VIRTUAL_PATH: "{{ lookup('env','tomcat_virtual_path') }}"
CODE_PATH: "{{ PROJECT_BASE }}/{{ CODE_DEPTH_PATH }}"
TOMCAT_USER: "{{ tomcat_user | default('tomcat') }}"
TOMCAT_PORT: "{{ PROJECT.split('-')[1] }}"
# 备份目录
HISTORY_ARCHIVR_DIR: "/data/deploy_release_library/{{ PROJECT }}"
# 应用日志(不是必须的)
LOGPATH: "/log/{{ PROJECT.split('-')[0] }}/{{ PROJECT }}"
③ .files/conf(直接从tomcat二进制包里的conf原样复制过来即可)
④ .tasks/main.yml
---
# 打印变量信息
- name: show project info
debug: var=project
failed_when: (project is not defined)
# 初始化实例
- include_tasks: "init.yml"
# 一般任务
- include_tasks: "commons.yml"
# 备份任务,存在代码才备份
- include_tasks: "backup.yml"
when: (code_status.rc == 0)
# 发布任务
- include_tasks: "deploy.yml"
⑤ .tasks/init.yml
---
# 创建运行账户
- name: Create {{ project.target.TOMCAT_USER }} user
user: name={{ project.target.TOMCAT_USER }}
# 检查tomcat是否安装
- name: Verify that tomcat installed
command: "{{ TOMCAT_HOME }}/bin/catalina.sh version"
environment:
JAVA_HOME: "{{ project.public.JAVA_HOME }}"
register: check_install
failed_when: false
# 没装就直接解压过去
- name: Install tomcat
unarchive:
src: tomcat.tar.gz
dest: /usr/share
owner: "{{ project.target.TOMCAT_USER }}"
group: "{{ project.target.TOMCAT_USER }}"
when: check_install.rc != 0
# 创建实例目录接口(file循环我嫌慢,shell来的快)
- name: Create {{ PROJECT }} directory
shell: |
mkdir -p {{ project.target.CATALINA_BASE }}/{conf,work,bin,webapps,temp,logs,lib} {{ project.target.LOGPATH }} {{ project.target.CODE_PATH }} &>/dev/null
chown {{ project.target.TOMCAT_USER }}. -R {{ project.target.CATALINA_BASE }} {{ project.target.LOGPATH }} {{ project.target.CODE_PATH }}
# 推送实例模板文件
- name: Push {{ PROJECT }} templates
template:
src: "{{ item.src }}"
dest: "{{ item.dest }}"
mode: "{{ item.mode }}"
owner: "{{ project.target.TOMCAT_USER }}"
group: "{{ project.target.TOMCAT_USER }}"
loop:
- { src: server.xml.j2, dest: "{{ project.target.CATALINA_BASE }}/conf/server.xml", mode: 644 }
- { src: catalina.sh.j2, dest: "{{ project.target.CATALINA_BASE }}/bin/catalina.sh", mode: 'u=rwx,g=rx,o=r' }
- { src: env.j2, dest: "{{ project.target.CATALINA_BASE }}/env", mode: 644 }
# 推送实例一般配置文件
- name: Push Tomcat config
synchronize:
src: conf/
dest: "{{ project.target.CATALINA_BASE }}/conf"
# CentOS7使用systemd管理服务
- name: Copy Systemd script
template:
src: systemd.service.j2
dest: /etc/systemd/system/{{ PROJECT }}.service
mode: 0644
when: ansible_distribution_major_version|int >= 7
# CentOS6使用SysV脚本
- block:
- name: Link to Sysvinit
file:
src: "{{ project.target.CATALINA_BASE }}/bin/catalina.sh"
dest: "/etc/init.d/{{ PROJECT }}"
state: link
force: yes
- name: Add to chkconfig
shell: chkconfig --add {{ PROJECT }}
when: ansible_distribution_major_version|int < 7
⑥ .tasks/commons.yml
---
# 设置变量,time相关的不要放入defaults里,因为include是动态取的,会有变化
- set_fact:
deploy_time: "{{ lookup('pipe','date +%Y%m%d%H') }}"
# 这里使用lookup获取本地时间,而不是strftime去取,因为客户端时间可能不一致
git_conf_tmp_path: "/tmp/.config/{{ lookup('pipe', 'date +%s') }}"
# 拉取git里存放的配置到临时目录
- name: Git conf
git:
repo: "{{ project.public.GIT_CONF_REPO }}"
dest: "{{ git_conf_tmp_path }}"
ssh_opts: '-o StrictHostKeyChecking=no'
delegate_to: localhost
run_once: true
# 检查应用代码是否为空
- name: Verify remote code exists
shell: ls {{ project.target.CODE_PATH }}/* &> /dev/null
register: code_status
failed_when: false
⑦ .tasks/backup.yml
---
# 创建备份目录
- name: Create Histroy Archive Directory
file:
dest: "{{ project.target.HISTORY_ARCHIVR_DIR }}/{{ deploy_time }}"
state: directory
owner: "{{ project.target.TOMCAT_USER }}"
group: "{{ project.target.TOMCAT_USER }}"
recurse: yes
# 压缩文件到备份目录下
- block:
- name: Compress and backup files
archive:
path:
- "{{ PROJECT_BASE }}/*"
dest: "{{ HISTORY_ARCHIVR_DIR }}/{{ deploy_time }}/{{ PROJECT }}.zip"
exclude_path:
- "{{ PROJECT_BASE }}/log"
- "{{ PROJECT_BASE }}/logs"
format: zip
owner: "{{ RUN_USER }}"
group: "{{ RUN_USER }}"
- name: Write Deploy release log
blockinfile:
path: "{{ project.local.deploy_release_log }}/{{ deploy_time }}"
block: |-
{{ release_log | to_nice_json(indent=2) }}
create: yes
delegate_to: localhost
run_once: true
vars:
release_log:
target: "{{ ansible_play_hosts | join(':') }}"
project: "{{ PROJECT }}"
run_user: "{{ RUN_USER }}"
backup_dir: "{{ HISTORY_ARCHIVR_DIR }}/{{ deploy_time }}"
backup_file: "{{ HISTORY_ARCHIVR_DIR }}/{{ deploy_time }}/{{ PROJECT }}.zip"
project_dir: "{{ PROJECT_BASE }}"
backup_time: "{{ deploy_time }}"
env: "{{ ENV }}"
# 备份该时间段的第一次,一小时内的不重复备份,防止破坏备份文件稳定性
when: not lookup('pipe','ls ' + project.local.deploy_release_log + '/' + deploy_time + ' &> /dev/null', errors='ignore')
⑧ .tasks/deploy.yml
---
# 同步构建后的代码
- name: "Sync {{ project.local.CODE_PATH }} >> {{ project.target.CODE_PATH }}"
synchronize:
src: "{{ project.local.CODE_PATH }}/"
dest: "{{ project.target.CODE_PATH }}"
delete: yes
# 同步git拉取到临时目录的配置文件,同事触发handler,不同系统触发不同的hander任务重启
- name: "Sync {{ git_conf_tmp_path }}/{{ ENV }}/{{ PROJECT }}/ >> {{ PROJECT_BASE }}"
synchronize:
src: "{{ git_conf_tmp_path }}/{{ ENV }}/{{ PROJECT }}/"
dest: "{{ PROJECT_BASE }}"
notify:
- "{{ ansible_distribution_major_version }} Restart Tomcat"
# 设置属主
- name: Set owner >> "{{ project.target.TOMCAT_USER }}"
file:
path: "{{ PROJECT_BASE }}"
recurse: yes
state: directory
owner: "{{ project.target.TOMCAT_USER }}"
group: "{{ project.target.TOMCAT_USER }}"
# 删除临时文件
- name: Remove local config
file:
path: "{{ git_conf_tmp_path }}"
state: absent
delegate_to: localhost
run_once: true
⑨ .handlers/main.yml
---
- name: 7 Restart Tomcat
systemd: name={{ PROJECT }} state=restarted enabled=yes daemon_reload=yes
# 因为我们是root过去的,6指定下become到运行账户启动
- name: 6 Restart Tomcat
service: name={{ PROJECT }} pattern=/etc/init.d/{{ PROJECT }} state=restarted sleep=5 enabled=yes
become_user: "{{ project.target.TOMCAT_USER }}"
become: yes
⑩ .templates/server.xml.j2
<?xml version='1.0' encoding='utf-8'?>
<!--
Licensed to the Apache Software Foundation (ASF) under one or more
contributor license agreements. See the NOTICE file distributed with
this work for additional information regarding copyright ownership.
The ASF licenses this file to You under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with
the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
-->
<!-- Note: A "Server" is not itself a "Container", so you may not
define subcomponents such as "Valves" at this level.
Documentation at /docs/config/server.html
-->
<Server port="{{ project.target.TOMCAT_PORT|int + 1000 }}" shutdown="SHUTDOWN">
<Listener className="org.apache.catalina.startup.VersionLoggerListener" />
<!-- Security listener. Documentation at /docs/config/listeners.html
<Listener className="org.apache.catalina.security.SecurityListener" />
-->
<!--APR library loader. Documentation at /docs/apr.html -->
<Listener className="org.apache.catalina.core.AprLifecycleListener" SSLEngine="off" />
<!-- Prevent memory leaks due to use of particular java/javax APIs-->
<Listener className="org.apache.catalina.core.JreMemoryLeakPreventionListener" />
<Listener className="org.apache.catalina.mbeans.GlobalResourcesLifecycleListener" />
<Listener className="org.apache.catalina.core.ThreadLocalLeakPreventionListener" />
<!-- Global JNDI resources
Documentation at /docs/jndi-resources-howto.html
-->
<GlobalNamingResources>
<!-- Editable user database that can also be used by
UserDatabaseRealm to authenticate users
-->
<Resource name="UserDatabase" auth="Container"
type="org.apache.catalina.UserDatabase"
description="User database that can be updated and saved"
factory="org.apache.catalina.users.MemoryUserDatabaseFactory"
pathname="conf/tomcat-users.xml" />
</GlobalNamingResources>
<!-- A "Service" is a collection of one or more "Connectors" that share
a single "Container" Note: A "Service" is not itself a "Container",
so you may not define subcomponents such as "Valves" at this level.
Documentation at /docs/config/service.html
-->
<Service name="Catalina">
<!--The connectors can use a shared executor, you can define one or more named thread pools-->
<!--
<Executor name="tomcatThreadPool" namePrefix="catalina-exec-"
maxThreads="150" minSpareThreads="4"/>
-->
<!-- A "Connector" represents an endpoint by which requests are received
and responses are returned. Documentation at :
Java HTTP Connector: /docs/config/http.html (blocking & non-blocking)
Java AJP Connector: /docs/config/ajp.html
APR (HTTP/AJP) Connector: /docs/apr.html
Define a non-SSL/TLS HTTP/1.1 Connector on port 8080
-->
<Connector port="{{ project.target.TOMCAT_PORT }}"
protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="3000" minSpareThreads="500"
acceptCount="1000" maxKeepAliveRequests="1"
connectionTimeout="20000" enableLookups="false"
URIEncoding="UTF-8" redirectPort="8443" />
<!-- A "Connector" using the shared thread pool-->
<!--
<Connector executor="tomcatThreadPool"
port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
-->
<!-- Define a SSL/TLS HTTP/1.1 Connector on port 8443
This connector uses the NIO implementation that requires the JSSE
style configuration. When using the APR/native implementation, the
OpenSSL style configuration is required as described in the APR/native
documentation -->
<!--
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol"
maxThreads="150" SSLEnabled="true" scheme="https" secure="true"
clientAuth="false" sslProtocol="TLS" />
-->
<!-- Define an AJP 1.3 Connector on port 8009
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" /> -->
<!-- An Engine represents the entry point (within Catalina) that processes
every request. The Engine implementation for Tomcat stand alone
analyzes the HTTP headers included with the request, and passes them
on to the appropriate Host (virtual host).
Documentation at /docs/config/engine.html -->
<!-- You should set jvmRoute to support load-balancing via AJP ie :
<Engine name="Catalina" defaultHost="localhost" jvmRoute="jvm1">
-->
<Engine name="Catalina" defaultHost="localhost">
<!--For clustering, please take a look at documentation at:
/docs/cluster-howto.html (simple how to)
/docs/config/cluster.html (reference documentation) -->
<!--
<Cluster className="org.apache.catalina.ha.tcp.SimpleTcpCluster"/>
-->
<!-- Use the LockOutRealm to prevent attempts to guess user passwords
via a brute-force attack -->
<Realm className="org.apache.catalina.realm.LockOutRealm">
<!-- This Realm uses the UserDatabase configured in the global JNDI
resources under the key "UserDatabase". Any edits
that are performed against this UserDatabase are immediately
available for use by the Realm. -->
<Realm className="org.apache.catalina.realm.UserDatabaseRealm"
resourceName="UserDatabase"/>
</Realm>
<Host name="localhost" appBase="{{ PROJECT_BASE }}"
unpackWARs="true" autoDeploy="true">
<!-- SingleSignOn valve, share authentication between web applications
Documentation at: /docs/config/valve.html -->
<!--
<Valve className="org.apache.catalina.authenticator.SingleSignOn" />
-->
<!-- Access log processes all example.
Documentation at: /docs/config/valve.html
Note: The pattern used is equivalent to using pattern="common" -->
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />
{#-
这是是虚拟路径的配置,只有指定了tomcat_virtual_path才生效,
默认TOMCAT_VIRTUAL_PATH为空,tomcat_vittual_path做为jenkins参数化构建传递,
格式为: /path:/docBase,比如,
项目名称为web-8080-static,代码路径为/data/web-8080-nc/static,那么格式如下:
/static:/static
多个路径格式为:
/static:/static,/nc:/nc
-#}
{% if project.target.TOMCAT_VIRTUAL_PATH != "" %}
{% for vpath in project.target.TOMCAT_VIRTUAL_PATH.split(',') %}
<Context path="{{ vpath.split(':')[0] }}" docBase="{{ PROJECT_BASE }}{{ vpath.split(':')[1] }}" reloadable="true" crossContext="true"></Context>
{% endfor %}
{% endif %}
<!--<Context path="" docBase="{{ PROJECT_BASE }}" reloadable="true" crossContext="true"></Context>-->
</Host>
</Engine>
</Service>
</Server>
⑪ .templates/catalina.sh.j2
#!/bin/bash
# Provides: {{ PROJECT }}
# chkconfig: - 55 25
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Short-Description: Start Tomcat.
# Description: Start the Tomcat servlet engine.
### END INIT INFO
# Source function library.
. /etc/rc.d/init.d/functions
program="{{ PROJECT }}"
export TOMCAT_USER="{{ project.target.TOMCAT_USER }}"
export CATALINA_PID="{{ project.target.CATALINA_BASE }}/bin/$program.pid"
export TOMCAT_HOME={{ TOMCAT_HOME }}
export CATALINA_HOME=$TOMCAT_HOME
export CATALINA_BASE={{ project.target.CATALINA_BASE }}
export CATALINA_LOGBASE={{ project.target.CATALINA_LOGBASE }}
export JAVA_OPTS="{{ project.public.JAVA_OPTS }} -Dcatalina.logbase=$CATALINA_LOGBASE -Djava.security.egd=file:/dev/./urandom"
{% if project.public.JAVA_HOME != '' %}
export JAVA_HOME={{ project.public.JAVA_HOME }}
{% else %}
source /etc/profile
{% endif %}
# Set Tomcat environment.
export CLASSPATH=.:$JAVA_HOME/lib/dt.jar:$JAVA_HOME/lib/tools.jar:$CATALINA_HOME
export CATALINA_OUT=$CATALINA_LOGBASE/catalina.out
export PATH=$PATH:$JAVA_HOME/bin:/usr/bin:/usr/lib/bin
# locale环境,防止中文写日志乱码问题
export LANG=en_US.UTF-8
export LC_CTYPE="en_US.UTF-8"
export LC_NUMERIC="en_US.UTF-8"
export LC_TIME="en_US.UTF-8"
export LC_COLLATE="en_US.UTF-8"
export LC_MONETARY="en_US.UTF-8"
export LC_MESSAGES="en_US.UTF-8"
export LC_PAPER="en_US.UTF-8"
export LC_NAME="en_US.UTF-8"
export LC_ADDRESS="en_US.UTF-8"
export LC_TELEPHONE="en_US.UTF-8"
export LC_MEASUREMENT="en_US.UTF-8"
export LC_IDENTIFICATION="en_US.UTF-8"
export LC_ALL=
# cache
cache_dir="${CATALINA_BASE}/work"
# check --no-daemonize option
args=$2
# sctipt
catalinaScript="${TOMCAT_HOME}/bin/catalina.sh"
get_pid() {
ps -eo pid,cmd | grep java | grep "${CATALINA_BASE}" | grep -Pv "grep|python" | awk '{print $1}'
}
run_program() {
if [[ "${args}"x == "--no-daemonize"x ]];then
${catalinaScript} start 2>&1 | tee -a $CATALINA_OUT
tailf $CATALINA_OUT
else
nohup ${catalinaScript} start &>> $CATALINA_OUT
return $?
fi
}
run_or_not() {
pid=$(get_pid)
if [[ ! ${pid} ]];then
return 1
else
return 0
fi
}
start() {
run_or_not
if [[ $? -eq 0 ]];then
pid=$(get_pid)
success;echo -e "[\e[0;32m${pid}\e[0m] Tomcat ${program} is running..."
else
echo -n 'Start Tomcat.'
run_program
if [[ $? -eq 0 ]];then
sleep 5
pid=$(get_pid)
if [[ "$pid"x != x ]];then
flag=success
else
flag=failure
$flag;echo -e "Success Start Tomcat ${program}, but it exists.!"
return 1
fi
else
flag=failure
fi
$flag;echo -e "[\e[0;32m$pid\e[0m] Start Tomcat ${program}"
fi
}
stop() {
run_or_not
if [[ $? -eq 0 ]];then
${catalinaScript} stop |& tee -a $CATALINA_OUT
sleep 5
run_or_not
if [[ $? -eq 0 ]];then
pid=$(get_pid)
kill -9 $pid
echo -e "Stop Failed...Killing Process [\e[0;32m$pid\e[0m]..."
fi
success;echo -e "Stop Tomcat $program"
else
failure;echo -e "Tomcat $program is not running."
fi
}
status() {
run_or_not
if [[ $? -eq 0 ]];then
pid=$(get_pid)
success;echo -e "[\e[0;32m${pid}\e[0m] Tomcat $program is running..."
return 0
else
failure;echo -e "Tomcat $program not running."
return 1
fi
}
case $1 in
start)
start
;;
stop)
stop
;;
status)
status
;;
restart)
stop
start
;;
*)
echo "Usage: $0 {start [--no-daemonize]|stop|status|restart}"
return 1
esac
⑫ .templates/systemd.service.j2
[Unit]
Description={{ PROJECT }}
After=network.target
[Service]
ExecStart={{ project.target.CATALINA_BASE }}/bin/catalina.sh start --no-daemonize
ExecStop={{ project.target.CATALINA_BASE }}/bin/catalina.sh stop
#WorkingDirectory={{ project.target.CATALINA_BASE }}
#Restart=on-failure
#RestartSec=30
User={{ project.target.TOMCAT_USER }}
Group={{ project.target.TOMCAT_USER }}
[Install]
WantedBy=multi-user.target
⑬ .templates/env.j2(描述信息而已)
project_name="{{ PROJECT }}"
tomcat_port="{{ project.target.TOMCAT_PORT }}"
control_port="{{ project.target.TOMCAT_PORT|int + 1000 }}"
project_base="{{ PROJECT_BASE }}"
log_base="{{ project.target.CATALINA_LOGBASE }}"
last_update="{{ '%Y-%m-%d %H:%M' | strftime }}"
发布演示
大部分的工作都OK了,由于我们没有沿用之前jenins中账号密码的方式去验证,所以我们需要先做ssh key的信任,这里提供一个playbooks
ssh_keys.yml
---
- hosts: "{{ target }}"
gather_facts: false
tasks:
- name: Create user if not exists
user: name={{ luser }} shell=/bin/bash generate_ssh_key=yes
register: local
delegate_to: localhost
- name: Ensure remote user exists
user: name={{ ruser }} shell=/bin/bash
- name: Write key to authorized_keys
authorized_key:
user: "{{ ruser }}"
state: present
key: "{{ local.ssh_public_key }}"
manage_dir: yes
运行方式:
ansible-playbook ssh_keys.yml -e 'target=你远程的服务器 luser=jenkins ruser=root' -u root -k
# 其中target是你远程服务器的地址,luser是你jenkins运行账户,ruser是远程账户
发布预览:

查看服务:

查看备份日志

查看备份文件:

回滚概述
基于前面发布时做了备份处理,那么这里说说怎么做回滚。我们知道,备份是很简单的,但是怎么去根据策略动态去回滚到我们备份的历史版本呢?
在这之前,我们先考虑一个问题,回滚需要哪些东西?
- 备份文件:备份的代码是基础,备份都没有怎么谈回滚?
- 备份的路径:需要获取的备份代码的所在位置
- 备份的还原点:即这是哪一次的备份,归档应该至少有一份
- 代码的所在路径:备份的代码需要还原到哪里去
- 应用的所在服务器地址:我们需要在哪台机器上做回滚的操作
- 应用名称:我们需要回滚哪个应用,一台服务器也可能有多个应用
- 还原的环境:我们所还原的应用是哪个环境的
- 应用的类型:我们回滚后可能会需要做重启服务的操作,不同类型的服务启动方式千差万别,所以还可能需要描述一个类型去区分启动方式(方便的是我们服务都写入了systemd)
- 启动账户:通过什么用户去启动应用
我们在备份时候写入了一个备份日志,日志的内容大概如下:
# cat 2019053018
# BEGIN ANSIBLE MANAGED BLOCK
{
"backup_dir": "/data/deploy_release_library/web-8080-nc/2019053018",
"backup_file": "/data/deploy_release_library/web-8080-nc/2019053018/web-8080-nc.zip",
"backup_time": "2019053018",
"env": "UAT",
"project": "web-8080-nc",
"project_dir": "/data/web-8080-nc",
"run_user": "devops",
"target": "10.18.4.50"
}
# END ANSIBLE MANAGED BLOC
可以看到,这个日志里有我们需要的大部分内容,接下来就好办了,只要我们通过Ansible动态读取到这个文件,就能根据这个JSON的东西来做应用回滚。
回滚流程

回滚任务配置
Jenkins Job的配置
这里我们使用Active choice
来读取$JENKINS_HOME/jobs
下的名称,拆分出相应的内容来拼接出环境
,分组名称
,以及应用名称
,并到/data/deploy_release_log
下动态读取出备份日志传递给Ansible.
1.获取环境名称

2.获取分组名称

3.获取应用名称

4.从归档目录里获取备份日志列表

5.获取应用名称的描述信息(可选)

6.Ansible Plugin配置

点开高级,附加参数里,添加指向到备份日志的extra vars
(通过拼接变量名)

Role配置文件说明
playbooks
└──manager-rollback.yml # 入口文件
roles
└──manager-rollback
├── defaults
│ └── main.yml # 默认变量
├── README.md
└── tasks
├── other-server.yml # 其他服务的启动方式(之前说的,可能有些服务启动方式一样)
├── general.yml # 一般程序的启动(我这里是指注册到systemd里服务)
└── main.yml # 主task文件
① .manager-rollback.yml
---
# target通过备份日志里的json获取
- hosts: "{{ target }}"
# 直接root去操作
remote_user: root
roles:
- manager-rollbac
② .defaults/main.yml
---
# defaults file for rollback
# 备份日志(那个json)里如果指定了app_type,否则全部当一般应用
APP_TYPE: "{{ app_type | default('general') }}"
③ .tasks/main.yml
---
# 有些系统可能没装解压软件
- name: Install unzip
yum: name=unzip
# 将备份文件解压到项目路径下覆盖
- name: Rollback {{ project }} to {{ backup_time }}
unarchive:
src: "{{ backup_file }}"
dest: "{{ project_dir }}"
owner: "{{ run_user }}"
group: "{{ run_user }}"
remote_src: yes
# 判断应用类型,默认调用general
- include: "{{ APP_TYPE }}.yml"
④ .tasks/general.yml
---
# 注册到systemd的一般服务启动
# CentOS7通过systemd启动服务
- name: 7 Restart service
systemd: name={{ project }} state=restarted
when: ansible_distribution_major_version|int >= 7
# CentOS6通过service启动服务
- name: 6 Restart service
service: name={{ project }} pattern=/etc/init.d/{{ project }} state=restarted sleep=3
when: ansible_distribution_major_version|int < 7
become_user: "{{ run_user }}"
become: yes
⑤ .tasks/other-server.yml(这里是个示例,可以自己拓展)
---
# 这里是一个示例,备份日志里还写入了该程序的启动脚本在哪
# manager_script也是插入到了备份日志里。
- name: Restart GciTask
shell: bash {{ manager_script }} -t all -a restart
args:
chdir: "{{ manager_script | dirname}}"
become_user: "{{ run_user }}"
become: yes
回滚演示
首先,我们进入应用路径/data/web-8080-nc/nc下,修改一个文件,我们插入了一段注释

接下来,我们回滚到昨天的版本

我们看下文件是否还原

可以看到,文件正确的还原了!而且服务也正常的在启动中
说在最后的话
其实我们不管是tomcat还是其他类型的应用,备份回滚策略都大同小异,另外我们在填充备份日志的时候可以添加任何自己需要的字段,然后传递给Ansible做各种各样的判断和后续操作,十分的方便!本章也到此结束,如果有什么需要改进或者建议,