Linux Note / 运维笔记

Jenkins+Ansible 持续集成与自动发布

Einic Yeo · 6月2日 · 2019年 ·

概述

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

发布流程

规范与标准

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

  • 应用名称{应用类型}-{端口}-{应用名称} or {类型}-{应用名称}, 例如:web-8080-gateway,app-jobs-platform
  • 主机名称{地区}-{机房}-{应用类型}-{代码语言}-{项目分组}-{ip结尾}, 例如:sz-rjy-service-java-foo-14-版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!81
  • 日志路径/log/web,例如:/log/web/web-8080-gate版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!way
  • 代码路径/data/,例如:/data/web-8080-gateway
  • Tomcat安装路径/usr/local/tomcat
  • 实例路径/usr/local/tomcat版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!/{应用名称}
  • 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依赖插件

环境配置

  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 }}"

 版权声明:本文遵循 CC 4.0 BY-SA 版权协议,若要转载请务必附上原文出处链接及本声明,谢谢合作!   ③ .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是远程账户

发布预览:

查看服务:

查看备份日志

查看备份文件:

回滚概述

基于前面发布时做了备份处理,那么这里说说怎么做回滚。我们知道,备份是很简单的,但是怎么去根据策略动态去回滚到我们备份的历史版本呢?

在这之前,我们先考虑一个问题,回滚需要哪些东西?

  1. 备份文件:备份的代码是基础,备份都没有怎么谈回滚?
  2. 备份的路径:需要获取的备份代码的所在位置
  3. 备份的还原点:即这是哪一次的备份,归档应该至少有一份
  4. 代码的所在路径:备份的代码需要还原到哪里去
  5. 应用的所在服务器地址:我们需要在哪台机器上做回滚的操作
  6. 应用名称:我们需要回滚哪个应用,一台服务器也可能有多个应用
  7. 还原的环境:我们所还原的应用是哪个环境的
  8. 应用的类型:我们回滚后可能会需要做重启服务的操作,不同类型的服务启动方式千差万别,所以还可能需要描述一个类型去区分启动方式(方便的是我们服务都写入了systemd)
  9. 启动账户:通过什么用户去启动应用

我们在备份时候写入了一个备份日志,日志的内容大概如下:

# 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的东西来做应用回滚。

回滚流程

alt

回滚任务配置

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做各种各样的判断和后续操作,十分的方便!本章也到此结束,如果有什么需要改进或者建议,欢迎留言。

0 条回应