持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)

文章目录

    • 前言
    • 部署的需求
    • 落地方案
      • 技术架构
      • 主要特点
      • 技术选型
      • 环境配置
    • 预备知识
      • Ansible
        • Ansible是什么?
        • Ansible能做什么?
        • Ansible架构
        • Ansible工作原理
        • Ansible特性
        • Ansible主要组成部分
        • 注意事项
    • 具体实现
      • 环境规划
      • 搭建 Master 环境(Linux)
      • 搭建受控端环境(window)
        • 主机要求
        • 升级 Upgrading PowerShell 和 .NET Framework
        • WinRM 内存修补程序
        • WinRM 安装程序
        • WinRM 监听
        • 设置 WinRM 侦听器
        • 设置Windows远端管理
      • Inventory 主机清单
      • PlayBook 任务剧本
        • 剧本、资源路径
        • 完整剧本
      • 回滚部署
      • Jenkins 执行
      • 钉钉通知
    • 小结

前言

无论是为新需求添加的代码,还是静态配置的变更,应用的任何变动都要经过部署这道工序才能最终落地。但通常,新的部署意味着应用重启、服务中断。工程师和测试人员经常在深夜搞得筋疲力尽,甚至焦头烂额。进入持续交付的时代后,这个痛点只会更加突显,因为持续交付意味着持续部署。例如,在测试环境小时级的持续集成场景中,如果没有办法将部署过程流程化、自动化,显然会频繁打断最终的交付过程,大幅降低开发测试效率。
因此,我们想要的应该是:一个易用、快速、稳定、容错力强,必要时有能力迅速回滚的部署系统。

部署的需求

单机部署过程高度抽象后其实就三个步骤:

  • 在目标机器上执行命令停掉运行中的服务
  • 把提前准备好的变更包传上机器覆盖原来的目录
  • 运行命令把服务再跑起来

假设我们实现了一个自动部署程序,简单地顺序执行上面的步骤,让我们一起来检验是否能满足发布的需求:

  • 易用:执行脚本就好,填入参数,一键执行。
  • 快速:自动化肯定比手工快,并且有提升空间。比如,因为有版本的概念,我们可以跳过相同版本的部署,或是某些步骤。
  • 稳定:因为这个程序逻辑比较简单,而且执行步骤并不多,没有交叉和并行,所以稳定性也没什么大的挑战。
  • 容错性强:表现一般,脚本碰到异常状况只能停下来,但因为版本间是隔离的,不至于弄坏老的服务,通过人工介入仍能恢复。
  • 回滚顺滑:因为每个版本都是完整的可执行产物,所以回滚可以视作使用旧版本重新做一次部署。甚至我们可以在目标机器上缓存旧版本产物,实现超快速回滚。

通过这个程序的简单执行过程,我们可以看到这套流程的简单实现,基本满足了我们部署的需求。而且,可以通过添加更复杂的控制流,获得更大的提升空间。

而如今架构基本上告别了单点世界,面向集群的部署带来了更高维度的问题。当部署的目标是一组机器而不是一台机器时,主要问题就变成了如何协调整个过程。比如,追踪、同步一组机器目前部署进行到了哪一步,编排集群的部署命令就成为了更核心功能。

落地方案

技术架构

持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第1张图片

主要特点

  • 使用 Jenkins 作为一站式部署平台,方便选择参数,自动协调各主机,自动运行部署命令,自动通知等
  • 支持快速回滚指定旧版本
  • 支持面向集群进行编排、追踪和同步任务
  • 实现钉钉自动化通知及跳转功能

技术选型

  • 执行引擎:Ansible
  • 自动通知:钉钉webhook & python
  • Jenkins 插件:
    • Shell:执行 shell 脚本
    • Active Choices Plugin:动态交互参数
    • AnsiColor:彩色输出,非必须

环境配置

  • Ansible: 2.9.0
  • Python: 2.7.5
  • CentOS: 7.6
  • Java: 1.8.0_73
  • Jenkins: 2.164.3

