无论是为新需求添加的代码,还是静态配置的变更,应用的任何变动都要经过部署这道工序才能最终落地。但通常,新的部署意味着应用重启、服务中断。工程师和测试人员经常在深夜搞得筋疲力尽,甚至焦头烂额。进入持续交付的时代后,这个痛点只会更加突显,因为持续交付意味着持续部署。例如,在测试环境小时级的持续集成场景中,如果没有办法将部署过程流程化、自动化,显然会频繁打断最终的交付过程,大幅降低开发测试效率。
因此,我们想要的应该是:一个易用、快速、稳定、容错力强,必要时有能力迅速回滚的部署系统。
单机部署过程高度抽象后其实就三个步骤:
假设我们实现了一个自动部署程序,简单地顺序执行上面的步骤,让我们一起来检验是否能满足发布的需求:
通过这个程序的简单执行过程,我们可以看到这套流程的简单实现,基本满足了我们部署的需求。而且,可以通过添加更复杂的控制流,获得更大的提升空间。
而如今架构基本上告别了单点世界,面向集群的部署带来了更高维度的问题。当部署的目标是一组机器而不是一台机器时,主要问题就变成了如何协调整个过程。比如,追踪、同步一组机器目前部署进行到了哪一步,编排集群的部署命令就成为了更核心功能。
Ansible 是一个自动化运维管理工具,支持 Linux/Windows 跨平台的配置管理,任务分发等操作,可以帮我们大大减少在变更环境时所花费的时间。与其他三大主流的配置管理工具 Chef、Puppet、Salt 相比,Ansible 最大的特点在于“agentless”,即无需在目标机器装安装 agent 进程,即可通过 SSH 或者 PowerShell 对一个环境中的集群进行中心化的管理。所以,这个“agentless” 特性,可以大大减少我们配置管理平台的学习成本,尤其适合于第一次尝试使用此类配置管理工具。
正如其他配置管理工具一样,Ansible 可以帮助我们完成一些批量任务,或者完成一些需要经常重复的工作
这些场景中我们都可以使用到 Ansible
/etc/anaible/hosts
ansible-doc –l
可查看模块序号 | 名称 | 操作系统 | 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 | 自动化测试环境 |
这里以 Centos 7.x yum安装为例:
# yum install ansible
查看版本:
# 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界面与用户交互的执行工具 |
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的服务器操作系统。
简单总结如下:
可以使用 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 参数,脚本将提示用户手动重新启动并在需要时登录。下次登录用户时,脚本将从上次停止的地方继续,然后继续该过程,直到不需要其他操作为止。
注意:
在 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
一旦将 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 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。一些有用的关键选项是:
要获取证书本身的详细信息,请在PowerShell中使用相关的证书指纹运行以下命令:
$thumbprint = "E6CDAA82EEAF2ECE8546E05DB7F3E01AA47D76CE"
Get-ChildItem -Path cert:\LocalMachine\My -Recurse | Where-Object { $_.Thumbprint -eq $thumbprint } | Select-Object *
可以通过三种方式设置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
查看 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 主机配置到此结束,我们验证配置的是否有问题。
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 对 Release_AutoTest 组内的主机进行 ping 模块测试
# ansible Release_AutoTest -m win_ping
172.16.106.191 | SUCCESS => {
"changed": false,
"ping": "pong"
}
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 这两者来做到一个通用的服务回滚策略。
#!/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自动发布中交互式参数应用
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()
注意:
如果主机比较多的情况,建议不要使用这种硬编码的方式,可以考虑放到一个配置文件进行读取。
在今天这篇文章中,主要基于 Ansible 系统的能力,和大家分享了搭建一套部署系统的过程。在搭建过程中,你最需要关注的几部分内容是:
至此,我们要搭建的整个自动部署系统,也算是顺利完成了
相关资源:
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讲 王潇俊