系列导读: 第一篇我们对比了 S3 触发和 CodeCommit 触发两种架构。本篇开始实战,从零搭建 S3 触发模式的完整 CodePipeline,覆盖 S3、CodeBuild、CodeDeploy、IAM 权限全链路配置。
目标
完成本篇后,你将拥有一个可工作的 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.go、buildspec.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 角色:
AWSCodePipelineServiceRoleV2CodeBuild 角色:
AWSCodeBuildAdminAccessCodeDeploy 角色:
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 源存储桶(已开启版本控制)
EC2 实例(Amazon Linux 2023,带
mfmsapp-server标签)CodeDeploy Agent(已在 EC2 上安装并运行)
IAM 角色:CodePipeline角色、CodeBuild角色、CodeDeploy角色
buildspec.yml(放在源码根目录)
appspec.yml(放在源码根目录)
源码 zip 包(已上传到 S3)
官方文档参考
下一篇预告
第 03 篇:S3 模式避坑指南
本篇虽然搭好了流水线,但还有很多潜在陷阱没有覆盖:
S3 事件重复导致重复部署
artifact 路径配置错误
多 Region 部署问题
KMS 加密 artifact 的配置
下一篇详细盘点这些坑,帮你少踩雷。
本文档内容基于 2026 年 4 月 AWS 官方文档整理。AWS 服务可能随时更新,如遇差异请以 AWS 官方文档 为准。