系列导读: 第一篇我们对比了 S3 触发和 CodeCommit 触发两种架构。本篇开始实战,从零搭建 S3 触发模式的完整 CodePipeline,覆盖 S3、CodeBuild、CodeDeploy、IAM 权限全链路配置。


目标

完成本篇后,你将拥有一个可工作的 CI/CD 流水线

S3 触发模式 CI/CD 流水线架构


准备工作

在开始之前,你需要:

  • 一个 AWS 账号

  • AWS CLI 已安装并配置好凭证

  • 一台 Amazon EC2 实例(Amazon Linux 2023),已安装 CodeDeploy Agent

检查 AWS CLI 配置

aws sts get-caller-identity
# 应返回当前账号、用户ID、ARN

第一步:创建 S3 源存储桶

CodePipeline 需要一个 S3 桶作为源代码来源。每次上传新文件,流水线就会自动触发。

# 设置变量
export AWS_REGION="ap-northeast-1"  # 替换为你的区域
export SOURCE_BUCKET="awscodepipeline-demo-${RANDOM}"

# 1. 创建 S3 桶
aws s3api create-bucket \
    --bucket "$SOURCE_BUCKET" \
    --region "$AWS_REGION" \
    --create-bucket-configuration LocationConstraint="$AWS_REGION"

# 2. 启用版本控制(⚠️ 必须启用,否则 CodePipeline 无法检测更新)
aws s3api put-bucket-versioning \
    --bucket "$SOURCE_BUCKET" \
    --versioning-configuration Status=Enabled

echo "源桶已创建: $SOURCE_BUCKET"

⚠️ 避坑 1:S3 桶必须启用版本控制! 如果不启用,CodePipeline 只会检测到第一次上传,后续覆盖上传同一个文件不会触发流水线。这是最常见的错误之一。


第二步:准备 mfmsapp 源代码

我们把 mfmsapp(现代化农场管理系统)的源代码打包上传到 S3。

目录结构

mfmsapp-v1/
├── main.go          # Go 源码
├── go.mod           # Go 模块定义
├── buildspec.yml    # CodeBuild 构建配置
├── appspec.yml      # CodeDeploy 部署配置
└── scripts/
    └── start.sh     # 启动脚本

1. main.go

package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os/exec"
	"strings"
	"time"
)

type Crop struct {
	ID    int    `json:"id"`
	Name  string `json:"name"`
	Area  string `json:"area"`
	Date  string `json:"date"`
}

var crops = []Crop{
	{1, "小麦", "A区", time.Now().Format("2006-01-02")},
	{2, "玉米", "B区", time.Now().Format("2006-01-02")},
}

func main() {
	http.HandleFunc("/api/crops", handleCrops)
	http.HandleFunc("/api/crops/", handleCropByID)
	http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
		w.Header().Set("Content-Type", "application/json")
		json.NewEncoder(w).Encode(map[string]string{
			"status": "ok",
			"time":   time.Now().Format("2006-01-02 15:04:05"),
			"version": getVersion(),
		})
	})

	port := ":8080"
	log.Printf("mfmsapp v1 listening on %s", port)
	log.Fatal(http.ListenAndServe(port, nil))
}

func getVersion() string {
	cmd := exec.Command("date", "+%Y%m%d%H%M")
	out, err := cmd.Output()
	if err != nil {
		return "unknown"
	}
	return strings.TrimSpace(string(out))
}

func handleCrops(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	if r.Method == http.MethodPost {
		var crop Crop
		json.NewDecoder(r.Body).Decode(&crop)
		crop.ID = len(crops) + 1
		crop.Date = time.Now().Format("2006-01-02")
		crops = append(crops, crop)
		w.WriteHeader(http.StatusCreated)
		json.NewEncoder(w).Encode(crop)
		return
	}
	json.NewEncoder(w).Encode(crops)
}

func handleCropByID(w http.ResponseWriter, r *http.Request) {
	w.Header().Set("Content-Type", "application/json")
	idStr := strings.TrimPrefix(r.URL.Path, "/api/crops/")
	for _, c := range crops {
		if fmt.Sprintf("%d", c.ID) == idStr {
			json.NewEncoder(w).Encode(c)
			return
		}
	}
	w.WriteHeader(http.StatusNotFound)
	json.NewEncoder(w).Encode(map[string]string{"error": "not found"})
}

