AWS CI/CD 实战系列 06:mfmsapp 版本演进实战(v1→v2→v3)
系列导读: 上一篇我们解决了 CodeCommit 模式的常见陷阱,终于有了一个稳定的 CI/CD 水线。现在,是时候实战演练了!本文将以 mfmsapp 为案例,演示三个版本的代码演进:从内存存储到 SQLite 持久化,再到蓝绿部署零停机升级。全程在 CodePipeline 上操作,展示真实的 CI/CD 工作流。
演进路线图
为什么要演进?
| 版本 | 数据存储 | 部署策略 | 停机时间 | 适用场景 |
|---|---|---|---|---|
| v1 | 内存 map | 就地部署 | 5-10秒 | 开发测试、PoC |
| v2 | SQLite 文件 | 就地部署 | 5-10秒 | 中小规模生产 |
| v3 | 共享 RDS | 蓝绿部署 | 0秒 | 大规模、高可用 |
- v1 → v2: 数据不能持久化,服务重启就丢失所有作物记录,无法用于生产
- v2 → v3: 就地部署需要停机(停止旧进程 → 启动新进程),用户会看到服务暂时不可用
版本一:内存存储的单体 Go 应用(v1.0.0)
功能概述
最简单的 mfmsapp:REST API 端口 8080,数据存储在内存 map[string]Crop(应用重启清空),支持 CRUD 操作。
项目结构
mfmsapp-v1/
├── main.go # 入口文件(纯标准库,无外部依赖)
├── go.mod
├── appspec.yml # CodeDeploy 部署规范
├── buildspec.yml # CodeBuild 构建配置
└── scripts/
├── before_install.sh
├── after_install.sh
├── start_server.sh
├── validate_service.sh
└── mfmsapp.service
核心代码(main.go)
package main
import (
"encoding/json"
"log"
"net/http"
"sync"
)
type Crop struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Area float64 `json:"area"`
PlantingDate string `json:"planting_date"`
Status string `json:"status"`
}
type Server struct {
crops map[string]Crop
mu sync.RWMutex
}
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func main() {
s := &Server{crops: make(map[string]Crop)}
http.HandleFunc("/crops", s.cropsHandler)
http.HandleFunc("/crops/", s.cropDetailHandler)
log.Println("mfmsapp v1.0.0 starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func (s *Server) cropsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
s.mu.RLock()
crops := make([]Crop, 0, len(s.crops))
for _, c := range s.crops { crops = append(crops, c) }
s.mu.RUnlock()
json.NewEncoder(w).Encode(Response{Success: true, Data: crops})
case http.MethodPost:
var crop Crop
if err := json.NewDecoder(r.Body).Decode(&crop); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
s.mu.Lock()
s.crops[crop.ID] = crop
s.mu.Unlock()
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Response{Success: true, Data: crop})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) cropDetailHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
id := r.URL.Path[len("/crops/"):]
s.mu.RLock()
crop, exists := s.crops[id]
s.mu.RUnlock()
if !exists { http.Error(w, "Crop not found", http.StatusNotFound); return }
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(Response{Success: true, Data: crop})
case http.MethodPut:
var updated Crop
if err := json.NewDecoder(r.Body).Decode(&updated); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest); return
}
updated.ID = id
s.mu.Lock()
s.crops[id] = updated
s.mu.Unlock()
json.NewEncoder(w).Encode(Response{Success: true, Data: updated})
case http.MethodDelete:
s.mu.Lock()
delete(s.crops, id)
s.mu.Unlock()
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
关键配置文件
go.mod(纯标准库,无外部依赖)
module mfmsapp
go 1.21
appspec.yml(CodeDeploy 部署规范)
version: 0.0
os: linux
files:
- source: /
destination: /opt/mfmsapp
hooks:
BeforeInstall:
- location: scripts/before_install.sh
timeout: 60
runas: root
AfterInstall:
- location: scripts/after_install.sh
timeout: 60
runas: root
ApplicationStart:
- location: scripts/start_server.sh
timeout: 60
runas: root
ValidateService:
- location: scripts/validate_service.sh
timeout: 300
runas: root
scripts/before_install.sh
#!/bin/bash
systemctl stop mfmsapp || true
sleep 3
scripts/after_install.sh
#!/bin/bash
chmod +x /opt/mfmsapp/mfmsapp
cp /opt/mfmsapp/scripts/mfmsapp.service /etc/systemd/system/
systemctl daemon-reload
scripts/start_server.sh
#!/bin/bash
systemctl start mfmsapp
sleep 2
scripts/validate_service.sh
#!/bin/bash
for i in {1..15}; do
if curl -sf http://localhost:8080/crops > /dev/null; then
echo "mfmsapp v1 is healthy"
exit 0
fi
sleep 2
done
echo "mfmsapp v1 failed to start"
exit 1
scripts/mfmsapp.service(systemd 服务文件)
[Unit]
Description=mfmsapp v1.0.0 - Modern Farm Management System
After=network.target
[Service]
Type=simple
User=ec2-user
WorkingDirectory=/opt/mfmsapp
ExecStart=/opt/mfmsapp/mfmsapp
Restart=always
RestartSec=5
[Install]
WantedBy=multi-user.target
buildspec.yml(CodeBuild 构建配置)
version: 0.2
phases:
install:
runtime-versions:
go: 1.21
build:
commands:
- go build -o mfmsapp .
- mkdir -p artifact
- cp mfmsapp appspec.yml artifact/
- cp -r scripts artifact/scripts
- chmod +x artifact/scripts/*.sh
artifacts:
files:
- '**/*'
base-directory: artifact
部署验证
# 1. 添加作物
curl -X POST http://localhost:8080/crops \
-H "Content-Type: application/json" \
-d '{"id":"crop001","name":"水稻","type":"rice","area":150.5,"planting_date":"2025-04-01","status":"growing"}'
# 2. 查询所有作物
curl http://localhost:8080/crops
# 3. 重启验证数据丢失(内存存储特性)
sudo systemctl restart mfmsapp
curl http://localhost:8080/crops # 返回空数组 []
版本二:引入 SQLite 持久化(v2.0.0)
为什么需要数据库?
v1 的数据存在内存里,一重启就丢。生产环境必须持久化。
技术选型:SQLite
- 零配置,单文件数据库,适合中小规模应用
- Go 标准库
database/sql+github.com/mattn/go-sqlite3驱动 - 与现有单体架构兼容性好,无需额外数据库服务
新增文件结构
mfmsapp-v2/
├── main.go # 修改:改用数据库替代内存存储
├── database/
│ ├── db.go # 数据库连接和初始化
│ ├── schema.sql # 表结构定义
│ └── models.go # 数据访问层
├── go.mod # 添加 sqlite3 依赖
└── appspec.yml # 微调:增加数据库目录创建
database/schema.sql
CREATE TABLE IF NOT EXISTS crops (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
type TEXT NOT NULL,
area REAL NOT NULL,
planting_date TEXT NOT NULL,
status TEXT NOT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
CREATE INDEX IF NOT EXISTS idx_crops_type ON crops(type);
CREATE INDEX IF NOT EXISTS idx_crops_status ON crops(status);
database/db.go
package database
import (
"database/sql"
"log"
"os"
_ "github.com/mattn/go-sqlite3"
)
var DB *sql.DB
func InitDB(dataSource string) error {
var err error
DB, err = sql.Open("sqlite3", dataSource)
if err != nil {
return err
}
if err = DB.Ping(); err != nil {
return err
}
schema, err := os.ReadFile("database/schema.sql")
if err != nil {
return err
}
_, err = DB.Exec(string(schema))
if err != nil {
return err
}
log.Println("Database initialized at:", dataSource)
return nil
}
database/models.go
package database
type Crop struct {
ID string `json:"id"`
Name string `json:"name"`
Type string `json:"type"`
Area float64 `json:"area"`
PlantingDate string `json:"planting_date"`
Status string `json:"status"`
}
type CropModel struct{}
func (m *CropModel) Create(crop *Crop) error {
_, err := DB.Exec(
`INSERT INTO crops (id, name, type, area, planting_date, status)
VALUES (?, ?, ?, ?, ?, ?)`,
crop.ID, crop.Name, crop.Type, crop.Area, crop.PlantingDate, crop.Status,
)
return err
}
func (m *CropModel) GetAll() ([]Crop, error) {
rows, err := DB.Query(`SELECT id, name, type, area, planting_date, status FROM crops`)
if err != nil { return nil, err }
defer rows.Close()
var crops []Crop
for rows.Next() {
var c Crop
if err := rows.Scan(&c.ID, &c.Name, &c.Type, &c.Area, &c.PlantingDate, &c.Status); err != nil {
return nil, err
}
crops = append(crops, c)
}
return crops, nil
}
func (m *CropModel) GetByID(id string) (*Crop, error) {
row := DB.QueryRow(
`SELECT id, name, type, area, planting_date, status FROM crops WHERE id = ?`, id)
var crop Crop
err := row.Scan(&crop.ID, &crop.Name, &crop.Type, &crop.Area, &crop.PlantingDate, &crop.Status)
if err != nil { return nil, err }
return &crop, nil
}
func (m *CropModel) Update(crop *Crop) error {
_, err := DB.Exec(
`UPDATE crops SET name=?, type=?, area=?, planting_date=?, status=?, updated_at=CURRENT_TIMESTAMP
WHERE id=?`,
crop.Name, crop.Type, crop.Area, crop.PlantingDate, crop.Status, crop.ID)
return err
}
func (m *CropModel) Delete(id string) error {
_, err := DB.Exec(`DELETE FROM crops WHERE id = ?`, id)
return err
}
main.go(v2 改造)
package main
import (
"encoding/json"
"log"
"net/http"
"mfmsapp/database"
)
type Server struct {
model *database.CropModel
}
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
}
func main() {
if err := database.InitDB("/opt/mfmsapp/data/mfmsapp.db"); err != nil {
log.Fatal("Failed to init database:", err)
}
s := &Server{model: &database.CropModel{}}
http.HandleFunc("/crops", s.cropsHandler)
http.HandleFunc("/crops/", s.cropDetailHandler)
log.Println("mfmsapp v2.0.0 starting on :8080 (SQLite)")
log.Fatal(http.ListenAndServe(":8080", nil))
}
func (s *Server) cropsHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
switch r.Method {
case http.MethodGet:
crops, err := s.model.GetAll()
if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return }
json.NewEncoder(w).Encode(Response{Success: true, Data: crops})
case http.MethodPost:
var crop Crop
if err := json.NewDecoder(r.Body).Decode(&crop); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest); return
}
if err := s.model.Create(&crop); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError); return
}
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(Response{Success: true, Data: crop})
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func (s *Server) cropDetailHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
id := r.URL.Path[len("/crops/"):]
crop, err := s.model.GetByID(id)
if err != nil { http.Error(w, "Crop not found", http.StatusNotFound); return }
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(Response{Success: true, Data: crop})
case http.MethodPut:
var updated Crop
if err := json.NewDecoder(r.Body).Decode(&updated); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest); return
}
updated.ID = id
if err := s.model.Update(&updated); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError); return
}
json.NewEncoder(w).Encode(Response{Success: true, Data: updated})
case http.MethodDelete:
if err := s.model.Delete(id); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError); return
}
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
go.mod
module mfmsapp
go 1.21
require github.com/mattn/go-sqlite3 v1.14.22
scripts/after_install.sh(新增数据库目录创建)
#!/bin/bash
chmod +x /opt/mfmsapp/mfmsapp
mkdir -p /opt/mfmsapp/data
chown ec2-user:ec2-user /opt/mfmsapp/data
cp /opt/mfmsapp/scripts/mfmsapp.service /etc/systemd/system/
systemctl daemon-reload
验证v2部署
# 1. 添加数据
curl -X POST http://localhost:8080/crops \
-d '{"id":"crop001","name":"小麦","type":"wheat","area":200,"planting_date":"2025-03-15","status":"growing"}'
# 2. 重启服务
sudo systemctl restart mfmsapp
# 3. 再次查询,数据应该还在(SQLite持久化)
curl http://localhost:8080/crops
版本三:蓝绿部署零停机升级(v3.0.0)
v2 的问题:部署需要停机
v2 使用就地部署(In-place Deployment):
- CodeDeploy 停止旧版本进程
- 安装新版本
- 启动新版本
停机窗口 = 服务停止 + 安装 + 启动时间(几秒到几十秒)。生产环境无法接受,尤其是 7×24 小时运行的农业管理系统。
解决方案:蓝绿部署
根据 AWS CodeDeploy 官方文档,CodeDeploy 支持三种计算平台:
| 计算平台 | 支持的部署类型 | 流量切换方式 |
|---|---|---|
| EC2/On-Premises | 就地部署 或 蓝绿部署 | 注册/注销 ELB 实例 |
| AWS Lambda | 仅蓝绿部署 | canary / linear / all-at-once |
| Amazon ECS | 仅蓝绿部署 | canary / linear / all-at-once |
重要: EC2/On-Premises 蓝绿部署仅支持 Amazon EC2 实例,不支持本地(on-premises)服务器。
蓝绿部署工作流程(EC2/On-Premises 计算平台)
- 使用现有 Auto Scaling Group 作为模板创建替换环境的新实例
- 在新实例上安装应用程序修订版
- 可选等待时间:进行应用程序测试和系统验证
- 替换环境中的实例注册到一个或多个负载均衡器,流量开始路由到新实例
- 原始环境中的实例被注销,可终止或保留继续使用