预备知识

Ansible

Ansible是什么?

Ansible 是一个自动化运维管理工具,支持 Linux/Windows 跨平台的配置管理,任务分发等操作,可以帮我们大大减少在变更环境时所花费的时间。与其他三大主流的配置管理工具 Chef、Puppet、Salt 相比,Ansible 最大的特点在于“agentless”,即无需在目标机器装安装 agent 进程,即可通过 SSH 或者 PowerShell 对一个环境中的集群进行中心化的管理。所以,这个“agentless” 特性,可以大大减少我们配置管理平台的学习成本,尤其适合于第一次尝试使用此类配置管理工具。

Ansible能做什么?

正如其他配置管理工具一样,Ansible 可以帮助我们完成一些批量任务,或者完成一些需要经常重复的工作

  • 比如:同时在 100 台服务器上安装 nginx 服务,并在安装后启动它们
  • 比如:将某个文件一次性拷贝到 100 台服务器上
  • 比如:每当有新服务器加入工作环境时,你都要为新服务器部 redis 服务,也就是说你需要经常重复的完成相同的工作

这些场景中我们都可以使用到 Ansible

Ansible架构

持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第2张图片

Ansible工作原理

持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第3张图片

Ansible特性

  • 模块化:调用特定的模块,完成特定任务
  • 有 Paramiko,PyYAML,Jinja2(模板语言)三个关键模块
  • 支持自定义模块
  • 基于 Python 语言实现
  • 部署简单,基于 python 和 SSH(默认已安装),agentless
  • 安全,基于 OpenSSH
  • 支持 playbook 编排任务
  • 幂等性:一个任务执行1遍和执行n遍效果一样,不因重复执行带来意外情况
  • 无需代理不依赖 PKI(无需ssl)
  • 可使用任何编程语言写模块
  • YAML 格式,编排任务,支持丰富的数据结构
  • 较强大的多层解决方案

Ansible主要组成部分

  • PLAYBOOKS:任务剧本(任务集),编排定义 Ansible 任务集的配置文件,由 Ansible 顺序依次执行,通常是 JSON 格式的 YML 文件
  • INVENTORY:Ansible 管理主机的清单 /etc/anaible/hosts
  • MODULES:Ansible 执行命令的功能模块,多数为内置的核心模块,也可自定义,ansible-doc –l 可查看模块
  • PLUGINS:模块功能的补充,如连接类型插件、循环插件、变量插件、过滤插件等,该功能不常用
  • API:供第三方程序调用的应用程序编程接口
  • ANSIBLE:组合 INVENTORY、 API、 MODULES、PLUGINS 的绿框,可以理解为是 Ansible 命令工具,其为核心执行工具

注意事项

  • 执行 Ansible 的主机一般称为主控端,中控,master 或堡垒机
  • 主控端 Python 版本需要2.6或以上
  • 被控端 Python 版本小于2.4需要安装 python-simplejson
  • 被控端如开启 SELinux 需要安装 libselinux-python
  • windows 不能做为主控端

具体实现

环境规划

序号 名称 操作系统 IP地址 用途
1 enkins-salve Centos7 172.16.106.122 Jenkins salve
2 win7-app-Develop-Dev win7专业版 172.16.106.199 开发环境
3 win7-app-Develop-Release win7专业版 172.16.106.153 开发环境
4 win7-app-Test-Dev win7专业版 172.16.106.180 功能测试环境
5 win7-app-Test-Release win7专业版 172.16.106.185 功能测试环境
6 win7-app-AutoTest-Release win7专业版 172.16.106.191 自动化测试环境
7 win7-app-AutoTest-Dev win7专业版 172.16.106.14 自动化测试环境

搭建 Master 环境(Linux)

这里以 Centos 7.x yum安装为例:

# yum install ansible

持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第4张图片

查看版本:

