大家好,今天想分享一次生产环境架构迁移的完整经历。
表面上看,这次项目只是把前端服务迁移到 ECS,但真正实施后会发现,它远远不是一次简单的“前端切换”。
我们实际上对整套生产架构进行了一次系统性升级,涉及存储、数据库、后端、前端、网络、TLS 证书和 DNS 等多个层面。
整个迁移主要包括以下几个部分:
数据存储从 EC2 本地文件系统迁移到 S3
MySQL 从原有 EC2 迁移到 ECS 中的
crm-dbPHP 单体后端拆分并整合为
crm-backend管理后台前端独立为
crm-webNext.js 主站前端部署为独立服务
gp-web引入 ALB、Target Group 和 ACM 证书
通过 Cloudflare 完成 DNS 灰度切换
在新架构稳定后,逐步下线旧 EC2
这是一场真正意义上的分层迁移,而不是简单地替换某一个服务。

一、存储迁移:从 EC2 本地磁盘到 S3
首先处理的是文件存储。
旧系统中的静态资源和用户上传文件都保存在 EC2 的本地文件系统里。这种方式虽然简单,但会让应用与具体服务器强绑定。
一旦应用迁移到容器环境,问题就会变得明显:容器随时可能被替换或重新调度,本地文件不会天然保留,也无法在多个容器实例之间共享。
因此,这一步的目标非常明确:让文件存储与计算节点彻底解耦。
我们先梳理了 EC2 上的静态文件和上传文件,然后将它们同步到 S3 Bucket:
gp-fileserver
接下来,对后端的文件处理逻辑进行调整:
不再保存服务器本地文件路径
改为保存 S3 Object Key
上传文件时直接写入 S3
读取文件时根据 Object Key 获取资源
完整验证文件上传、访问和历史文件兼容性
这一步背后的核心原则是:
存储必须是无状态的,应用容器不能依赖本地磁盘。
完成改造以后,无论 ECS 启动多少个容器实例,也无论容器是否被重新部署,文件都不会受到影响。
二、数据库迁移:从 EC2 MySQL 到 crm-db
第二步是数据库迁移。
原来的生产数据库 gp_sales_prod 运行在旧 EC2 上。为了让数据库与遗留服务器解耦,我们将它迁移到了 ECS 中运行的 MySQL 服务 crm-db。
迁移流程本身比较传统。
首先导出生产数据库:
mysqldump gp_sales_prod > dump.sql然后将数据导入新的数据库:
mysql -h <crm-db-host> < dump.sql真正花费时间的部分,不是导出和导入,而是数据兼容性问题。
其中最典型的是排序规则冲突:
utf8mb4_0900_ai_ciutf8mb4_general_ci
除此之外,还需要处理字符集差异、外键约束以及不同 MySQL 版本之间的兼容问题。
数据库完成迁移后,我们还更新了所有应用的数据库连接配置,并验证了数据完整性和业务读写是否正常。
这部分给我们的经验是:
数据库迁移失败,很多时候并不是基础设施本身出了问题,而是排序规则、字符集和外键约束没有对齐。
所以在正式迁移之前,应该尽早确认以下信息:
源数据库和目标数据库的 MySQL 版本
数据库、数据表和字段使用的字符集
排序规则是否一致
外键导入顺序是否正确
应用账号是否具备所需权限
这些细节往往比创建一台新数据库更容易影响迁移进度。
三、后端重构:从 PHP 单体应用到 crm-backend
旧系统是一套 PHP 单体应用,同时承担了多种职责:
主站后端
管理后台后端
汇率更新任务
页面渲染
文件读写
数据库访问
这些功能集中在同一个项目中,不仅耦合严重,也让后续迁移和维护变得困难。
重构后,核心后端能力被统一整合到 ECS 服务:
crm-backend
这次重构主要有四个目标:
将业务逻辑与页面展示层分离
移除对本地文件系统的依赖
集中管理数据库访问
为未来迁移到 Java 技术栈做准备
这一步的意义不只是“把 PHP 放进容器”。
如果只是原封不动地把单体应用打包进容器,原有的耦合关系和维护问题仍然存在。真正有价值的工作,是在迁移过程中重新划分服务职责,让后端成为一个相对独立、稳定的业务服务。
四、管理后台简化:拆分为 crm-web
旧版管理后台的前端页面与 PHP 后端混合在一起。
为了减少迁移复杂度,我们没有在第一阶段追求完整重做,而是先收缩管理后台的功能范围,再将它拆分为独立的 ECS 服务:
crm-web
迁移后的管理后台只保留最核心的 CRUD 功能:
新闻内容管理
汇率数据管理
基础管理界面
这里采用的原则是:
在迁移之前,先缩小系统的功能边界。
迁移本身已经包含很多不确定因素。如果同时重写大量低频功能,会让验证范围迅速扩大,也会增加上线风险。
先保留最重要的能力,等新架构稳定之后再逐步扩展,整体上会更加可控。
五、Next.js 主站迁移:部署独立服务 gp-web
这次迁移中最复杂的部分,是将 Next.js 主站作为独立服务部署到 ECS。
新服务命名为:
gp-web
它不仅需要运行应用本身,还涉及一整套外围基础设施:
新建 ECS Task Definition
新建 ECS Service
创建 IP 类型的 Target Group
配置 ALB Listener
申请和验证 ACM 证书
更新 Cloudflare DNS
调整安全组访问规则
配置健康检查与滚动部署策略
最终的请求链路如下:
用户
↓
Cloudflare DNS
↓
公网 ALB
↓
Target Group(IP 模式,HTTP 3000)
↓
ECS gp-web由于使用的是 Fargate,Target Group 必须选择 IP 类型,不能使用传统的 Instance 类型。
Next.js 容器监听 3000 端口,同时必须绑定:
0.0.0.0:3000如果应用只监听 localhost,即使容器内部运行正常,ALB 也无法从容器外部访问它。
六、ALB、Target Group 与 ECS 配置
ALB 使用 Internet-facing 模式,对公网提供服务。
Listener 配置为:
协议:HTTPS
端口:443
证书:ACM 签发的正式证书
Target Group 的配置为:
Target Type:IP
协议:HTTP
端口:3000
健康检查路径:
/成功状态码:
200-399
ECS Task Definition 中,容器暴露 3000 端口。
ECS Service 初始配置为:
Desired Count:1
Minimum Healthy Percent:100
Maximum Percent:200
这套部署参数意味着:发布新版本时,ECS 会先尝试启动新的任务,在新任务健康后再停止旧任务,从而降低部署期间服务中断的风险。
不过,当 Desired Count 只有 1 时,仍然需要重点关注新任务的启动时间、健康检查宽限期以及集群资源是否足够。
七、迁移过程中遇到的四类典型故障
这次迁移最有价值的部分之一,是我们经历了几种非常典型的云上故障。
它们看起来都是“网站打不开”,但实际上分别发生在完全不同的架构层。
第一阶段:连接超时
最开始访问站点时,请求直接超时。
最终发现,ALB 的安全组没有开放 HTTPS 端口。
修复方式是在 ALB Security Group 中添加入站规则:
HTTPS 443
来源:0.0.0.0/0这个问题属于最外层的网络访问问题。
请求甚至还没有进入 ALB 的业务转发流程,就已经被安全组拦截。
第二阶段:证书不匹配
解决网络访问后,浏览器开始提示 TLS 证书与域名不匹配。
排查发现有两个原因:
ACM 证书中没有包含
www子域名ALB Listener 绑定了错误的证书
修复时,我们通过 Cloudflare 添加 DNS 验证记录,重新完成 ACM 证书验证,并将正确的证书绑定到 HTTPS Listener。
这类问题通常需要同时检查:
当前访问的完整域名
ACM 证书中的主域名和 SAN
ALB Listener 实际绑定的证书
Cloudflare 中的 DNS 验证记录
Cloudflare SSL 模式
第三阶段:504 Gateway Timeout
证书问题修复后,ALB 可以接收到请求,但开始返回 504 Gateway Timeout。
这意味着外部请求已经到达 ALB,但 ALB 无法在规定时间内连接到后端服务。
最终原因是:ECS 服务的安全组没有允许来自 ALB 的 3000 端口流量。
修复方式是在 ECS Security Group 中添加规则:
TCP 3000
来源:ALB Security Group这里不建议直接向公网开放容器的 3000 端口。更合理的方式,是只允许 ALB 所在的安全组访问 ECS 服务。
这样,请求只能通过 ALB 进入应用,容器本身不会直接暴露在公网。
第四阶段:503 Service Unavailable
网络打通后,部署过程中偶尔又出现了 503 Service Unavailable。
这次的原因不是端口不通,而是 Target Group 中暂时没有健康的目标。
在新版本部署时,旧任务已经进入 draining 状态,但新任务尚未完成注册和健康检查,因此出现了短暂的服务空窗期。
解决方法包括:
等待新任务完成注册
检查 Target Group 健康状态
调整 ECS 部署参数
设置合理的健康检查宽限期
避免旧任务过早退出
确保集群有足够资源同时运行新旧任务
这个问题提醒我们:滚动发布策略写在配置里,并不代表系统就一定能够做到零停机。
还必须同时满足资源容量、应用启动速度和健康检查设置等条件。
八、最终切换策略:先验证,再灰度切流
为了降低上线风险,我们没有直接关停旧 EC2,而是采用了灰度切换策略。
整个过程分为几个阶段:
保持旧 EC2 继续提供生产服务
部署新的 ECS 服务
通过 ALB 自带域名访问并测试新环境
验证文件上传和访问
验证数据库数据一致性
验证管理后台的 CRUD 操作
更新 Cloudflare DNS,将正式流量切换到 ALB
持续监控访问日志、错误率和 Target 健康状态
确认系统稳定后,再逐步下线旧 EC2
这种方式最大的价值,是让“部署新系统”和“正式切换流量”成为两个独立动作。
即使新环境出现问题,也不会立刻影响所有用户,还可以快速将流量切回旧环境。
九、迁移前后的架构变化
迁移前,所有核心能力都集中在一台 EC2 上:
单台 EC2
├── 本地文件系统
├── MySQL 数据库
├── PHP 后端
├── 管理后台
└── 主站前端任何一个模块出现资源竞争或部署故障,都可能影响整套系统。
迁移后,架构变成:
S3
└── 文件存储
ECS crm-db
└── 数据库
ECS crm-backend
└── 业务后端与 API
ECS crm-web
└── 管理后台
ECS gp-web
└── Next.js 主站前端
ALB
└── 流量入口与服务转发
ACM
└── TLS 证书
Cloudflare
└── DNS 与公网流量入口新的架构并不只是组件数量变多了,更重要的是每个组件的职责更加清晰。
存储不再依赖计算节点,前后端可以独立部署,外部流量通过统一入口进入,各个服务也可以单独扩展和维护。
十、这次迁移带来的工程能力提升
通过这次迁移,我们不仅完成了系统升级,也积累了一套更成熟的 DevOps 实践。
其中包括:
分层设计基础设施
将有状态应用改造成无状态服务
使用安全组描述服务之间的访问关系
通过健康检查定位后端状态
理解 ECS 滚动部署和零停机条件
处理 ACM 证书与 DNS 验证
设计可回滚的灰度切换方案
根据错误类型快速判断故障层级
这些能力比单纯掌握某个 AWS 服务更加重要。
因为生产环境中的问题通常不会只发生在一个组件里,而是发生在多个组件的连接处。
十一、最有价值的经验:根据错误判断故障层
这次迁移中,我认为最值得总结的经验是:
不同的错误类型,通常对应不同的架构层。
可以建立这样一套快速判断关系:
请求超时,通常说明流量连入口都没有到达。
证书不匹配,说明网络已经连通,但 TLS 配置存在问题。
出现 504,说明请求已经到达 ALB,但 ALB 无法正常连接后端服务。
出现 503,则通常意味着 ALB 可以工作,但当前没有可用的健康目标。
掌握这种映射关系以后,排查问题时就不需要在所有组件之间反复猜测,而是可以根据错误现象快速缩小范围。
结语
回头来看,这次迁移真正困难的地方,并不是创建 ECS Service、配置 ALB,或者修改一条 DNS 记录。
真正的挑战,是理解每一层的职责,以及这些层之间是如何连接的。
从本地文件迁移到 S3,是在解决状态管理问题。
从 EC2 数据库迁移到独立服务,是在解决数据层耦合问题。
拆分后端和管理前端,是在重新划分应用边界。
引入 ALB、ACM 和 Cloudflare,则是在重新设计整个系统的流量入口。
当这些层被逐一拆开、验证并重新连接以后,系统才真正从一台服务器上的单体应用,演进成一套职责清晰、可独立部署、可持续扩展的云上架构。
而这也是这次迁移最重要的价值:我们完成的不只是一次服务搬迁,而是一次完整的架构升级。