2. go.mod

module mfmsapp

go 1.21

3. buildspec.yml(⚠️ 关键文件)

version: 0.2

phases:
  install:
    runtime-versions:
      golang: 1.21
    commands:
      - echo "=== Installing Go dependencies ==="
      - go version
  pre_build:
    commands:
      - echo "=== Pre-build: Testing ==="
      - echo "No tests configured for v1"
  build:
    commands:
      - echo "=== Building mfmsapp ==="
      - go build -o mfmsapp .
      - echo "Build complete, binary size:"
      - ls -lh mfmsapp
  post_build:
    commands:
      - echo "=== Post-build: Preparing artifacts ==="
      - mkdir -p artifact
      - cp mfmsapp artifact/
      - cp appspec.yml artifact/
      - cp scripts/start.sh artifact/scripts/
      - echo "Artifact prepared"

artifacts:
  files:
    - mfmsapp
    - appspec.yml
    - scripts/start.sh
  discard-paths: yes
  base-directory: artifact

4. appspec.yml(⚠️ 关键文件)

version: 0.0
os: linux
files:
  - source: mfmsapp
    destination: /opt/mfmsapp
  - source: scripts/start.sh
    destination: /opt/mfmsapp/scripts
hooks:
  BeforeInstall:
    - location: scripts/before_install.sh
      timeout: 60
      runas: root
  ApplicationStart:
    - location: scripts/after_install.sh
      timeout: 60
      runas: root

5. scripts/start.sh

#!/bin/bash
# mfmsapp start script
cd /opt/mfmsapp
nohup ./mfmsapp > /var/log/mfmsapp.log 2>&1 &
echo "mfmsapp started, PID: $!"

6. scripts/before_install.sh

#!/bin/bash
# 停止旧版本
if pgrep mfmsapp > /dev/null; then
    echo "Stopping existing mfmsapp..."
    pkill mfmsapp || true
    sleep 2
fi
echo "BeforeInstall complete"

7. scripts/after_install.sh

#!/bin/bash
# 设置执行权限
chmod +x /opt/mfmsapp/mfmsapp
# 启动新版本
cd /opt/mfmsapp
nohup ./mfmsapp > /var/log/mfmsapp.log 2>&1 &
echo "AfterInstall complete, PID: $!"
sleep 2
# 验证服务是否正常
curl -s http://localhost:8080/health || echo "Health check failed"

8. 打包并上传到 S3