# ansible --version
ansible 2.9.0
  config file = /etc/ansible/ansible.cfg
  configured module search path = [u'/root/.ansible/plugins/modules', u'/usr/share/ansible/plugins/modules']
  ansible python module location = /usr/lib/python2.7/site-packages/ansible
  executable location = /usr/bin/ansible
  python version = 2.7.5 (default, Jun 20 2019, 20:27:34) [GCC 4.8.5 20150623 (Red Hat 4.8.5-36)]

配置文件:

配置文件 描述
/etc/ansible/ansible.cfg 主配置文件,配置ansible工作特性
/etc/ansible/hosts 主机清单
/etc/ansible/roles/ 存放角色的目录
/usr/bin/ansible 主程序,临时命令执行工具
/usr/bin/ansible-doc 查看配置文档,模块功能查看工具
/usr/bin/ansible-galaxy 下载/上传优秀代码或Roles模块的官网平台
/usr/bin/ansible-playbook 定制自动化任务,编排剧本工具
/usr/bin/ansible-pull 远程执行命令的工具
/usr/bin/ansible-vault 文件加密工具
/usr/bin/ansible-console 基于Console界面与用户交互的执行工具

搭建受控端环境(window)

主机要求

Ansible 从 1.7+ 版本开始支持 Windows,但前提是管理机必须为 Linux 系统,远程主机的通信方式也由SSH变更为PowerShell,同时管理机必须预安装 Python 的 Winrm 模块,方可和远程 Windows 主机正常通信,但 PowerShell 需4.0+版本且Management Framework 4.0+版本,。Ansible 可以管理包括 Windows 7、8.1和10的桌面操作系统以及包括Windows Server 2008、2008 R2、2012、2012 R2、2016和2019的服务器操作系统。

简单总结如下:

  • 管理机必须为 Linux 系统且需预安装 Python 和 Winrm 模块
  • 底层通信基于 PowerShell,版本为3.0+,Management Framework 版本为4.0+
  • 远程主机开启 Winrm 服务

升级 Upgrading PowerShell 和 .NET Framework

可以使用 Upgrade-PowerShell.ps1 脚本来更新它们

这是如何从PowerShell运行此脚本的示例:

$url = "https://raw.githubusercontent.com/jborean93/ansible-windows/master/scripts/Upgrade-PowerShell.ps1"
$file = "$env:temp\Upgrade-PowerShell.ps1"
$username = "Administrator"
$password = "Password"

