Day 9: 部署自动化
学习目标
完成今天的学习后,你将能够:
- 实现服务器自动化部署
- 使用Docker容器化部署
- 管理部署配置
- 实现版本控制和回滚
技术原理
部署方式
- 传统部署:直接部署到服务器
- 容器化部署:使用Docker部署
- 云部署:部署到云平台
- Serverless:无服务器部署
Docker基础
bash
# 构建镜像
docker build -t myapp .
# 运行容器
docker run -d -p 8000:8000 myapp
# 查看容器
docker ps
# 停止容器
docker stop <container_id>Dockerfile示例
dockerfile
FROM python:3.9-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install -r requirements.txt
COPY . .
EXPOSE 8000
CMD ["python", "app.py"]案例:自动化部署脚本
创建一个自动化部署脚本,实现:
- 代码拉取和更新
- 依赖安装
- 应用构建
- 服务重启
- 健康检查
应用场景
1. Web应用部署
- Flask/Django应用
- FastAPI服务
- 静态网站
2. 微服务部署
- 服务编排
- 负载均衡
- 服务发现
3. 批处理任务
- 定时任务部署
- 数据处理管道
- 批量作业
代码案例
案例1:服务器部署脚本
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
服务器部署脚本
功能:自动化部署Python应用到服务器
"""
import os
import sys
import subprocess
import shutil
from pathlib import Path
from datetime import datetime
class ServerDeployer:
"""服务器部署器"""
def __init__(self, config):
self.config = config
self.project_dir = Path(config['project_dir'])
self.deploy_dir = Path(config['deploy_dir'])
self.backup_dir = Path(config.get('backup_dir', '/tmp/backups'))
def run_command(self, cmd, cwd=None):
"""执行命令"""
print(f"执行: {' '.join(cmd)}")
result = subprocess.run(
cmd,
cwd=cwd or self.project_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"错误: {result.stderr}")
raise RuntimeError(f"命令执行失败: {' '.join(cmd)}")
return result.stdout
def pull_code(self):
"""拉取最新代码"""
print("\n[1/6] 拉取最新代码...")
self.run_command(['git', 'pull', 'origin', 'main'])
def backup_current(self):
"""备份当前版本"""
print("\n[2/6] 备份当前版本...")
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
backup_path = self.backup_dir / f"backup_{timestamp}"
if self.deploy_dir.exists():
shutil.copytree(self.deploy_dir, backup_path)
print(f"备份完成: {backup_path}")
return backup_path
return None
def install_dependencies(self):
"""安装依赖"""
print("\n[3/6] 安装依赖...")
# 创建虚拟环境
venv_dir = self.deploy_dir / 'venv'
if not venv_dir.exists():
self.run_command([sys.executable, '-m', 'venv', str(venv_dir)])
# 安装依赖
pip_path = venv_dir / 'bin' / 'pip'
requirements = self.project_dir / 'requirements.txt'
if requirements.exists():
self.run_command([str(pip_path), 'install', '-r', str(requirements)])
def copy_files(self):
"""复制文件到部署目录"""
print("\n[4/6] 复制文件...")
# 创建部署目录
self.deploy_dir.mkdir(parents=True, exist_ok=True)
# 复制项目文件
for item in self.project_dir.iterdir():
if item.name in ['.git', '__pycache__', 'venv', '.env']:
continue
dest = self.deploy_dir / item.name
if item.is_dir():
if dest.exists():
shutil.rmtree(dest)
shutil.copytree(item, dest)
else:
shutil.copy2(item, dest)
def restart_service(self):
"""重启服务"""
print("\n[5/6] 重启服务...")
service_name = self.config.get('service_name', 'myapp')
# 使用systemctl重启服务
try:
self.run_command(['sudo', 'systemctl', 'restart', service_name])
print(f"服务 {service_name} 已重启")
except RuntimeError:
# 如果systemctl失败,尝试直接运行
print("尝试直接启动服务...")
self.start_service_directly()
def start_service_directly(self):
"""直接启动服务"""
venv_dir = self.deploy_dir / 'venv'
python_path = venv_dir / 'bin' / 'python'
app_path = self.deploy_dir / 'app.py'
if app_path.exists():
# 使用nohup在后台运行
subprocess.Popen(
[str(python_path), str(app_path)],
stdout=subprocess.DEVNULL,
stderr=subprocess.DEVNULL,
start_new_session=True
)
def health_check(self):
"""健康检查"""
print("\n[6/6] 执行健康检查...")
import time
import requests
url = self.config.get('health_url', 'http://localhost:8000/health')
max_retries = self.config.get('health_retries', 5)
retry_interval = self.config.get('health_interval', 2)
for i in range(max_retries):
try:
response = requests.get(url, timeout=5)
if response.status_code == 200:
print("健康检查通过!")
return True
except requests.RequestException:
pass
print(f"健康检查失败,{retry_interval}秒后重试 ({i+1}/{max_retries})...")
time.sleep(retry_interval)
raise RuntimeError("健康检查失败")
def rollback(self, backup_path):
"""回滚到备份版本"""
print(f"\n回滚到备份版本: {backup_path}")
if backup_path and backup_path.exists():
# 删除当前部署
if self.deploy_dir.exists():
shutil.rmtree(self.deploy_dir)
# 恢复备份
shutil.copytree(backup_path, self.deploy_dir)
self.restart_service()
print("回滚完成!")
else:
print("错误:备份不存在")
def deploy(self):
"""执行完整部署"""
print("=" * 50)
print("开始部署")
print("=" * 50)
backup_path = None
try:
self.pull_code()
backup_path = self.backup_current()
self.copy_files()
self.install_dependencies()
self.restart_service()
self.health_check()
print("\n" + "=" * 50)
print("部署成功!")
print("=" * 50)
except Exception as e:
print(f"\n部署失败: {e}")
if backup_path:
confirm = input("是否回滚到备份版本?(y/n): ")
if confirm.lower() == 'y':
self.rollback(backup_path)
sys.exit(1)
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='服务器部署工具')
parser.add_argument('--config', required=True, help='配置文件路径')
parser.add_argument('--rollback', help='回滚到指定备份')
args = parser.parse_args()
# 加载配置
import json
with open(args.config) as f:
config = json.load(f)
deployer = ServerDeployer(config)
if args.rollback:
deployer.rollback(Path(args.rollback))
else:
deployer.deploy()
if __name__ == "__main__":
main()案例2:Docker部署
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Docker部署脚本
功能:使用Docker自动化部署应用
"""
import os
import subprocess
import sys
from pathlib import Path
class DockerDeployer:
"""Docker部署器"""
def __init__(self, project_dir='.'):
self.project_dir = Path(project_dir).resolve()
self.image_name = None
self.container_name = None
def run_command(self, cmd, cwd=None):
"""执行命令"""
print(f"执行: {' '.join(cmd)}")
result = subprocess.run(
cmd,
cwd=cwd or self.project_dir,
capture_output=True,
text=True
)
if result.returncode != 0:
print(f"错误: {result.stderr}")
return False
if result.stdout:
print(result.stdout)
return True
def create_dockerfile(self):
"""创建Dockerfile"""
dockerfile = self.project_dir / 'Dockerfile'
if not dockerfile.exists():
dockerfile.write_text("""FROM python:3.9-slim
# 设置工作目录
WORKDIR /app
# 复制依赖文件
COPY requirements.txt .
# 安装依赖
RUN pip install --no-cache-dir -r requirements.txt
# 复制项目文件
COPY . .
# 暴露端口
EXPOSE 8000
# 启动命令
CMD ["python", "app.py"]
""")
print("Dockerfile创建完成")
def create_dockerignore(self):
"""创建.dockerignore文件"""
dockerignore = self.project_dir / '.dockerignore'
if not dockerignore.exists():
dockerignore.write_text("""__pycache__
*.pyc
*.pyo
.git
.gitignore
.env
venv
.venv
*.md
tests
""")
print(".dockerignore创建完成")
def build_image(self, tag='latest'):
"""构建Docker镜像"""
self.image_name = f"{self.project_dir.name}:{tag}"
print(f"\n构建镜像: {self.image_name}")
return self.run_command([
'docker', 'build',
'-t', self.image_name,
'.'
])
def stop_container(self):
"""停止现有容器"""
if self.container_name:
print(f"\n停止容器: {self.container_name}")
self.run_command(['docker', 'stop', self.container_name])
self.run_command(['docker', 'rm', self.container_name])
def run_container(self, port=8000, name=None):
"""运行容器"""
self.container_name = name or f"{self.project_dir.name}_app"
print(f"\n运行容器: {self.container_name}")
return self.run_command([
'docker', 'run', '-d',
'--name', self.container_name,
'-p', f'{port}:8000',
self.image_name
])
def check_health(self, port=8000):
"""检查容器健康状态"""
import time
import requests
print("\n检查容器健康状态...")
for i in range(10):
try:
response = requests.get(f'http://localhost:{port}/health', timeout=2)
if response.status_code == 200:
print("容器运行正常!")
return True
except:
pass
time.sleep(2)
print("警告:健康检查超时")
return False
def show_logs(self):
"""显示容器日志"""
if self.container_name:
print(f"\n容器日志:")
self.run_command(['docker', 'logs', self.container_name])
def deploy(self, port=8000, tag='latest'):
"""执行完整部署"""
print("=" * 50)
print("开始Docker部署")
print("=" * 50)
# 创建配置文件
self.create_dockerfile()
self.create_dockerignore()
# 构建镜像
if not self.build_image(tag):
print("镜像构建失败")
return False
# 停止现有容器
self.stop_container()
# 运行新容器
if not self.run_container(port):
print("容器启动失败")
return False
# 健康检查
self.check_health(port)
# 显示日志
self.show_logs()
print("\n" + "=" * 50)
print("部署完成!")
print(f"访问地址: http://localhost:{port}")
print("=" * 50)
return True
def main():
"""主函数"""
import argparse
parser = argparse.ArgumentParser(description='Docker部署工具')
parser.add_argument('project_dir', nargs='?', default='.', help='项目目录')
parser.add_argument('--port', type=int, default=8000, help='映射端口')
parser.add_argument('--tag', default='latest', help='镜像标签')
parser.add_argument('--build-only', action='store_true', help='仅构建镜像')
args = parser.parse_args()
deployer = DockerDeployer(args.project_dir)
if args.build_only:
deployer.build_image(args.tag)
else:
deployer.deploy(args.port, args.tag)
if __name__ == "__main__":
main()课后练习
练习1:部署脚本
- 编写一个简单的部署脚本
- 支持代码更新和服务重启
- 添加回滚功能
练习2:Docker部署
- 为现有项目创建Dockerfile
- 使用Docker Compose编排多个服务
- 实现自动构建和部署
练习3:配置管理
- 管理不同环境的配置
- 实现配置热更新
- 保护敏感配置信息
常见问题
Q1: 如何选择部署方式?
A: 小型项目用传统部署,大型项目用Docker,微服务用Kubernetes。
Q2: 如何实现零停机部署?
A: 使用蓝绿部署或滚动更新策略。
Q3: 如何管理部署密钥?
A: 使用环境变量或密钥管理服务。
下一步学习
完成今天的学习后,建议你:
- 为项目编写部署脚本
- 尝试Docker容器化部署
- 配置自动化部署流程
- 准备进入Day 10的学习:CI/CD
明天我们将学习如何配置持续集成和持续部署。