最近刚带团队完成了一次从本地机房到AWS的云迁移,整个过程没有花钱买商业迁移工具,核心全靠十几段Python脚本加上AWS CLI硬啃下来。不少朋友在群里问我要方案,今天干脆整理成一篇实战记录,把设计思路、脚本细节和踩过的坑都摊开来讲清楚。
如果你正准备把自己的应用、文件或数据库迁到AWS,又不想被手工上传下载折磨到半夜,这篇文章应该能帮你省下大量时间。你不用是AWS专家,只要会一点Python基础语法,能把boto3装起来,就能跟着这篇文章走一遍完整的自动化迁移流程。
1. 迁移方案设计:先想清楚再动手
1.1 为什么选Python而不是纯命令或商业工具
很多人问,迁移这种事用aws s3 sync不就完了吗,为什么还要写Python?没错,简单场景下aws命令行确实够用,但真实迁移项目一旦超过几十个实例、上百GB文件、多个数据库,纯命令行的短板就暴露了:没法做复杂的重试逻辑,没法实现自定义的依赖顺序,更没法把流程串起来做统一的进度追踪。
选择Python,主要是因为它的生态和灵活性。boto3是AWS官方SDK,Python环境下调用S3、EC2、RDS、IAM这些服务的API非常顺手,写出来的自动化脚本可读性和可维护性都比Shell脚本高一个档次。对于团队里有研发背景的运维来说,这套方案几乎是零学习成本。
另外,迁移不是一次性动作,后面还有增量同步、定期备份、数据校验。用代码把流程固化下来,等于给整个迁移上了保险,随时可以重放、可以审计、可以改进。这一点是手动操作和简单命令无法替代的。
1.2 迁移对象盘点:搞清楚你手里有什么
动手之前最重要的一件事,是把迁移对象分门别类。我的习惯是按数据特征来分:
- 静态文件与对象存储:比如图片、附件、日志归档、静态网页资源,这类数据对一致性要求不高,但对传输速度和完整性校验有要求。
- 数据库数据:MySQL、PostgreSQL这类关系型数据库,迁移时需要考虑一致性、字符集、账号权限等问题。
- 服务器镜像与配置:云主机、虚拟机镜像,以及Nginx、Tomcat、定时任务等配置文件。服务器迁移最怕漏配置,脚本化可以保证每一步都执行到位。
- 中间件状态与依赖组件:Redis缓存、消息队列里的积压数据等,部分可以重建,部分需要导出。
把盘点结果记在一张表格里,每类数据对应一套迁移方案。有些可以全量迁移后再增量同步,有些必须先停机再迁,这些会在后面的章节具体展开。
1.3 迁移流程的四个阶段
整个自动化迁移,我把它拆成四个阶段:盘点、备份、搬运、校验。
- 盘点:统计数据量、文件数、实例规格、依赖关系,输出一份迁移清单。
- 备份:迁移前必须做完整备份,这是所有云迁移的底线,别指望“应该没问题”这种话。
- 搬运:写脚本把数据传到AWS,考虑并发、断点续传、失败重试。
- 校验:对比源端和目的端的文件数量、大小、哈希,数据库做行数统计和抽样查询。
四个阶段不是串行执行,很多时候是循环的。比如S3增量同步阶段,先跑全量,再定期跑增量,等到正式切换前做最终一致性校验,然后才把流量切过去。这个过程脚本化之后,执行起来非常踏实。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备:Python、AWS CLI和IAM权限一个都不能少
2.1 Python运行环境准备
这一步最简单,但对新手来说也是最容易卡住的。通常Linux服务器自带Python 3,Windows机器如果没有,需要去官网下载安装包,记得勾选Add Python to PATH。装完在终端确认一下:
bash复制python3 --version
pip3 --version
我更推荐用虚拟环境来隔离依赖,尤其是迁移脚本所在的服务器可能还会跑其他业务。创建虚拟环境:
bash复制python3 -m venv .venv
source .venv/bin/activate
pip install --upgrade pip
然后安装本次迁移需要的基础依赖:
bash复制pip install boto3 awscli
boto3负责在Python脚本里调用AWS API,awscli提供命令行工具,两个都很轻量,装完基本不会冲突。如果服务器在国内,pip源建议换成国内镜像,否则下载速度会很折磨人。
2.2 AWS CLI与boto3的初始化配置
安装完成后,先配置AWS凭证。AWS CLI支持通过aws configure命令交互式输入Access Key、Secret Key、region和输出格式:
bash复制aws configure
如果团队里有多套环境,建议用命名配置文件,避免把生产环境的Key和本地测试环境混在一起:
bash复制aws configure --profile migration
在Python脚本里指定使用哪个配置:
python复制import boto3
session = boto3.Session(profile_name="migration")
s3_client = session.client("s3")
这段代码的意思是:创建一个名为migration的会话对象,然后在会话基础上创建S3客户端。后续所有脚本都复用这行逻辑,不管脚本放到哪台机器上跑,只要凭证配置文件同步过去了,就不会连错环境。
2.3 IAM权限的最小化设计
权限配置是最容易出错的地方,也是安全问题的高发区。原则就一句话:只给迁移过程真正需要的权限。不要图省事直接给AdministratorAccess,万一脚本写错把不该删除的对象删了,后悔都来不及。
我给迁移账号创建的IAM策略大致长这样:
json复制{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:ListBucket",
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject",
"s3:HeadObject"
],
"Resource": [
"arn:aws:s3:::my-app-backup-2024",
"arn:aws:s3:::my-app-backup-2024/*"
]
},
{
"Effect": "Allow",
"Action": [
"ec2:DescribeInstances",
"ec2:DescribeVolumes",
"ec2:CreateSnapshot",
"ec2:CreateTags"
],
"Resource": "*"
}
]
}
注意这里S3的权限只圈定了一个桶,EC2只给了查询快照和打标签的权限,没有给创建或终止实例的权限。每一项都是按需申请的,既满足脚本执行,又控制了风险面。
3. 核心脚本实战:从S3到EC2的自动化迁移
3.1 本地文件批量上传到S3
静态文件迁移的核心是绝对不丢文件、不传漏文件。我写了一个批量上传脚本,思路是递归扫描本地目录,把每个文件的相对路径拼成S3的key,再用线程池并发上传:
python复制import os
from pathlib import Path
from concurrent.futures import ThreadPoolExecutor, as_completed
import boto3
from boto3.s3.transfer import TransferConfig
SOURCE_DIR = "/data/www"
BUCKET_NAME = "my-app-backup-2024"
PREFIX = "webapp/prod"
session = boto3.Session(profile_name="migration")
s3_client = session.client("s3")
config = TransferConfig(
multipart_threshold=8 * 1024 * 1024,
multipart_chunksize=8 * 1024 * 1024,
max_concurrency=10,
use_threads=True,
)
def upload_file(local_path: Path):
key = f"{PREFIX}/{local_path.relative_to(SOURCE_DIR)}"
try:
s3_client.upload_file(
str(local_path),
BUCKET_NAME,
key,
Config=config,
ExtraArgs={"StorageClass": "STANDARD_IA"}
)
return True, str(local_path)
except Exception as exc:
return False, f"{local_path}: {exc}"
def main():
files = [
p for p in Path(SOURCE_DIR).rglob("*")
if p.is_file() and p.stat().st_size > 0
]
print(f"待上传文件数: {len(files)}")
failed = []
with ThreadPoolExecutor(max_workers=8) as pool:
futures = [pool.submit(upload_file, f) for f in files]
for future in as_completed(futures):
ok, msg = future.result()
if not ok:
failed.append(msg)
if failed:
print("以下文件上传失败:")
for item in failed:
print(item)
else:
print("全部上传完成")
if __name__ == "__main__":
main()
这段代码有几个关键点:
- TransferConfig里设置了multipart_threshold为8MB,意思是单个文件超过8MB就自动走分片上传,避免大文件单请求超时重传。
- max_concurrency控制单文件内部的分片并发数,线程池再控制多文件并发数。实测下来,文件多且小的时候,线程池并发收益更大;文件少且大的时候,分片并发收益更大。两者配合效果最好。
- ExtraArgs把存储类型设成STANDARD_IA,成本比标准存储低,适合备份类数据。如果迁移的是在线业务数据,建议还是用STANDARD,避免首次访问延迟波动。
3.2 增量同步与文件一致性校验
全量跑完之后,开始正式业务迁移之前,肯定还有增量数据。增量同步的做法是比对本地文件和S3对象的元数据,只上传变化的部分。boto3的upload_file本身会做同名覆盖,所以只要维护一个“本地文件清单”就行。
我更推荐把校验逻辑直接写进同步脚本,每次同步时先构建一个manifest文件,记录每个文件的相对路径和MD5值。上传完成后,再把manifest一并传到S3,方便后续核对:
python复制import hashlib
import json
from pathlib import Path
def file_md5(file_path, chunk_size=8192):
h = hashlib.md5()
with open(file_path, "rb") as f:
while chunk := f.read(chunk_size):
h.update(chunk)
return h.hexdigest()
def build_manifest(source_dir):
manifest = {}
for p in Path(source_dir).rglob("*"):
if p.is_file() and p.stat().st_size > 0:
rel = str(p.relative_to(source_dir))
manifest[rel] = file_md5(p)
return manifest
def save_manifest(manifest, output_path):
Path(output_path).write_text(json.dumps(manifest, indent=2))
这里有一个坑要重点提醒:S3直接上传的文件的ETag,如果文件走了分片上传,ETag并不是整个文件的MD5,而是每个分片MD5拼接后再算的MD5,后面还带个“-分片数”后缀。所以不要直接用S3的ETag和本地MD5做比较,一定要维护一份属于自己的manifest文件。这是我在项目里踩过最深的坑之一。
3.3 EC2实例快照与AMI自动备份脚本
服务器迁移不能只传文件,还需要给现有云主机或虚拟机做镜像。如果源端是VMware或其他虚拟化平台,迁移到AWS常用工具是AWS Application Migration Service;但如果你只是需要把本地云平台的卷快照归档到S3,或者需要把线上实例的AMI定期备份,Python脚本也完全可以搞定。
我写过一个针对AWS EC2实例做定期快照的脚本,按标签筛选需要备份的实例,批量创建快照:
python复制from datetime import datetime
import boto3
session = boto3.Session(profile_name="migration")
ec2 = session.client("ec2")
def create_snapshot_for_instances(tag_key="backup", tag_value="true"):
resp = ec2.describe_instances(
Filters=[
{"Name": f"tag:{tag_key}", "Values": [tag_value]}
]
)
instance_ids = []
for reservation in resp["Reservations"]:
for instance in reservation["Instances"]:
instance_ids.append(instance["InstanceId"])
date_str = datetime.now().strftime("%Y%m%d-%H%M%S")
for instance_id in instance_ids:
desc = ec2.describe_instances(InstanceIds=[instance_id])
mappings = desc["Reservations"][0]["Instances"][0]["BlockDeviceMappings"]
for mapping in mappings:
volume_id = mapping["Ebs"]["VolumeId"]
snapshot = ec2.create_snapshot(
VolumeId=volume_id,
Description=f"snapshot for {instance_id} at {date_str}",
TagSpecifications=[{
"ResourceType": "snapshot",
"Tags": [
{"Key": "instance-id", "Value": instance_id},
{"Key": "created-by", "Value": "auto-backup"}
]
}]
)
print(f"Creating snapshot {snapshot['SnapshotId']} for {volume_id}")
if __name__ == "__main__":
create_snapshot_for_instances()
快照创建属于异步任务,调用create_snapshot接口之后,AWS会在后台处理。实际项目里,建议再加上一个轮询等待状态变为completed的逻辑,确保后续操作不会基于不完整的快照进行。脚本里的TagSpecifications也是关键,给快照打上标签,后续做成本分析和按实例清理快照都会非常方便。
3.4 数据库导出与导入的自动化辅助
数据库迁移是最繁琐的部分,因为不能简单复制文件。我这次用的是逻辑备份的方式,在源库执行mysqldump,然后压缩上传S3,在AWS侧下载导入RDS。这个过程也可以脚本化:
python复制import subprocess
from pathlib import Path
import boto3
DB_NAME = "appdb"
DB_USER = "root"
DB_PASSWORD = "your_password"
DUMP_PATH = Path("/tmp/appdb.sql.gz")
BUCKET_NAME = "my-app-backup-2024"
S3_KEY = "database/appdb.sql.gz"
def dump_mysql():
cmd = (
f"mysqldump --single-transaction --quick "
f"-u{DB_USER} -p{DB_PASSWORD} {DB_NAME} | gzip"
)
with open(DUMP_PATH, "wb") as out:
proc = subprocess.run(cmd, shell=True, stdout=out, stderr=subprocess.PIPE)
if proc.returncode != 0:
raise RuntimeError(f"mysqldump failed: {proc.stderr.decode()}")
def upload_to_s3():
session = boto3.Session(profile_name="migration")
s3_client = session.client("s3")
s3_client.upload_file(str(DUMP_PATH), BUCKET_NAME, S3_KEY)
print(f"Uploaded to s3://{BUCKET_NAME}/{S3_KEY}")
if __name__ == "__main__":
dump_mysql()
upload_to_s3()
mysqldump加--single-transaction可以保证在InnoDB引擎下导出时获得一致性的快照,同时不会长时间锁表。对于核心生产库,建议还是在业务低峰期操作,并且先在测试环境做一次完整的导入演练。导入RDS的时候,可能会遇到sql_mode、字符集、时区等差异,这些都需要提前在测试环境验证。
4. 踩坑记录:迁移中常见问题的排查与解决
4.1 大文件上传超时和连接重置
上传单个超过1GB的文件时,控制台经常卡住,后来检查发现是默认超时时间不够。手动上传时S3控制台对超大文件的支持很弱,而boto3的upload_file接口内部已经封装了分片上传和重试机制,用起来要靠谱得多。
如果用的是Request对象,而不是upload_file,就很容易遇到连接重置。正确的做法是尽量不自己拼接PUT请求,把rate limiter和重试都交给boto3的TransferConfig。如果网络环境差,还可以把max_concurrency调低一点,比如从10调到5,避免并发请求太多反而互相拖垮。
另一个经验:传输策略不要一直最高并发。对于跨境链路或带宽受限的机房,建议先用一个文件实际测速,判断单线程能达到多少,再推算需要多少并发。
4.2 权限错误五花八门,先看这几种
我把迁移期间遇到最多的几种权限报错整理成了表格,方便你对照排错:
| 错误现象 | 根本原因 | 解决办法 |
|---|---|---|
| 上传时AccessDenied | IAM策略缺少s3:PutObject权限 | 在策略中为对应桶和前缀添加PutObject权限 |
| 列出桶内对象时AccessDenied | 缺少s3:ListBucket权限 | 添加s3:ListBucket权限,Resource填桶ARN本身 |
| 创建快照时UnauthorizedOperation | IAM策略缺少ec2:CreateSnapshot | 添加ec2:CreateSnapshot和Describe权限 |
| 连接AWS超时 | 网络无法访问S3服务端点 | 检查出口网络、安全组、代理配置,确认能连通S3 endpoint |
| 时间偏差RequestTimeTooSkewed | 本地服务器系统时间不准确 | 用ntpdate同步系统时间 |
权限问题的排查思路很简单:先看是哪种API报错,再顺着API反查IAM策略里有没有对应Action。AWS的错误信息已经写得非常详细,不要凭感觉瞎猜。
4.3 增量同步怎么保证中途不重复传
迁移不是一次传完就结束,从全量到切换之间通常还有几轮增量。最简单的做法是每轮生成manifest时都用固定的md5值做对比,只传文件内容或大小有变化的文件。如果担心漏传,可以在脚本执行前先对比上一份manifest和当前manifest的差异清单,输出到日志里人工确认。
另外一个容易被忽略的点:S3里同一个key如果被重复覆盖,会产生多版本。如果桶开启了版本控制,会保留所有历史版本,存储成本会快速增长。增量同步阶段如果大量覆盖,建议关闭版本控制,或者定期配置生命周期规则清理旧版本。
4.4 切换前最后的完整性校验
正式切换流量之前,必须做一次完整的校验。文件类数据的校验方式是:对比源端文件总数、总大小、MD5清单;数据库则做行数统计、抽样查询、主外键校验。我习惯在切换窗口内跑一个独立的校验脚本,把所有结果输出到一个报告文件,存档备查。
校验脚本可以用boto3列出S3对象,和本地manifest做比对。对于S3对象的元数据,可以用head_object拿到ContentLength和LastModified,结合manifest里的MD5,形成三重校验结果。只要发现任何一个差异,就要停下来排查,而不是直接切换。
5. 封装成可复用的迁移工具包
迁移完成后,我并没有让这些脚本躺在服务器里吃灰,而是把它们重新整理成了一个轻量的内部工具包,所有脚本共用一套配置文件和日志规范。目录结构大致如下:
text复制migration-toolkit/
├── config.yaml
├── requirements.txt
├── s3_sync.py
├── snapshot_backup.py
├── db_migration.py
├── verify_manifest.py
└── utils/
├── aws_session.py
├── manifest.py
└── logger.py
配置文件用yaml维护,每个环境一套,避免把测试环境和生产环境的地址搞混。日志统一写到文件和控制台,每次执行都会留下完整痕迹,后续排查问题或者做审计都非常方便。
这里面最值得推荐的是utils/aws_session.py里的会话管理逻辑。所有脚本都从同一个入口获取boto3客户端,不再每段代码里重复写profile_name,省掉了大量的重复样板代码。
如果只是迁移一次,这些封装可能显得多余。但对于要持续维护多套环境的团队来说,把一次性的迁移脚本升级成可复用的工具包,后续做备份、容灾演练、环境复制都能直接收益。
最后再分享一个实际经验:任何云迁移项目,都要先把回滚方案想好。我这次迁移前在AWS上把目标环境完整建好,数据同步跑了一周,每次同步完都在目标端做应用功能测试,确认没有问题后才在切换窗口把流量拨过去。就算这样,切换当天我还是保留了源环境的只读权限,直到线上稳定运行两周后才彻底下线源端。这套“先备份、再演练、后切换、留后路”的节奏,建议所有做迁移的团队都严格执行。