(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
Set-ExecutionPolicy -ExecutionPolicy Unrestricted -Force

# Version can be 3.0, 4.0 or 5.1
&$file -Version 5.1 -Username $username -Password $password -Verbose

完成后,将需要删除自动登录并将执行策略重新设置为默认值 Restricted。可以使用以下PowerShell命令执行此操作:

# This isn't needed but is a good security practice to complete
Set-ExecutionPolicy -ExecutionPolicy Restricted -Force

$reg_winlogon_path = "HKLM:\Software\Microsoft\Windows NT\CurrentVersion\Winlogon"
Set-ItemProperty -Path $reg_winlogon_path -Name AutoAdminLogon -Value 0
Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultUserName -ErrorAction SilentlyContinue
Remove-ItemProperty -Path $reg_winlogon_path -Name DefaultPassword -ErrorAction SilentlyContinue

该脚本通过检查是否需要安装哪些程序(例如.NET Framework 4.5.2)以及所需的PowerShell版本来工作。如果需要重新启动 username 并且 password 已设置和参数,则脚本将从重新启动后自动重新启动并登录。该脚本将继续执行,直到不需要其他操作并且PowerShell版本与目标版本匹配为止。如果未设置 usernam 和 password 参数,脚本将提示用户手动重新启动并在需要时登录。下次登录用户时,脚本将从上次停止的地方继续,然后继续该过程,直到不需要其他操作为止。

注意:

  • 如果在 Server 2008 上运行,则必须安装SP2。如果在 Server 2008 R2 或 Windows 7 上运行,则必须安装SP1
  • Windows Server 2008 只能安装 PowerShell 3.0,指定较新的版本将导致脚本失败
  • 在 username 和 password 参数都存储在注册表中的纯文本。确保脚本完成后运行清除命令,以确保主机上仍没有存储凭据。

WinRM 内存修补程序

在 PowerShell v3.0 上运行时,WinRM 服务存在一个错误,该错误会限制 WinRM 可用的内存量。没有安装此修补程序,Ansible 将无法在 Windows 主机上执行某些命令。这些修补程序应作为系统引导或映像过程的一部分进行安装
脚本Install-WMF3Hotfix.ps1可用于在受影响的主机上安装此修补程序

$url = "https://raw.githubusercontent.com/jborean93/ansible-windows/master/scripts/Install-WMF3Hotfix.ps1"
$file = "$env:temp\Install-WMF3Hotfix.ps1"

(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)
powershell.exe -ExecutionPolicy ByPass -File $file -Verbose

WinRM 安装程序

一旦将 Powershell 升级到至少3.0版,最后一步就是配置 WinRM 服务,以便 Ansible 可以连接到它。WinRM 服务的两个主要组件决定着 Ansible 与 Windows 主机的接口方式:listener和和service配置设置。
可以使用脚本 ConfigureRemotingForAnsible.ps1 来设置基础。该脚本使用自签名证书设置HTTP和HTTPS侦听器,并Basic 在服务上启用身份验证选项。

要使用此脚本,请在PowerShell中运行以下命令:

$url = "https://raw.githubusercontent.com/ansible/ansible/devel/examples/scripts/ConfigureRemotingForAnsible.ps1"
$file = "$env:temp\ConfigureRemotingForAnsible.ps1"

(New-Object -TypeName System.Net.WebClient).DownloadFile($url, $file)

powershell.exe -ExecutionPolicy ByPass -File $file

WinRM 监听

WinRM 服务在一个或多个端口上侦听请求。这些端口中的每一个都必须具有创建和配置的侦听器。
要查看 WinRM 服务上正在运行的当前侦听器,请运行以下命令:

winrm enumerate winrm/config/Listener

Listener
    Address = *
    Transport = HTTP
    Port = 5985
    Hostname
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint
    ListeningOn = 10.0.2.15, 127.0.0.1, 192.168.56.155, ::1, fe80::5efe:10.0.2.15%6, fe80::5efe:192.168.56.155%8, fe80::
ffff:ffff:fffe%2, fe80::203d:7d97:c2ed:ec78%3, fe80::e8ea:d765:2c69:7756%7

Listener
    Address = *
    Transport = HTTPS
    Port = 5986
    Hostname = SERVER2016
    Enabled = true
    URLPrefix = wsman
    CertificateThumbprint = E6CDAA82EEAF2ECE8546E05DB7F3E01AA47D76CE
    ListeningOn = 10.0.2.15, 127.0.0.1, 192.168.56.155, ::1, fe80::5efe:10.0.2.15%6, fe80::5efe:192.168.56.155%8, fe80::
ffff:ffff:fffe%2, fe80::203d:7d97:c2ed:ec78%3, fe80::e8ea:d765:2c69:7756%7

在上面的示例中,激活了两个侦听器。一种是通过 HTTP 监听端口5985,另一种是通过HTTPS监听端口5986。一些有用的关键选项是:

  • Transport:无论侦听器是通过HTTP还是HTTPS运行,建议对HTTPS使用侦听器,因为数据已加密,无需进行任何进一步更改。
  • Port:监听器运行的端口,默认情况下是5985用于HTTP和5986 TTPS的端口。该端口可以更改为所需的任何端口,并与主机var对应ansible_port。
  • Prefix:要侦听的URL前缀,默认为wsman。如果更改此 ansible_winrm_path 设置,则必须将主机 var 设置为相同的值。
  • CertificateThumbprint:如果运行在HTTPS侦听器上,这是连接中使用的 Windows 证书存储中证书的指纹。

要获取证书本身的详细信息,请在PowerShell中使用相关的证书指纹运行以下命令:

$thumbprint = "E6CDAA82EEAF2ECE8546E05DB7F3E01AA47D76CE"
Get-ChildItem -Path cert:\LocalMachine\My -Recurse | Where-Object { $_.Thumbprint -eq $thumbprint } | Select-Object *

设置 WinRM 侦听器

可以通过三种方式设置WinRM侦听器:

  • 使用了 HTTP 或 HTTPS的。在域环境之外运行并且需要一个简单的侦听器时,这是最容易使用的选项。与其他选项不同,此过程还具有为所需的端口打开防火墙并启动WinRM服务的额外好处。
winrm quickconfigwinrm quickconfig -transport:https
  • 使用组策略对象。当主机是域的成员时,这是创建侦听器的最佳方法,因为配置是自动完成的,无需任何用户输入。有关组策略对象的更多信息,请参阅 组策略对象文档。

  • 使用 PowerShell 创建具有特定配置的侦听器。这可以通过运行以下 PowerShell 命令来完成:

$selector_set = @{
    Address = "*"
    Transport = "HTTPS"
}
$value_set = @{
    CertificateThumbprint = "E6CDAA82EEAF2ECE8546E05DB7F3E01AA47D76CE"
}

New-WSManInstance -ResourceURI "winrm/config/Listener" -SelectorSet $selector_set -ValueSet $value_set

设置Windows远端管理

查看 winrm service listener:

winrm e winrm/config/listener

为 winrm service 配置 auth:

winrm set winrm/config/service/auth @{Basic="true"}

为 winrm service 配置加密方式为允许非加密:

winrm set winrm/config/service @{AllowUnencrypted="true"}

好了,远程 Windows 主机配置到此结束,我们验证配置的是否有问题。

Inventory 主机清单

Ansible 必须通过 Inventory 来管理主机。Ansible 可同时操作属于一个组的多台主机,组和主机之间的关系通过 inventory 文件配置。

# vi /etc/ansible/hosts 
[Dev_ALL]
172.16.106.14 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore
172.16.106.180 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore
172.16.106.199 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Dev_AutoTest]
172.16.106.14 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Dev_FunctionTest]
172.16.106.180 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Dev_Develop]
172.16.106.199 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Release_AutoTest]
172.16.106.191 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Release_Develop]
172.16.106.153 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Release_FunctionTest]
172.16.106.185 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

