用 Python 写一个自动化部署脚本(完整代码)

前言

运维日常最烦的就是重复部署。今天分享一个我常用的自动化部署脚本,支持多服务器、回滚、日志记录,开 箱即用。 16 年运维经验,这个脚本我已经在线上用了半年,稳定可靠。

功能需求

  • ✅ 支持多服务器并行部署
  • ✅ 部署失败自动回滚
  • ✅ 完整的日志记录
  • ✅ 配置文件管理
  • ✅ 钉钉/企业微信告警

项目结构

bash 复制代码
deploy_tool/
├── config.yaml
├── deploy.py
├── utils/
│
├── ssh_client.py
│
├── logger.py
│
└── notifier.py
├── scripts/
│
└── deploy_app.sh # 部署脚本
└── requirements.txt# 依赖

代码实现

1. 配置文件 (config.yaml)

yaml 复制代码
servers:
- host: 192.168.1.10port: 22
user: root
password: your_password
- host: 192.168.1.11
port: 22
user: root
password: your_password
app:
name: myapp
deploy_dir: /opt/myapp
backup_dir: /opt/backup
version: 1.0.0
notify:
dingtalk_webhook: https://oapi.dingtalk.com/robot/send?access_token=xxx
enabled: true

2. SSH 连接工具 (utils/ssh_client.py)

python 复制代码
import paramiko
from typing import Tuple
class SSHClient:
def __init__(self, host, port, user, password):
self.host = host
self.ssh = paramiko.SSHClient()
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
self.ssh.connect(host, port, user, password, timeout=10)
def exec(self, command: str) -> Tuple[int, str, str]:
"""执行命令,返回退出码、输出、错误"""
stdin, stdout, stderr = self.ssh.exec_command(command)
exit_code = stdout.channel.recv_exit_status()
return exit_code, stdout.read().decode(), stderr.read().decode()
def upload(self, local_path, remote_path):
"""上传文件"""
sftp = self.ssh.open_sftp()
sftp.put(local_path, remote_path)
sftp.close()
def close(self):
self.ssh.close()

---#### 3. 日志工具 (utils/logger.py)

python 复制代码
import logging
from datetime import datetime
def get_logger(name: str) -> logging.Logger:
logger = logging.getLogger(name)
logger.setLevel(logging.INFO)
# 控制台输出
ch = logging.StreamHandler()
ch.setLevel(logging.INFO)
# 文件输出
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
fh = logging.FileHandler(f'logs/deploy_{timestamp}.log')
fh.setLevel(logging.INFO)
# 格式
formatter = logging.Formatter(
'%(asctime)s - %(levelname)s - %(message)s'
)
ch.setFormatter(formatter)
fh.setFormatter(formatter)
logger.addHandler(ch)
logger.addHandler(fh)
return logger

4. 通知工具 (utils/notifier.py)

python 复制代码
import requests
import logging
logger = logging.getLogger(__name__)
def send_dingtalk(webhook: str, message: str):
"""发送钉钉通知"""
try:
data = {
"msgtype": "text",
"text": {
"content": f"
部署通知\n\n{message}"
}
}
🚀
response = requests.post(webhook, json=data, timeout=10)if response.status_code == 200:
logger.info("钉钉通知发送成功")
else:
logger.error(f"钉钉通知发送失败:{response.text}")
except Exception as e:
logger.error(f"钉钉通知异常:{str(e)}")
def send_wechat(webhook: str, message: str):
"""发送企业微信通知"""
try:
data = {
"msgtype": "text",
"text": {
"content": message
}
}
response = requests.post(webhook, json=data, timeout=10)
if response.status_code == 200:
logger.info("企业微信通知发送成功")
else:
logger.error(f"企业微信通知发送失败:{response.text}")
except Exception as e:
logger.error(f"企业微信通知异常:{str(e)}")