v3 架构变更
需要的基础设施:
┌─────────────┐
│ ALB │
│ (负载均衡器) │
└──────┬──────┘
│ 流量路由
┌────────────┼────────────┐
│ │
┌─────────┴─────────┐ ┌──────────┴──────────┐
│ 蓝色目标组 │ │ 绿色目标组 │
│ tf-mfmsapp-blue │ │ tf-mfmsapp-green │
└─────────┬─────────┘ └──────────┬──────────┘
│ │
┌─────────┴─────────┐ ┌──────────┴──────────┐
│ ASG (blue) │ │ ASG (green) │
│ Min=2,Desired=2 │ │ Min=2,Desired=2 │
│ [EC2] [EC2] │ │ [EC2] [EC2] │
└───────────────────┘ └─────────────────────┘
│ │
└────────────┬────────────┘
│
┌──────┴──────┐
│ RDS │
│ (PostgreSQL)│
└─────────────┘
v3 AppSpec 文件
# Amazon ECS 蓝绿部署
version: 0.0
Resources:
- TargetService:
Type: AWS::ECS::Service
Properties:
TaskDefinition: mfmsapp-task-def:3
LoadBalancerInfo:
ContainerName: mfmsapp
ContainerPort: 8080
PlatformVersion: LATEST
Hooks:
- BeforeInstall: "LambdaFunctionToValidateBeforeInstall"
- AfterInstall: "LambdaFunctionToValidateAfterInstall"
- AfterAllowTestTraffic: "LambdaFunctionToValidateTestTraffic"
- BeforeAllowTraffic: "LambdaFunctionToValidateBeforeShiftTraffic"
- AfterAllowTraffic: "LambdaFunctionToValidateAfterShiftTraffic"
注意: EC2/On-Premises 蓝绿部署和 ECS 蓝绿部署的 AppSpec 格式不同,以上为 ECS 蓝绿的 AppSpec 格式。EC2/On-Premises 蓝绿部署使用与之前类似的格式(files + hooks)。
v3 部署组配置(AWS CLI)
aws deploy create-deployment-group \
--application-name mfmsapp \
--deployment-group-name mfmsapp-bluegreen \
--service-role-arn "$CODEDEPLOY_ROLE_ARN" \
--auto-scaling-groups "asg-mfmsapp-blue" \
--deployment-config-name CodeDeployDefault.OneAtATime \
--load-balancer-info "{
\"elbInfoList\": [{ \"name\": \"mfmsapp-alb\" }],
\"targetGroupInfoList\": [
{ \"name\": \"tf-mfmsapp-blue\" },
{ \"name\": \"tf-mfmsapp-green\" }
]
}" \
--blue-green-deployment-configuration '{
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 10
},
"deploymentReadyOption": {
"actionOnTimeout": "CONTINUE_DEPLOYMENT",
"waitTimeInMinutes": 5
}
}'
v3 部署流程
- Source 阶段: CodeCommit 检测到新代码推送
- Build 阶段: CodeBuild 编译生成
mfmsapp可执行文件 - Deploy 阶段(蓝绿部署):
- CodeDeploy 以现有 Auto Scaling Group 为模板创建新实例(绿色环境)
- 在绿色实例上部署新版本应用(v3)
- 等待期(可配置 5 分钟):进行应用程序测试和系统验证
- 绿色实例注册到负载均衡器,流量从蓝色逐步切换到绿色
- 蓝色实例从 ELB 注销
- 10 分钟后(可配置),蓝色实例被终止
整个升级过程:0 秒停机。
回滚机制
如果绿色实例验证失败:
- CodeDeploy 自动停止流量切换
- 保持蓝色 100% 流量
- 绿色实例被终止
如果绿色已经上线但发现问题:
- 在 CodeDeploy 控制台点击 "Rollback"
- 流量瞬间切回蓝色(LB 重新指向蓝色 Target Group)
- 只要蓝色实例未被终止,回滚是秒级的
数据库策略
蓝绿部署的最大挑战是数据库一致性:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| 共享 RDS | 简单,蓝绿用同一数据库 | 连接数翻倍 | 推荐生产使用 |
| DynamoDB | 天然多可用区复制 | 需改变数据模型 | 高扩展场景 |
| S3 备份同步 | 灵活 | 有数据丢失风险 | 开发测试 |
| EFS 共享 | 文件系统共享 | 并发写有锁问题 | 不推荐 |
推荐方案: 使用 Amazon RDS(PostgreSQL/MySQL)替代 SQLite。
- 蓝绿两组实例共享同一个 RDS 实例
- RDS 提供自动备份、高可用、监控
- 连接数 = ASG 实例数 × 每个应用连接数 + 缓冲
go.mod 改动(v3 需要添加 AWS SDK 依赖)
module mfmsapp
go 1.21
require (
github.com/mattn/go-sqlite3 v1.14.22
github.com/aws/aws-sdk-go v1.55.0
)
环境变量配置(v3 改进)
port := os.Getenv("PORT")
if port == "" { port = "8080" }
dbURL := os.Getenv("DATABASE_URL")
if dbURL == "" { dbURL = "/opt/mfmsapp/data/mfmsapp.db" }
在 start_server.sh 中注入环境变量:
#!/bin/bash
export PORT=8080
export DATABASE_URL="/opt/mfmsapp/data/mfmsapp.db"
systemctl start mfmsapp
三种部署模式对比总结
| 对比项 | v1(内存存储) | v2(SQLite 就地) | v3(蓝绿部署) |
|---|---|---|---|
| 数据持久化 | ❌ 重启丢失 | ✅ SQLite 文件 | ✅ 共享 RDS |
| 部署策略 | 就地部署 | 就地部署 | 蓝绿部署 |
| 停机时间 | 5-10秒 | 5-10秒 | 0秒 |
| 资源成本 | 1 组 EC2 | 1 组 EC2 | 2 组 EC2 + ALB |
| 回滚速度 | 重新部署 | 重新部署 | 秒级(LB 切换) |
| 复杂度 | ⭐ 简单 | ⭐⭐ 中等 | ⭐⭐⭐⭐ 复杂 |
| 适用场景 | 开发测试、PoC | 中小规模生产 | 大规模、高可用 |
实际部署命令速查
v1/v2 就地部署
# 查看部署历史
aws deploy list-deployments \
--application-name mfmsapp \
--deployment-group-name mfmsapp-production
# 创建新部署
aws deploy create-deployment \
--application-name mfmsapp \
--deployment-group-name mfmsapp-production \
--revision revisionType=CodeCommit,gitCommitId=abc123def
v3 蓝绿部署
# 查看蓝绿部署状态
aws deploy get-deployment --deployment-id d-XXXXXXXX
# 手动停止部署(紧急回滚)
aws deploy stop-deployment --deployment-id d-XXXXXXXX
创建 RDS PostgreSQL
aws rds create-db-instance \
--db-instance-identifier mfmsapp-db \
--db-instance-class db.t3.micro \
--engine postgres \
--master-username admin \
--master-user-password YOUR_PASSWORD \
--allocated-storage 20
常见问题与排障
Q1: 蓝绿部署时,绿色实例健康检查失败怎么办?
原因: 应用启动失败或配置错误。
解决:
- SSH 登录绿色实例(通过 ASG 找到实例 ID)
- 查看应用日志:
sudo journalctl -u mfmsapp -f - 检查健康检查端点:
curl http://localhost:8080/crops - 修复代码,重新部署
Q2: 蓝绿部署后,数据库连接数暴增?
原因: 蓝绿两组实例同时连接数据库,连接数翻倍。
解决:
- RDS 配置
max_connections = ASG总数 × 每应用连接数 + 缓冲 - 例如:蓝 2 实例 + 绿 2 实例,每个 10 连接 →
max_connections = 50
Q3: CodeDeploy 蓝绿部署的流量切换太慢?
在创建部署组时调整配置:
--blue-green-deployment-configuration '{
"terminateBlueInstancesOnDeploymentSuccess": {
"action": "TERMINATE",
"terminationWaitTimeInMinutes": 5
},
"deploymentReadyOption": {
"actionOnTimeout": "CONTINUE_DEPLOYMENT",
"waitTimeInMinutes": 1
}
}'
后续演进建议
1. 数据库升级:SQLite → RDS PostgreSQL
- SQLite 发性能差,不适合多实例
- 蓝绿部署需要共享数据存储
- 迁移步骤:导出 SQLite → 转换 SQL → 导入 RDS → 修改 DATABASE_URL
2. 无服务器化:EC2 → Lambda + API Gateway
- 零运维,自动扩缩容
- 成本更低(按执行次数计费)
- 注意冷启动延迟(几秒)
3. 容器化:Docker + ECS
FROM golang:1.21-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build -o mfmsapp .
FROM alpine:latest
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=builder /app/mfmsapp .
COPY --from=builder /app/database/schema.sql ./database/
EXPOSE 8080
CMD ["./mfmsapp"]
本文小结
- v1 → v2(内存 → SQLite): 解决数据持久化,增加数据库初始化、schema、数据访问层
- v2 → v3(就地 → 蓝绿): 解决零停机,引入 ALB + 双 ASG + CodeDeploy 蓝绿配置
- 关键要点:
- 数据库迁移注意向后兼容(schema 变更时字段允许 NULL)
- 蓝绿部署需要共享数据库(推荐 RDS)
- 回滚窗口内可秒级恢复
- 生产建议: 定期演练回滚,确保灾难恢复能力
下一篇预告
第 07 篇:权限安全深度解析——IAM 最小权限、KMS 加密、VPC 构建
本文专注功能实现,但安全不能忽视!下一篇详细拆解:
- IAM 角色权限最小化(Pipeline、CodeBuild、CodeDeploy 各需要什么权限?)
- KMS 加密 S3 artifact 和 RDS 数据库
- 将 CI/CD 流水线放入 VPC,禁止公网访问
- 审计日志(CloudTrail)与合规检查
打造一个企业级安全的 AWS CI/CD 流水线。
本文档内容基于 2026 年 4 月 AWS 官方文档整理。部署类型、计算平台支持、API 参数等均已核对 AWS CodeDeploy 用户指南。如遇差异请以 AWS 官方文档为准。