[Release_ALL]
172.16.106.191 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore
172.16.106.153 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore
172.16.106.185 ansible_ssh_user="xxxx" ansible_ssh_pass="xxxx" ansible_ssh_port=5985 ansible_connection="winrm" ansible_winrm_server_cert_validation=ignore

参数说明:

  • ansible_ssh_user:用户名
  • ansible_ssh_pass:密码
  • ansible_ssh_port:端口号
  • ansible_connection:与主机的连接类型

主机说明:

  • Dev_ALL:所有dev版本环境
  • Dev_AutoTest:dev版本自动化测试环境
  • Dev_FunctionTest:dev版本功能测试环境
  • Dev_Develop:dev版本开发环境
  • Release_ALL:所有release版本环境
  • Release_AutoTest:release版本自动化测试环境
  • Release_Develop:release版本开发环境
  • Release_FunctionTest:release版本功能测试环境

使用 ansible 对 Release_AutoTest 组内的主机进行 ping 模块测试

# ansible Release_AutoTest -m win_ping
172.16.106.191 | SUCCESS => {
    "changed": false, 
    "ping": "pong"
}

PlayBook 任务剧本

PlayBook 是 Ansible 的脚本文件,使用 YAML 语言编写,包含需要远程执行的核心命令、定义任务具体内容,等等。

通常情况下,我们用脚本的方式使用 Ansible,只要使用好 Inventory 和 PlayBook 这两个组件就可以了,即:使用 PlayBook 编写 Ansible 脚本,然后用 Inventory 维护好需要管理的机器列表。这样,就能解决 90% 以上使用 Ansible 的需求。