5. 部署主程序 (deploy.py)

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import yaml
import datetime
import sys
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
from utils.ssh_client import SSHClient
from utils.logger import get_logger
from utils.notifier import send_dingtalk
logger = get_logger(__name__)
class DeployTool:
def __init__(self, config_path='config.yaml'):
self.config_path = Path(config_path)
self.load_config()
def load_config(self):"""加载配置文件"""
if not self.config_path.exists():
logger.error(f"配置文件不存在:{self.config_path}")
raise FileNotFoundError(f"Config file not found: {self.config_path}")
with open(self.config_path, 'r', encoding='utf-8') as f:
self.config = yaml.safe_load(f)
logger.info(f"配置文件加载成功:{self.config_path}")
def deploy_server(self, server: dict) -> bool:
"""部署单台服务器"""
host = server['host']
try:
logger.info(f"[{host}] 开始部署...")
ssh = SSHClient(
server['host'],
server['port'],
server['user'],
server.get('password', '')
)
# 1. 备份当前版本
logger.info(f"[{host}] 正在备份...")
backup_dir = self.config['app']['backup_dir']
deploy_dir = self.config['app']['deploy_dir']
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
backup_cmd = f"cp -r {deploy_dir} {backup_dir}/backup_{timestamp}"
exit_code, _, stderr = ssh.exec(backup_cmd)
if exit_code != 0:
logger.error(f"[{host}] 备份失败:{stderr}")
ssh.close()
return False
# 2. 执行部署脚本
logger.info(f"[{host}] 正在部署应用...")
deploy_script = self.config['app'].get('deploy_script',
'/opt/scripts/deploy_app.sh')
exit_code, output, stderr = ssh.exec(f"bash {deploy_script}")
if exit_code != 0:
logger.error(f"[{host}] 部署失败:{stderr}")
self.rollback(server, backup_dir)
ssh.close()
return False# 3. 健康检查
logger.info(f"[{host}] 执行健康检查...")
health_check = self.config['app'].get('health_check', 'systemctl
status myapp')
exit_code, _, stderr = ssh.exec(health_check)
if exit_code != 0:
logger.warning(f"[{host}] 健康检查未通过:{stderr}")
logger.info(f"[{host}]
ssh.close()
return True
✅ 部署成功")
except Exception as e:
logger.error(f"[{host}] 部署异常:{str(e)}")
self.rollback(server, backup_dir)
return False
def rollback(self, server: dict, backup_dir: str):
"""回滚到上一个版本"""
host = server['host']
deploy_dir = self.config['app']['deploy_dir']
try:
logger.info(f"[{host}] 开始回滚...")
ssh = SSHClient(
server['host'],
server['port'],
server['user'],
server.get('password', '')
)
# 获取最新备份
ls_cmd = f"ls -t {backup_dir} | head -1"
exit_code, latest_backup, _ = ssh.exec(ls_cmd)
if exit_code == 0 and latest_backup.strip():
rollback_cmd = f"rm -rf {deploy_dir} && cp -r
{backup_dir}/{latest_backup.strip()} {deploy_dir}"
ssh.exec(rollback_cmd)
logger.info(f"[{host}]
回滚完成")
else:
logger.error(f"[{host}] 未找到备份文件")
✅
ssh.close()
except Exception as e:
logger.error(f"[{host}] 回滚异常:{str(e)}")def deploy_all(self) -> bool:
"""并行部署所有服务器"""
servers = self.config['servers']
total = len(servers)
logger.info(f"开始部署,共 {total} 台服务器")
success_count = 0
failed_servers = []
with ThreadPoolExecutor(max_workers=5) as executor:
future_to_server = {
executor.submit(self.deploy_server, server): server
for server in servers
}
for future in as_completed(future_to_server):
server = future_to_server[future]
host = server['host']
try:
if future.result():
success_count += 1
else:
failed_servers.append(host)
except Exception as e:
logger.error(f"[{host}] 部署异常:{str(e)}")
failed_servers.append(host)
# 发送通知
self.send_notification(success_count, total, failed_servers)
# 返回结果
if success_count == total:
logger.info(f"
部署完成:{success_count}/{total} 成功")
return True
else:
logger.warning(f"
部署部分失败:{success_count}/{total} 成功")
return False
🎉
⚠️
def send_notification(self, success: int, total: int, failed: list):
"""发送部署通知"""
if not self.config.get('notify', {}).get('enabled', False):
return
webhook = self.config['notify'].get('dingtalk_webhook', '')
✅
if success == total:
message = f"
部署成功\n\n 成功:{success}/{total}\n 时间:
{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"⚠️
else:
message = f"
部署部分失败\n\n 成功:{success}/{total}\n 失败:{',
'.join(failed)}\n 时间:{datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}"
send_dingtalk(webhook, message)
if __name__ == '__main__':
try:
deploy_tool = DeployTool()
success = deploy_tool.deploy_all()
sys.exit(0 if success else 1)
except Exception as e:
logger.error(f"部署失败:{str(e)}")
sys.exit(1)

6. 部署脚本示例 (scripts/deploy_app.sh)

bash 复制代码
#!/bin/bash
# 应用部署脚本
set -e
APP_NAME="myapp"
APP_DIR="/opt/myapp"
VERSION="1.0.0"
echo "开始部署 $APP_NAME v$VERSION"
# 1. 停止服务
echo "停止服务..."
systemctl stop $APP_NAME
# 2. 备份当前版本
echo "备份当前版本..."
# (已在 Python 脚本中完成)
# 3. 下载新版本
echo "下载新版本..."
cd $APP_DIR
wget http://your-server.com/$APP_NAME-$VERSION.tar.gz
tar -xzf $APP_NAME-$VERSION.tar.gz
# 4. 启动服务
echo "启动服务..."
systemctl start $APP_NAME
# 5. 检查状态
echo "检查服务状态..."
systemctl status $APP_NAME
echo "部署完成!"```
---
#### 7. 依赖文件 (requirements.txt)

paramiko==3.4.0 pyyaml==6.0.1 requests==2.31.0

yaml 复制代码
---
### 使用方法
#### 1. 安装依赖
```bash
pip install -r requirements.txt

2. 修改配置

bash 复制代码
vim config.yaml

修改:

  • 服务器列表(IP、用户名、密码)
  • 部署目录
  • 钉钉 webhook(可选)

3. 执行部署

bash 复制代码
python deploy.py

运行效果

✅ ✅ 🎉---

进阶功能

1. 添加健康检查

在 config.yaml 中配置:

yaml 复制代码
app:
health_check: |
curl -f http://localhost:8080/health || exit 1

2. 灰度发布

修改 deploy_all 方法,先部署一台,检查成功后再部署其他:

python 复制代码
def deploy_canary(self):
"""灰度发布"""
servers = self.config['servers']
# 先部署第一台
if not self.deploy_server(servers[0]):
logger.error("灰度部署失败,停止")
return False
# 检查通过,部署其他
for server in servers[1:]:
self.deploy_server(server)
return True

3. 集成 CI/CD

在 Jenkins/GitLab CI 中调用:

yaml 复制代码
# .gitlab-ci.yml
deploy:
stage: deploy
script:
- pip install -r requirements.txt
- python deploy.py
only:
- master

总结这个脚本的核心优势:

  1. 简单 - 代码清晰,容易理解
  2. 实用 - 覆盖部署的核心场景
  3. 可靠 - 有回滚、有日志、有通知
  4. 可扩展 - 可以根据需求添加功能

完整代码已上传 Gitee:

git clone gitee.com/wgsummer/de...
欢迎 Star ⭐

下期预告

下篇分享:《FastAPI+Vue 搭建运维平台(一):项目架构》 想学什么可以在评论区告诉我!

如果对你有帮助,点个赞支持下~

相关推荐
聚客AI3 小时前
🎉OpenClaw深度解析:多智能体协同的三种模式、四大必装技能与自动化运维秘籍
人工智能·开源·agent
IvorySQL3 小时前
双星闪耀温哥华:IvorySQL 社区两项议题入选 PGConf.dev 2026
数据库·postgresql·开源
哈基咪怎么可能是AI3 小时前
OpenClaw 插件系统:如何打造全能私人助理 --OpenClaw源码系列第2期
开源·ai编程
卡尔AI工坊10 小时前
2026年3月,我实操后最推荐的3个AI开源项目
人工智能·开源·ai编程
Jahzo1 天前
openclaw本地化部署体验与踩坑记录--飞书机器人配置
人工智能·开源
Jahzo1 天前
openclaw本地化部署体验与踩坑记录--windows
开源·全栈
冬奇Lab1 天前
一天一个开源项目(第39篇):PandaWiki - AI 驱动的开源知识库搭建系统
人工智能·开源·资讯
HelloGitHub1 天前
这个年轻的开源项目,想让每个人都能拥有自己的专业级 AI 智能体
开源·github·agent
Kagol2 天前
🎉OpenTiny NEXT-SDK 重磅发布:四步把你的前端应用变成智能应用!
前端·开源·agent