# 确保脚本有执行权限
chmod +x scripts/*.sh

# 打包(⚠️ 不要加顶层目录,直接打包文件)
zip -r mfmsapp-v1.zip \
    main.go go.mod buildspec.yml appspec.yml \
    scripts/

# 上传到 S3(⚠️ 文件名必须和 CodePipeline 配置的一致)
aws s3 cp mfmsapp-v1.zip s3://$SOURCE_BUCKET/mfmsapp-v1.zip

echo "✅ 源码已上传到 S3: s3://${SOURCE_BUCKET}/mfmsapp-v1.zip"

⚠️ 避坑 2:zip 包内不要加顶层目录! CodePipeline 解压 zip 包后,直接从根目录找 buildspec.yml。如果你的 zip 包结构是 mfmsapp/v1/main.go,CodeBuild 会找不到文件。正确结构是直接 main.gobuildspec.yml 在根。


第三步:配置 IAM 权限

1. CodePipeline 服务角色

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:GetBucketVersioning",
                "s3:ListBucket"
            ],
            "Resource": [
                "arn:aws:s3:::$SOURCE_BUCKET",
                "arn:aws:s3:::$SOURCE_BUCKET/*"
            ]
        },
        {
            "Effect": "Allow",
            "Action": [
                "codebuild:BatchGetBuilds",
                "codebuild:StartBuild"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "codedeploy:CreateDeployment",
                "codedeploy:GetApplication",
                "codedeploy:GetApplicationRevision",
                "codedeploy:GetDeployment",
                "codedeploy:GetDeploymentGroup",
                "codedeploy:RegisterApplicationRevision"
            ],
            "Resource": "*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "iam:PassRole"
            ],
            "Resource": "*"
        }
    ]
}

2. CodeBuild 服务角色

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "logs:CreateLogGroup",
                "logs:CreateLogStream",
                "logs:PutLogEvents"
            ],
            "Resource": "arn:aws:logs:*:*:*"
        },
        {
            "Effect": "Allow",
            "Action": [
                "s3:GetObject",
                "s3:GetObjectVersion",
                "s3:PutObject"
            ],
            "Resource": "*"
        }
    ]
}

3. CodeDeploy 服务角色

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Action": [
                "ec2:DescribeInstances",
                "ec2:DescribeInstanceStatus"
            ],
            "Resource": "*"
        }
    ]
}

⚠️ 避坑 3:使用 AWS 托管策略更简单! 如果不想手写上面的自定义策略,可以直接附加托管策略:

  • CodePipeline 角色:AWSCodePipelineServiceRoleV2

  • CodeBuild 角色:AWSCodeBuildAdminAccess

  • CodeDeploy 角色:AWSCodeDeployRole

生产环境建议最小权限,但学习阶段用托管策略可以避免权限错误


第四步:创建 CodeDeploy 应用和部署组

# 1. 创建 CodeDeploy 应用
aws deploy create-application \
    --application-name mfmsapp \
    --compute-platform Server

# 2. 获取 CodeDeploy 角色 ARN
CODEDEPLOY_ROLE_ARN=$(aws iam get-role \
    --role-name AWSCodeDeployRole \
    --query 'Role.Arn' --output text)

# 3. 创建部署组(部署到所有带 mfmsapp 标签的 EC2 实例)
aws deploy create-deployment-group \
    --application-name mfmsapp \
    --deployment-group-name mfmsapp-prod \
    --service-role-arn "$CODEDEPLOY_ROLE_ARN" \
    --ec2-tag-filters Key=Name,Value=mfmsapp-server,Type=KEY_AND_VALUE \
    --deployment-config-name CodeDeployDefault.OneAtATime

第五步:创建 CodeBuild 项目

CODEBUILD_ROLE_ARN=$(aws iam get-role \
    --role-name AWSCodeBuildServiceRole \
    --query 'Role.Arn' --output text)

aws codebuild create-project \
    --name mfmsapp-build \
    --source type=S3 location="$SOURCE_BUCKET/mfmsapp-v1.zip" \
    --artifacts type=CODEPIPELINE \
    --environment type=LINUX_CONTAINER,image=aws/codebuild/amazonlinux2-x86_64-standard:5.0,computeType=BUILD_GENERAL1_SMALL \
    --service-role "$CODEBUILD_ROLE_ARN"

⚠️ 避坑 4:artifacts type 必须为 CODEPIPELINE! 当 CodeBuild 与 CodePipeline 集成时,artifacts.type 必须设为 CODEPIPELINE,不能设为 S3。否则流水线无法传递构建产物。


第六步:创建 CodePipeline

PIPELINE_ROLE_ARN=$(aws iam get-role \
    --role-name AWSCodePipelineServiceRole \
    --query 'Role.Arn' --output text)

aws codepipeline create-pipeline \
    --pipeline name=mfmsapp-pipeline \
    --pipeline-type V2 \
    --role-arn "$PIPELINE_ROLE_ARN" \
    --stages '[
        {
            "name": "Source",
            "actions": [{
                "name": "SourceAction",
                "actionTypeId": {
                    "category": "Source",
                    "owner": "AWS",
                    "provider": "S3",
                    "version": "1"
                },
                "outputArtifacts": [{"name": "SourceArtifact"}],
                "configuration": {
                    "S3Bucket": "'"$SOURCE_BUCKET"'",
                    "S3ObjectKey": "mfmsapp-v1.zip",
                    "PollForSourceChanges": "true"
                },
                "runOrder": 1
            }]
        },
        {
            "name": "Build",
            "actions": [{
                "name": "BuildAction",
                "actionTypeId": {
                    "category": "Build",
                    "owner": "AWS",
                    "provider": "CodeBuild",
                    "version": "1"
                },
                "inputArtifacts": [{"name": "SourceArtifact"}],
                "outputArtifacts": [{"name": "BuildArtifact"}],
                "configuration": {
                    "ProjectName": "mfmsapp-build"
                },
                "runOrder": 1
            }]
        },
        {
            "name": "Deploy",
            "actions": [{
                "name": "DeployAction",
                "actionTypeId": {
                    "category": "Deploy",
                    "owner": "AWS",
                    "provider": "CodeDeploy",
                    "version": "1"
                },
                "inputArtifacts": [{"name": "BuildArtifact"}],
                "configuration": {
                    "ApplicationName": "mfmsapp",
                    "DeploymentGroupName": "mfmsapp-prod"
                },
                "runOrder": 1
            }]
        }
    ]'

⚠️ 避坑 5:PollForSourceChanges 的坑! 如果你同时配置了 S3 事件通知 + 轮询,会导致重复触发

  • 使用 PollForSourceChanges: "true"(默认轮询)即可,不需要额外配置 S3 事件通知。

  • 轮询间隔约 1 分钟(V2 管道),如果你需要实时触发,才需要设置 S3 EventBridge。


第七步:验证部署

1. 查看流水线状态

aws codepipeline get-pipeline-state --name mfmsapp-pipeline

2. 检查 mfmsapp 是否运行

# 在 EC2 实例上执行
curl http://localhost:8080/health
# 应返回:
# {"status":"ok","time":"2026-04-04 22:00:00","version":"202604042200"}

3. 获取作物列表

curl http://localhost:8080/api/crops
# 应返回:
# [{"id":1,"name":"小麦","area":"A区","date":"2026-04-04"},...]

更新触发测试

当你修改完代码,重新打包并上传到同一个 S3 路径时,流水线会自动触发:

# 修改代码后,重新打包
zip -r mfmsapp-v1.zip main.go go.mod buildspec.yml appspec.yml scripts/

# 上传(覆盖原文件)
aws s3 cp mfmsapp-v1.zip s3://$SOURCE_BUCKET/mfmsapp-v1.zip

# 等待约 1 分钟,流水线自动开始运行

常见问题排查

症状

可能原因

解决方案

上传后不触发

S3 桶未开启版本控制

aws s3api put-bucket-versioning --bucket xxx --versioning-configuration Status=Enabled

CodeBuild 找不到 buildspec

zip 包有多层目录

解压后根目录直接有 buildspec.yml,不能有顶层文件夹

CodeDeploy 部署失败

CodeDeploy Agent 未运行

sudo systemctl status codedeploy-agent

artifacts 传递失败

artifacts.type 不是 CODEPIPELINE

修改为 "type": "CODEPIPELINE"

权限错误

IAM 角色缺少权限

先附加托管策略测试,排除权限问题

二次上传不触发

文件名相同且版本控制未启用

确保版本控制已开启,或更改文件名


完整配置清单

在开始之前,确认你已准备好以下资源:

  • S3 源存储桶(已开启版本控制)

  • EC2 实例(Amazon Linux 2023,带 mfmsapp-server 标签)

  • CodeDeploy Agent(已在 EC2 上安装并运行)

  • IAM 角色:CodePipeline角色、CodeBuild角色、CodeDeploy角色

  • buildspec.yml(放在源码根目录)

  • appspec.yml(放在源码根目录)

  • 源码 zip 包(已上传到 S3)


官方文档参考

服务

文档链接

CodePipeline S3 教程

AWS 官方教程

CodeBuild 构建规范

buildspec 参考

CodeDeploy AppSpec

AppSpec 文件参考


下一篇预告

第 03 篇:S3 模式避坑指南

本篇虽然搭好了流水线,但还有很多潜在陷阱没有覆盖:

  • S3 事件重复导致重复部署

  • artifact 路径配置错误

  • 多 Region 部署问题

  • KMS 加密 artifact 的配置

下一篇详细盘点这些坑,帮你少踩雷。


本文档内容基于 2026 年 4 月 AWS 官方文档整理。AWS 服务可能随时更新,如遇差异请以 AWS 官方文档 为准。