但如果你有一些更复杂的需求,比如通过代码调用 Ansible,可能还要用到 API 组件。感兴趣的话,你可以参考 Ansible 的官方文档。

剧本、资源路径

/home/ansible/playbooks 剧本存放目录
/home/ansible/python python搅拌

完整剧本

# vi server-deploy.yaml 
# ---------------------------------
# 1.各变量赋值
# 2.初始化目录,包括:程序目录,下载目录,资源备份目录(如果不存在)
# 3.结束正在运行的服务进程(等待3秒)
# 4.清空资源目录
# 5.备份 Data/Files 目录
# 6.备份 Data/projects 目录
# 7.清空程序目录
# 8.下载 server 程序文件
# 9.解压文件
# 10.清空&还原 Data/projects 目录
# 11.启动 server 服务
# ---------------------------------

- hosts: "{{target}}"
  remote_user: htsd
  vars:
    package:
      root_url: "http://172.16.106.188:8081/repository/app-{{branch}}-info/server/"
    deploy:
      app_path: "C:\\app\\app-{{branch}}-info\\server"
      package_path: C:\app\package
      res_path: C:\app\res
      PsExec_path: C:\app\tools\PSTools
  tasks:
 # --------------------初始化目录--------------------
  - name: 创建程序目录
    win_file:
      path: "{{deploy.app_path}}"
      state: directory
      become: yes
  - debug:
      msg: "{{deploy.app_path}}"
  - name: 创建下载目录
    win_file:
      path: "{{deploy.package_path}}"
      state: directory
      become: yes
  - debug:
      msg: "{{deploy.package_path}}"
  - name: 创建资源备份目录
    win_file:
      path: "{{deploy.res_path}}"
      state: directory
      become: yes
  - debug:
      msg: "{{deploy.package_path}}"
  # -------------------备份及结束进程------------------
  - name: 结束 Server 进程
    win_shell: Stop-Process -Name "app.Server" -Force
    ignore_errors: true
  - name: 等待3秒停止 Server 进程
    win_wait_for_process:
      process_name_pattern: app.Server
      state: absent
      timeout: 3
  - name: 清空资源目录
    win_shell: |
      $TargetFolder = "{{deploy.res_path}}"
      $Files = get-childitem $TargetFolder -force
      Foreach ($File in $Files)
      {
      $FilePath=$File.FullName
      Remove-Item -Path $FilePath -Recurse -Force
      }
  - name: 备份 Data/Files 目录
    win_shell: Copy-Item "{{deploy.app_path}}\Data\Files" -Destination {{deploy.res_path}} -Recurse
    ignore_errors: yes
  - name: 备份 Data/projects 目录
    win_shell: Copy-Item "{{deploy.app_path}}\Data\projects" -Destination {{deploy.res_path}} -Recurse
    ignore_errors: yes
  - name: 清空程序目录
    win_shell: |
      $TargetFolder = "{{deploy.app_path}}"
      $Files = get-childitem $TargetFolder -force
      Foreach ($File in $Files)
      {
      $FilePath=$File.FullName
      Remove-Item -Path $FilePath -Recurse -Force
      }
# ----------------下载&更新程序------------------------
  - name: 下载 server 程序文件
    win_get_url:
      url: "{{package.root_url}}{{package_name}}"
      dest: "{{deploy.package_path}}"
      force: no
  - debug:
      msg: "{{package_name}}"
  - name: 递归解压文件后删除zip包
    win_unzip:
     src:  "{{deploy.package_path}}/{{package_name}}"
     dest:  "{{deploy.app_path}}"
     recurse: yes
     delete_archive: yes
  - name: 删除原 Data/Files 目录
    win_shell: rmdir /s/q "{{deploy.app_path}}\Data\Files"
    args:
        executable: cmd.exe
    ignore_errors: yes
  - name: 删除原 Data/projects 目录
    win_shell: rmdir /s/q "{{deploy.app_path}}\Data\projects"
    args:
        executable: cmd.exe
    ignore_errors: yes
  - name: 还原 Data/Files 目录
    win_shell: Copy-Item "{{deploy.res_path}}\Files" -Destination "{{deploy.app_path}}\Data" -Recurse
    ignore_errors: yes
  - name: 还原 Data/projects 目录
    win_shell: Copy-Item "{{deploy.res_path}}\projects" -Destination "{{deploy.app_path}}\Data" -Recurse
    ignore_errors: yes
