AWS CI/CD 实战系列 06:mfmsapp 版本演进实战(v1 → v2 → v3)

系列导读: 上一篇我们解决了 CodeCommit 模式的常见陷阱,终于有了一个稳定的 CI/CD 水线。现在,是时候实战演练了!本文将以 mfmsapp 为案例,演示三个版本的代码演进:从内存存储到 SQLite 持久化,再到蓝绿部署零停机升级——全程在 CodePipeline 上完成,展示真实的 CI/CD 工作流。


演进路线图

版本演进路线图

一句话总结三条路:

  • v1: 内存存储,最简原型,重启数据全丢失
  • v2: SQLite 持久化,数据落地,适合小规模生产
  • v3: 共享 RDS + 蓝绿部署,彻底零停机,面向高可用

为什么需要演进?

版本 数据存储 部署策略 停机时间 适用场景
v1 内存 map 就地部署 5-10秒 开发测试、PoC
v2 SQLite 文件 就地部署 5-10秒 中小规模生产
v3 共享 RDS 蓝绿部署 0秒 大规模、高可用

版本一:内存存储的单体 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  # systemd 服务文件

核心代码(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 标准库,零外部依赖,编译后就是一个静态二进制文件。

go.mod

module mfmsapp
go 1.21

appspec.yml

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

[Unit]
Description=mfmsapp v1.0.0 - Crop 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

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

部署验证

# 添加作物
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"}'

# 查询所有作物
curl http://localhost:8080/crops

# 重启验证数据丢失(内存存储特性)
sudo systemctl restart mfmsapp
curl http://localhost:8080/crops  # 返回空数组 []

问题暴露: 重启后数据全部丢失——生产环境无法接受。


版本二:引入 SQLite 持久化(v2.0.0)

技术选型: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 改造)

核心变化只有一个:把内存 map 换成 database.CropModel

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 database.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 database.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

# 添加数据
curl -X POST http://localhost:8080/crops \
  -d '{"id":"crop001","name":"小麦","type":"wheat","area":200,"planting_date":"2025-03-15","status":"growing"}'

# 重启服务
sudo systemctl restart mfmsapp

# 数据依然在!(SQLite 持久化)
curl http://localhost:8080/crops

问题暴露: v2 虽然数据不丢了,但使用就地部署,每次部署需要停旧进程 → 启动新进程,用户会看到 5-10 秒的服务不可用。对于 7×24 运行的农业管理系统,这不可接受。


版本三:蓝绿部署零停机升级(v3.0.0)

旧问题:就地部署需要停机

v2 的 CodeDeploy 部署流程:

  1. 停止旧版本进程
  2. 安装新版本文件
  3. 启动新版本

停机窗口 = 服务停止 + 安装 + 启动时间(约 5-15 秒)。

解决方案:蓝绿部署

根据 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 实例,不支持本地服务器。

蓝绿部署工作流程

  1. 以现有 Auto Scaling Group 为模板,创建新的替换实例
  2. 在新实例上安装新版应用
  3. 可选等待期:进行应用测试和系统验证
  4. 新实例注册到负载均衡器,流量开始路由到新实例
  5. 旧实例被注销,可终止或保留

蓝绿部署流程图

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 文件(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"

注意: 以上是 ECS 蓝绿部署的 AppSpec 格式。EC2/On-Premises 蓝绿部署沿用与 v1/v2 类似的格式(files + hooks),只是 Deployment Group 配置不同。

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 部署流程

  1. Source — CodeCommit 检测到新代码推送
  2. Build — CodeBuild 编译生成可执行文件
  3. Deploy(蓝绿部署):
    • CodeDeploy 创建新实例(绿色环境)
    • 在绿色实例上部署 v3
    • 等待 5 分钟:应用测试和系统验证
    • 绿色实例注册 ALB,流量从蓝色逐步切换到绿色
    • 蓝色实例从 ELB 注销
    • 10 分钟后,蓝色实例被终止

整个升级过程:0 秒停机。

回滚机制

场景 行为
绿色实例验证失败 CodeDeploy 自动停止切换,保持蓝色 100% 流量
绿色已上线但发现问题 控制台点击 Rollback → 流量瞬间切回蓝色

数据库策略

蓝绿部署的最大挑战是数据库一致性

方案 优点 缺点 适用场景
共享 RDS 简单,蓝绿用同一库 连接数翻倍 ✅ 推荐生产使用
DynamoDB 天然多可用区复制 需改数据模型 高扩展场景
S3 备份同步 灵活 数据丢失风险 开发测试
EFS 共享 文件系统共享 并发写有锁问题 ❌ 不推荐

推荐方案:Amazon RDS(PostgreSQL/MySQL)

  • 蓝绿两组实例共享同一个 RDS
  • RDS 提供自动备份、高可用、监控
  • 连接数 = ASG 实例数 × 每应用连接数 + 缓冲(例如 4 实例 × 10 连接 → max_connections=50)

v3 go.mod

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="postgresql://admin:pass@mfmsapp-db.cluster.ap-northeast-1.rds.amazonaws.com:5432/mfmsapp"
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 \
    --s3-location bucket=mybucket,key=artifact.zip,bundleType=zip

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 \
    --engine-version 15 \
    --master-username admin \
    --master-user-password YOUR_PASSWORD \
    --allocated-storage 20 \
    --db-subnet-group-name mfmsapp-subnet \
    --vpc-security-group-ids sg-xxxxxxxx

常见问题

Q1: 蓝绿部署时,绿色实例健康检查失败?

  1. SSH 登录绿色实例(通过 ASG 找到实例 ID)
  2. 查看日志:sudo journalctl -u mfmsapp -f
  3. 检查健康端点:curl http://localhost:8080/crops
  4. 修复代码,重新部署

Q2: 数据库连接数暴增?

蓝绿两组实例同时连接,连接数翻倍。

方案: max_connections = 实例总数 × 每应用连接数 + 缓冲

  • 例:蓝 2 + 绿 2 = 4 实例,每个 10 连接 → max_connections = 50

Q3: 流量切换太慢?

调整部署组配置:

--blue-green-deployment-configuration '{
    "deploymentReadyOption": {
        "actionOnTimeout": "CONTINUE_DEPLOYMENT",
        "waitTimeInMinutes": 1
    }
}'

把等待期从 5 分钟降为 1 分钟。


后续演进建议

  1. 数据库升级: SQLite → RDS PostgreSQL(多实例必须共享存储)
  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"]

本文小结

  1. v1 → v2(内存 → SQLite): 解决数据持久化,增加数据库初始化、schema、数据访问层(DAS)
  2. v2 → v3(就地 → 蓝绿): 解决零停机,引入 ALB + 双 ASG + CodeDeploy 蓝绿配置
  3. 生产建议: 定期演练回滚,确保灾难恢复能力

下一篇预告

第 07 篇:权限安全深度解析——IAM 最小权限、KMS 加密、VPC 构建

本文专注功能实现,但安全不能忽视!下一篇详细拆解:

  • IAM 角色权限最小化(Pipeline、CodeBuild、CodeDeploy 各需要什么权限?)
  • KMS 加密 S3 artifact 和 RDS 数据库
  • 将 CI/CD 流水线放入 VPC,禁止公网访问
  • 审计日志(CloudTrail)与合规检查

本文档内容基于 2026 年 4 月 AWS 官方文档整理。部署类型、计算平台支持、API 参数等均已核对 AWS CodeDeploy 用户指南。如遇差异请以 AWS 官方文档为准。