# -------------------------启动-------------------------
  - name: 启动 app-server
    win_shell: "{{deploy.PsExec_path}}/psexec.exe  -accepteula -nobanner -i 1 -s -d {{deploy.app_path}}//app.Server.exe"
    register: output
    ignore_errors: yes
#  - name: 打印日志
#    debug: var=output

回滚部署

由于各种各样的原因,部署的版本可能会出现异常,这时候可能需要紧急回滚版本,我们可以手动去回滚版本,但是缺点也很明显,当主机实例过多时,手动回滚明显是不再明智的,所以我们可结合 Jenkins+Ansible 这两者来做到一个通用的服务回滚策略。

Jenkins 执行

#!/usr/bin/env bash
echo '版本类型:'$Branch
echo '环境类型:'$Hosts
echo '文件名称:'$Package_Name

ansible-playbook /home/ansible/playbooks/server-deploy.yaml --extra-vars "package_name=$Package_Name branch=$Branch target=$Hosts"

具体参考上文:持续交付之解决Jenkins自动发布中交互式参数应用

Jenkins 执行日志:
持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第5张图片

钉钉通知

Jenkins 调用:
持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第6张图片

python 脚本:

# coding=utf-8

'''
@author: zuozewei
@file: notification.py
@time: 2019/4/25 18:00
@description:dingTalk通知类
'''
import os, jenkins, json
from dingtalkchatbot.chatbot import DingtalkChatbot
from jsonpath import jsonpath

JOB_NAME = str(os.getenv("JOB_NAME"))
BUILD_URL = str(os.getenv("BUILD_URL")) + "console"
BUILD_NUMBER = str(os.getenv("BUILD_NUMBER"))
Package_Name = str(os.getenv("Package_Name"))
VERSION = Package_Name.split('-')[4].replace('.zip','')
Branch = str(os.getenv("Branch"))
Hosts =  str(os.getenv("Hosts"))

Branch_Name = ''
Eev = ''
Host_name = ''
if Branch == 'dev':
    Branch_Name = '开发版'
    if Hosts == 'Dev_ALL':
        Eev = 'Dev所有环境'
        Host_name =  '- 172.16.106.175' + '\n' + \
                     '- 172.16.106.155' + '\n' + \
                     '- 172.16.106.115' + '\n'
    elif Hosts == 'Dev_AutoTest':
        Eev = 'Dev自动化测试环境'
        Host_name =  '- 172.16.106.175' + '\n'
    elif Hosts == 'Dev_FunctionTest':
        Eev = 'Dev功能测试环境'
        Host_name =  '- 172.16.106.155' + '\n'
    elif Hosts == 'Dev_Develop':
        Eev = 'Dev开发环境'
        Host_name =  '- 172.16.106.115' + '\n'
elif Branch == 'release':
    Branch_Name = '预览版'
    if Hosts == 'Release_ALL':
        Eev = 'Release所有环境'
        Host_name =  '- 172.16.106.58' + '\n' + \
                     '- 172.16.106.168' + '\n' + \
                     '- 172.16.106.203' + '\n'
    elif Hosts == 'Release_AutoTest':
        Eev = 'Release自动化测试环境'
        Host_name =  '- 172.16.106.58' + '\n'
    elif Hosts == 'Release_FunctionTest':
        Eev = 'Release功能测试环境'
        Host_name =  '- 172.16.106.203' + '\n'
    elif Hosts == 'Release_Develop':
        Eev = 'Release开发环境'
        Host_name =  '- 172.16.106.168' + '\n'
print("【版本类型】:" + Branch_Name)
print("【环境类型】:" + Eev)
print("【主机列表】:" + Host_name)

# 连接jenkins
server = jenkins.Jenkins(url="http://172.16.106.251:8080", username='xxx', password="xxx")

# 获取指定项目编译状态
BUILD_STATUS = server.get_build_info(JOB_NAME, int(BUILD_NUMBER))['result']
print("【BUILD_STATUS】:" + BUILD_STATUS)

build_info = server.get_build_info(JOB_NAME, int(BUILD_NUMBER))
# dict字典转json数据
build_info_json = json.dumps(build_info)
# 把json字符串转json对象
build_info_jsonobj = json.loads(build_info_json)
causes = jsonpath(build_info_jsonobj, '$.actions..shortDescription')

def packagNotification():
    title = 'xxx部署通知'

    textFail = '#### ' + JOB_NAME + ' # ' + BUILD_NUMBER + ' \n' + \
               '##### 部署状态: ' + BUILD_STATUS + ' \n' + \
               '##### **版本类型**: ' + Branch_Name + '\n' + \
               '##### **当前版本**: ' + VERSION + '\n' + \
               '##### **文件名称**: ' + Package_Name + '\n' + \
               '##### **触发类型**: ' + str(causes[0]) + '\n' + \
               '##### **部署日志**: [查看详情](' + BUILD_URL + ') \n' + \
               '##### **关注人**: @186xxxx2487 \n' + \
               '##### **部署环境**: ' + Eev + '\n' + \
               '##### **执行主机**: \n' + \
                Host_name + '\n' + \
               '> ###### xxx技术团队 \n '

    textSuccess = '#### ' + JOB_NAME + ' # ' + BUILD_NUMBER + ' \n' + \
              '##### **部署状态**: ' + BUILD_STATUS + '\n' + \
              '##### **版本类型**: ' + Branch_Name + '\n' + \
              '##### **当前版本**: ' + VERSION + '\n' + \
              '##### **文件名称**: ' + Package_Name + '\n' + \
              '##### **触发类型**: ' + str(causes[0]) + '\n' + \
              '##### **部署日志**: [查看详情](' + BUILD_URL + ') \n' + \
              '##### **部署环境**: ' + Eev + '\n' + \
              '##### **执行主机**: \n' + \
               Host_name + '\n' + \
              '> ###### xxx技术团队 \n '

    if BUILD_STATUS == 'SUCCESS':
        dingText = textSuccess
    else:
        dingText = textFail

    sendding(title, dingText)

def sendding(title, content):
    at_mobiles = ['186xxxx2487']
    Dingtalk_access_token_v3c = 'https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxxx'
    # 初始化机器人小丁
    xiaoding1 = DingtalkChatbot(Dingtalk_access_token_v3c)
    # Markdown消息@指定用户
    xiaoding1.send_markdown(title=title, text=content, at_mobiles=at_mobiles)

if __name__ == "__main__":
    packagNotification()

通知效果:
持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第7张图片

持续交付之Jenkins+Ansible+Python搭建自动化部署框架(win版)_第8张图片

注意:
如果主机比较多的情况,建议不要使用这种硬编码的方式,可以考虑放到一个配置文件进行读取。

小结

在今天这篇文章中,主要基于 Ansible 系统的能力,和大家分享了搭建一套部署系统的过程。在搭建过程中,你最需要关注的几部分内容是:

  • 利用 Inventory 做好部署目标的管理
  • 利用 PlayBook 编写部署过程的具体逻辑
  • 利用 Jenkins 对主机集群进行编排、追踪和同步任务
  • 利用 Python 脚本钉钉自动化通知及跳转功能

至此,我们要搭建的整个自动部署系统,也算是顺利完成了

相关资源:
https://github.com/zuozewei/Jenkins-CI/tree/master/jenkins-ansible-python

参考资料:
[1]:https://blog.51cto.com/191226139/2066936
[2]:https://docs.ansible.com/ansible/latest/user_guide/windows.html
[3]:持续交付36讲 王潇俊

你可能感兴趣的:(持续交付)