照着做,就能跑通模块 A
这份手册假设你没碰过 AWS。每一步都告诉你:去哪个网址、点哪个按钮、填什么内容、看到什么画面算成功。
全程只用浏览器,不用装任何软件。跟着 12 个步骤走完,你会得到一个能接收数据、存进两个数据库、按 id 求和返回的完整系统。
开始之前
你要准备什么
| 项目 | 说明 |
|---|---|
| 一个 AWS 账号 | 能登录控制台就行。比赛时主办方会给账号 |
| 一台能上网的电脑 | 装了 Chrome / Edge 浏览器 |
| 约 2 小时 | 其中 Aurora 创建要等 6-10 分钟,EC2 启动要等 2-3 分钟 |
| 少量费用 | 按宁夏区域算,6 小时比赛大约 ¥4-5。用免费套餐则更低 |
最重要的一件事:确认区域
每次打开控制台第一件事:看右上角,确认显示
中国(宁夏)。比赛时则确认 美国东部(弗吉尼亚北部)us-east-1。
不对就点它切换。
三条红线(违反直接取消成绩)
2. 不许用 API Gateway —— API 必须自己写在 EC2 上。
3. 操作系统用 Amazon Linux 2。
yum 在 AL2023 上会自动转向 dnf),所有脚本两边都能跑。
比赛环境如果提供 AL2,直接照抄即可。
先搞懂 6 个名词
不用背,扫一眼就行。后面遇到哪个忘了,回来看。
1. EC2 —— 一台云服务器
就是一台租来的电脑,装了 Linux 系统。你的程序(API、网站)都跑在上面。比赛规定只能用这个。
2. DynamoDB —— 一个"表格型"数据库
AWS 自己的数据库,不需要装、不需要管服务器。就像一个 Excel 表:一列是 id(编号),一列是 value(值)。题目第一组数据存这里。
3. Aurora —— 一个 MySQL 数据库
就是常见的 MySQL,AWS 托管版。题目第二组数据存这里。需要建库、建表。
4. S3 —— 一个网盘
存文件用的。题目里用它放 server.zip(网站程序包)。自动发布就是盯着这里有没有新文件。
5. IAM 角色 —— 给服务器的"通行证"
EC2 默认什么都访问不了。给它挂一个"角色",就像给它发一张通行证,它才有权限去读写 DynamoDB、S3。这一步新手最容易忘,忘了 API 就一直报错没权限。
6. 安全组 —— 防火墙
决定哪些端口能被访问。我们要开 80(网页)、22(远程登录)、443(加密网页)。不开 80,外面就访问不了你的 API。
外部请求 → EC2 的 80 端口 → Nginx 判断路径 →
• 路径是
/post_data1 → 交给 API → 存进 DynamoDB• 路径是
/post_data2 → 交给 API → 存进 Aurora• 路径是
/get_value?id=01 → 交给 API → 两边各查一次,加起来返回• 其他路径 → 交给 Web 应用(8082 端口)
步骤 1 · 建 DynamoDB 表
{"id":"01","value":"123"})存进 DynamoDB。得先有个表才能存。操作步骤
(比赛时把
cn-northwest-1 换成 us-east-1,下面所有网址同理)- 进去后点右上角橙色按钮 创建表
- 按下面这张表填:
| 字段 | 填什么 | 说明 |
|---|---|---|
| 表名称 | cr-data1 | 名字随便起,后面代码里要对应上 |
| 分区键 | id | 就是"编号"这列的名字 |
| 分区键类型 | 字符串(默认就是) | 不用改 |
| 表设置 | 按需(默认) | 按实际用量计费,最便宜 |
- 拉到最下面,点橙色按钮 创建表
cr-data1-b,
但要记得后面步骤 8 的代码里 DDB_TABLE 也要跟着改。
步骤 2 · 建 S3 存储桶
server.zip 传到一个桶里,你的服务器要能自动发现并更新。这个桶就是那个"中转站"。操作步骤
| 字段 | 填什么 | 说明 |
|---|---|---|
| 存储桶名称 | cr-autodeploy-你的账号ID例如 cr-autodeploy-578617970405 |
全球唯一!全世界 AWS 用户的桶名不能重复,所以一定要加上你的账号 ID 或一串随机数字 |
| 区域 | 保持默认(中国宁夏) | 别改 |
| 阻止公有访问 | 保持默认(全部勾选) | 安全选项,不用动 |
| 其他 | 全部保持默认 | 版本控制、加密、标签都不用开 |
拉到最下面点 创建存储桶。
cr-autodeploy-578617970405-a7。把最终用的桶名记下来,步骤 10 和步骤 11 都要用。
步骤 3 · 建 IAM 角色(通行证)
操作步骤
第 1 步:选可信实体
- 选 AWS 服务(默认就是这个)
- "服务或使用案例"那里选 EC2
- 点 下一步
{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": { "Service": "ec2.amazonaws.com" },
"Action": "sts:AssumeRole"
}]
}
第 2 步:勾选权限(5 个)
在搜索框里一个一个搜,搜到就勾选上。每勾一个,右上角计数会 +1。
| 搜索关键词 | 勾选这个 | 作用 |
|---|---|---|
DynamoDB | AmazonDynamoDBFullAccess | 读写 DynamoDB 表 |
RDS | AmazonRDSFullAccess | 连 Aurora 数据库 |
S3 | AmazonS3FullAccess | 从桶里下载 server.zip |
SSMManaged | AmazonSSMManagedInstanceCore | 让你能用浏览器直接连服务器(步骤 7 要用) |
CloudWatchAgentServer | CloudWatchAgentServerPolicy | 上报监控指标 |
点 下一步。
第 3 步:起名字
| 字段 | 填什么 |
|---|---|
| 角色名称 | CR-EC2-Role |
| 描述 | 随便填或不填 |
拉到最下面点 创建角色。
步骤 4 · 建 Aurora 数据库
{"id":"01","value":"456"})要存进 Aurora(MySQL)。而且最后 /get_value 要从这里读数据出来做加法。操作步骤
点橙色按钮 创建数据库
| 字段 | 填什么 / 选什么 | 说明 |
|---|---|---|
| 数据库创建方法 | 标准创建 | 能自己控制所有选项 |
| 引擎类型 | Aurora (MySQL Compatible) | 别选 PostgreSQL! |
| 模板 | 开发/测试 | 生产模板会贵很多 |
| 数据库集群标识符 | cr-aurora | 数据库集群的名字 |
| 主用户名 | admin | 默认就是 |
| 凭证管理 | 自行管理 | 这样能自己设密码(选 Secrets Manager 会多花钱) |
| 主密码 / 确认密码 | 123456789QWERTY | 8 位以上,字母+数字,记下来 |
| 实例类 | 可突增类 → db.t3.small | 最便宜的档,别选"内存优化类" |
| 多可用区部署 | 不创建 Aurora 副本 | 比赛不需要,省一半钱 |
| 公开访问 | 否 | 更安全,且 EC2 在同一 VPC 内能连上 |
拉到最下面点 创建数据库。
cr-aurora,状态先是"正在创建",
6-10 分钟后变成"可用(Available)"。
创建完成后:记下两个重要信息
- 在数据库列表点
cr-aurora进去 - 在"连接和安全"标签页找到 终端节点(Endpoint),长得像:
cr-aurora.cluster-xxxxxx.cn-northwest-1.rds.amazonaws.com.cn - 把这个地址复制下来,步骤 8 要填进配置文件
步骤 5 · 启动 EC2 服务器
操作步骤
点右上角橙色按钮 启动实例
| 区块 | 填什么 / 选什么 | 说明 |
|---|---|---|
| 名称和标签 | 名称填 cr-app-server | 方便认 |
| 应用程序和操作系统映像 | Amazon Linux 2023(默认那个) | 不要选 Ubuntu/Windows |
| 实例类型 | t3.micro | 免费套餐可用,够用 |
| 密钥对(登录) | 创建新密钥对,名字填 cr-key |
系统会要求必须有一个。但我们后面用 Session Manager 登录,不需要下载这个文件 |
| 网络设置 · 防火墙 | 选"创建安全组",勾选 ✅ 允许 SSH 流量 ✅ 允许来自互联网的 HTTP 流量 ✅ 允许来自互联网的 HTTPS 流量 |
HTTP(80) 必须勾,否则 API 访问不了 |
| 配置存储 | 默认 8 GiB gp3 | 免费额度内,不用改 |
关键一步:填"高级详细信息"里的用户数据
- 展开最下面的 高级详细信息(点标题左边的箭头)
- 往下找到 用户数据 – 可选 这个文本框
- 打开本手册的 代码附录,复制完整的 user-data 脚本
- 粘贴进去
- 安装 Python、Nginx、解压工具
- 安装 Flask、Gunicorn、boto3(连 AWS 用)、pymysql(连 MySQL 用)
- 把 API 代码写到
/opt/crapp/api.py - 把 API 注册成系统服务,开机自启、崩溃自动重启
- 配置 Nginx:把 80 端口的请求按路径分给 API(5000)或网站(8082)
- 设置每分钟检查一次 S3 桶的自动发布脚本
拉到最下面,右侧点 启动实例。
- 公有 IPv4 地址:如
52.82.29.246—— 步骤 8 测试要用 - 公有 IPv4 DNS:如
ec2-52-82-29-246.cn-northwest-1.compute.amazonaws.com.cn—— 步骤 11 提交要用
步骤 6 · 给 EC2 挂上 IAM 角色
6.1 先建实例配置文件(壳)
- 打开 https://console.amazonaws.cn/iam/home#/roles/details/CR-EC2-Role
- 在角色详情页找到 实例配置文件 这一节(在"权限"和"信任关系"附近)
- 如果显示"无关联的实例配置文件",点右边的 创建
- 起名:
CR-EC2-Profile - 提交。系统会把这个壳和
CR-EC2-Role绑定
① 左侧 访问管理 → 角色 → 点
CR-EC2-Role 进去② 顶部有 角色 ARN 那段,挨着找"实例配置文件" 区域
CR-EC2-Profile,且状态显示"正在使用"或"已关联"。6.2 把壳挂到 EC2 上
- 回到 EC2 实例列表,勾选
cr-app-server前面的方框 - 点顶部的 操作 按钮
- 鼠标移到 安全 上
- 点 修改 IAM 角色
- 在下拉框里选
CR-EC2-Profile(不是 CR-EC2-Role!下拉里出现的是壳的名字) - 点 更新 IAM 角色
步骤 7 · 用浏览器连上服务器
好消息:不用装 PuTTY、不用下载密钥文件、不用配 SSH。因为步骤 3 勾了 SSM 权限, AWS 提供了一个直接在网页里开终端的功能,叫 Session Manager。
操作步骤
- EC2 实例列表,勾选
cr-app-server - 点顶部 连接 按钮
- 选 Session Manager 标签页
- 点 连接
sh-5.2$
现在你输入的每条命令,都直接跑在云服务器上。
① 确认步骤 6 的 IAM 角色挂上了(最常见原因)
② 确认实例状态是"正在运行"且状态检查"2/2 通过"
③ 等 2 分钟再试 —— SSM 代理刚开机时还没注册上来
进去后第一件事:切到 root 用户
Session Manager 默认登录的是普通用户,装软件要管理员权限。执行:
sudo su -
cd ~
提示符变成 [root@ip-xxx ~]# 就对了。
步骤 8 · 部署并测试 API
8.1 先确认自动装机跑完了
在 Session Manager 终端里执行:
tail -20 /var/log/user-data.log
==== done ... ==== 就说明装完了。如果没看到,等 2 分钟再看(yum 安装需要时间)。8.2 放行 Aurora 3306 + 建 crdb 数据库
如果 API 日志里出现 Can't connect to MySQL server 或 Access denied,八成是这两个问题。
8.2.1 放行 3306 端口
EC2 访问 Aurora 的 3306 端口,必须要在 Aurora 所属的安全组里加一条入站规则:
找到 Aurora 用的安全组(通常是
default) → 编辑入站规则 → 添加规则| 字段 | 填什么 |
|---|---|
| 类型 | 自定义 TCP |
| 端口范围 | 3306 |
| 源 | 自定义 → 选 EC2 实例用的安全组(如 launch-wizard-2 / sg-0267ac394adca56a7) |
timeout 5 bash -c 'cat </dev/null >/dev/tcp/你的Aurora端点/3306' 不再超时。
3306 来自 launch-wizard-28.2.2 建库建表
把 你的Aurora端点 换成步骤 4 记下的那个地址:
mysql -h 你的Aurora端点 -u admin -p'123456789QWERTY' \
-e "CREATE DATABASE IF NOT EXISTS crdb CHARACTER SET utf8mb4; USE crdb; CREATE TABLE IF NOT EXISTS data2 (id VARCHAR(64) PRIMARY KEY, value VARCHAR(255) NOT NULL) ENGINE=InnoDB; SHOW DATABASES;"
crdb,表列表里能看到 data2。mysql: command not found:先装客户端:
dnf install -y mariadb105
如果提示 Access denied:说明密码不对。去 RDS 控制台 → 集群 → 修改 → 重新设置主密码,并勾选立即应用。
8.3 写入数据库配置
把下面整段复制进去(替换掉其中的 Aurora 端点):
cat > /etc/crapp.env <<EOF
AWS_REGION=cn-northwest-1
DDB_TABLE=cr-data1
DB_HOST=你的Aurora端点
DB_USER=admin
DB_PASS=123456789QWERTY
DB_NAME=crdb
EOF
chmod 600 /etc/crapp.env
systemctl restart cr-api
sleep 3
systemctl status cr-api --no-pager
active (running)。如果是红色的 failed,看下面排查。8.4 测试三个接口(关键!)
依次执行这三条 curl 命令:
# ① 存第一组数据到 DynamoDB
curl -s -X POST http://localhost/post_data1 \
-H "Content-Type: application/json" \
-d '{"id":"01","value":"123"}'
# 期望输出: {"message":"ok"}
# ② 存第二组数据到 Aurora
curl -s -X POST http://localhost/post_data2 \
-H "Content-Type: application/json" \
-d '{"id":"01","value":"456"}'
# 期望输出: {"message":"ok"}
# ③ 查询求和 —— 这是评分点!
curl -s "http://localhost/get_value?id=01"
# 期望输出: {"message":"579"}
{"message":"579"} 就成功了!123 + 456 = 579,说明两个库都通了,求和也对。
http://52.82.29.246/get_value?id=01 成功返回 {"message":"579"}8.5 从外网再测一遍
在你自己电脑的浏览器里打开(换成你的公网 IP):
http://52.82.29.246/get_value?id=01
{"message":"579"} 就大功告成。步骤 9 · 部署 Web 应用
server.zip)跑起来,并且能通过 http://你的域名 直接访问。temp-418028000011.s3...)现在已经 404 失效,
不要照抄地址。重点是搞懂它长什么样、怎么跑起来。
server.zip 长什么样
| 文件 | 作用 |
|---|---|
run.sh | 启动脚本。必须有可执行权限,否则网站起不来 |
index.html | 网站页面(实际包里可能还有别的文件) |
它运行时会读同目录 config.json 里的 server_port(题目指定 8082)。
这个 config.json 由我们的自动发布脚本自动生成,不用你操心。
9.1 自己做一份测试包(强烈建议先练一遍)
拿不到主办方的包,就自己做一份,把整条链路先跑通:
mkdir -p /tmp/mk/index
cat > /tmp/mk/run.sh <<'EOF'
#!/bin/bash
cd "$(dirname "$0")"
PORT=$(python3 -c "import json;print(json.load(open('config.json'))['server_port'])" 2>/dev/null || echo 8082)
echo "[run.sh] starting web app on port $PORT"
exec python3 -m http.server "$PORT" --bind 0.0.0.0
EOF
cat > /tmp/mk/index/index.html <<'EOF'
<h1>Cloud Raiser Web App (v1)</h1>
<p>这个页面由 S3 自动发布部署到 EC2</p>
EOF
chmod +x /tmp/mk/run.sh
cd /tmp/mk && zip -r /tmp/server.zip run.sh index
ls -la /tmp/server.zip
9.2 上传到你自己的 S3 桶
aws s3 cp /tmp/server.zip s3://你的桶名/server.zip --region cn-northwest-1
# 确认上传成功(返回的一串字符就是 ETag,也就是文件指纹)
aws s3api head-object --bucket 你的桶名 --key server.zip \
--region cn-northwest-1 --query ETag --output text
9.3 什么都不用做,等它自动部署
最多等 1 分钟,自动发布脚本会发现新文件并启动网站。然后验证:
# 看自动发布日志
tail -5 /var/log/autodeploy.log
# 期望看到两行:
# [autodeploy] new server.zip detected (xxxx)
# [autodeploy] web app launched
# 确认 8082 端口有人在听
ss -lntp | grep 8082
# 访问网站
curl -s http://localhost/ | head -5
http://你的IP/ 能看到页面。
http://52.82.29.246/ —— 网站已由自动发布部署上线run.sh 没有执行权限。
unzip 解压时不会还原 zip 里存的执行权限位,
解压出来的 run.sh 是 -rw-r--r--,直接跑就报 Permission denied。
chmod +x /opt/webapp/app/run.sh
# 重新触发一次部署
rm -f /opt/webapp/.last_etag && /opt/crapp/autodeploy.sh
本手册的自动发布脚本已内置这行 chmod;如果你自己写脚本,千万别漏。
(备用)手动部署方式
如果主办方给了可直接下载的网址,也可以手动跑一遍(换成真实地址):
mkdir -p /opt/webapp/app
cd /opt/webapp
curl -o server.zip 主办方给的地址
unzip -o server.zip -d /opt/webapp/app
chmod +x /opt/webapp/app/run.sh # ← 别漏这行
# 建端口配置文件(题目指定 8082)
cat > /opt/webapp/app/config.json <<EOF
{"server_port": 8082}
EOF
# 启动网站
cd /opt/webapp/app && nohup ./run.sh > /var/log/webapp.log 2>&1 &
sleep 3
curl -s http://localhost:8082 | head -20
通过 80 端口访问
步骤 5 的 Nginx 已经配好了:访问 /(根路径)会自动转给 8082 的网站。
所以你直接访问:
http://52.82.29.246/
/ 是网站,/get_value 是 API,互不干扰。/post_data1、/get_value 领去 API(5000 端口),其他一律领去网站(8082 端口)。
步骤 10 · 配置自动发布
正常人第一反应是"S3 有新文件 → 触发 Lambda → 通知 EC2 更新"。 但规则第 1 条就禁了 Lambda!
所以必须换个办法:让 EC2 自己每分钟去 S3 看一眼,发现文件变了就自动更新。
验证自动发布是否在工作
# 确认桶名已写入配置
cat /etc/crapp.d/bucket
# 应该输出你的桶名: cr-autodeploy-xxxx
# 确认定时任务已注册
cat /etc/cron.d/cr-autodeploy
# 应该输出: * * * * * root /opt/crapp/autodeploy.sh ...
# 手动跑一次看效果
/opt/crapp/autodeploy.sh
echo "退出码: $?"
# 看日志
tail -20 /var/log/autodeploy.log
[autodeploy] new server.zip detected 和 web app launched。模拟比赛验收
比赛结束时,主办方会往你的桶里传一个新的 server.zip。你现在就能自己测:
- 改一下测试包的版本号,重新打包上传(内容必须真的变了,ETag 才会变)
- 什么都不做,等 最多 1 分钟
- 看服务器上的日志:
tail -f /var/log/autodeploy.log - 访问
http://你的IP/确认网站已更新
- v1 包上传 → 约 1 分钟后网站上线,页面显示
autodeploy v1 OK - 改版本号重打包 → 新 ETag
2c0b9b4a...上传 - 不碰服务器,纯等 80 秒 → 页面自动变成
autodeploy v2 OK
systemctl status crond
不是 active (running) 就执行 systemctl enable --now crond。
步骤 11 · 提交答案
| 输入框 | 填什么 | 例子 | 常见错误 |
|---|---|---|---|
| 第 1 个 (API 地址) |
你的域名,不带路径 | http://ec2-52-82-29-246.cn-northwest-1.compute.amazonaws.com.cn |
❌ 写成 http://.../post_data1❌ 结尾多写个 / |
| 第 2 个 (网站地址) |
和上面一样 | http://ec2-52-82-29-246.cn-northwest-1.compute.amazonaws.com.cn |
❌ 写成 http://...:8082(端口不能暴露) |
| 第 3 个 (S3 桶名) |
纯桶名,不是 ARN | cr-autodeploy-578617970405 |
❌ 写成 arn:aws-cn:s3:::cr-autodeploy-xxx |
每填一个,点旁边的 Update 按钮提交。
- 往你的 API 发数据,检查
/get_value返回对不对 - 访问你的网站,检查页面正不正常
- 比赛结束后往你的桶里传新
server.zip,1 分钟后检查网站有没有自动更新
ec2-43-192-... 那个)。
因为 IP 在服务器重启后会变,DNS 名不会。
步骤 12 · 架构优化(加分项)
12.1 加负载均衡 + 自动扩容
现在是单台服务器,挂了就全挂。加个 ALB(负载均衡)把流量分给多台,并让服务器数量随流量自动增减。
- 建目标组:EC2 → 目标群组 → 创建 → 协议 HTTP:80,健康检查路径填
/healthz - 建启动模板:EC2 → 启动模板 → 创建 → 配置和步骤 5 一样(用户数据还是贴同一段脚本)
- 建 Auto Scaling 组:EC2 → Auto Scaling 组 → 创建 →
- 选刚才的启动模板
- 选 2 个子网
- 挂到刚才的目标组
- 所需容量 2、最小 2、最大 6
- 扩展策略:CPU > 60% 时加一台
- 建负载均衡器:EC2 → 负载均衡器 → 创建 ALB → 面向 Internet → 侦听器 80 → 默认转发到目标组
12.2 加监控告警
CloudWatch → 告警 → 创建,建议配这三条:
| 指标 | 条件 | 作用 |
|---|---|---|
| EC2 CPUUtilization | > 70% 持续 5 分钟 | 流量太大要扩容 |
| ALB HTTPCode_Target_5XX_Count | > 5 次/分钟 | 程序报错了 |
| ALB UnHealthyHostCount | > 0 | 有服务器挂了 |
12.3 成本参考
| 资源 | 6 小时费用 |
|---|---|
| EC2 t3.micro × 2 | ≈ ¥0.84 |
| Aurora db.t3.small | ≈ ¥2.76(最贵一项) |
| DynamoDB 按需 | < ¥0.10 |
| S3 | < ¥0.05 |
| ALB | ≈ ¥0.96 |
| 合计 | ≈ ¥4.7 |
出问题了怎么办
API 返回 502 Bad Gateway
说明 Nginx 在,但后面的 API 没起来。执行:
systemctl status cr-api --no-pager
journalctl -u cr-api -n 50 --no-pager
tail -30 /var/log/cr-api.err.log
报错 AccessDeniedException / Unable to locate credentials
100% 是步骤 6 没做或没生效。确认:
① EC2 实例详情里"IAM 角色"显示 CR-EC2-Role
② 该角色上挂了 AmazonDynamoDBFullAccess 和 AmazonS3FullAccess
报错 Can't connect to MySQL server(连接超时)
按顺序排查:
① cat /etc/crapp.env 看 DB_HOST 是不是 Aurora 端点(不是 IP、不是 localhost)
② Aurora 状态是不是"可用"
③ Aurora 安全组有没有放行 3306,来源是不是 EC2 的安全组(最常见)
④ EC2 和 Aurora 是不是在同一个 VPC
快速判定第 ③ 条,在 EC2 上执行:
timeout 5 bash -c 'cat </dev/null >/dev/tcp/你的Aurora端点/3306' && echo 通 || echo 不通
报错 Access denied for user 'admin'
用户名对但密码错。去 RDS 控制台 → 集群 database-1 → 修改 →
在"主密码 / 确认主密码"里重设 → 拉到最底 → 修改集群。
注意确认页默认选的是"在下一个维护时段应用",要改成"立即应用"才马上生效。
改完同步更新服务器配置:
sudo sed -i 's|^DB_PASS=.*|DB_PASS=你的新密码|' /etc/crapp.env
sudo systemctl restart cr-api
报错 Unknown database 'crdb'
库不存在。本手册的 api.py 启动时会自动建库建表;
如果你用的是更早的版本,手工建一次即可:
mysql -h 你的Aurora端点 -u admin -p'你的密码' -e "CREATE DATABASE IF NOT EXISTS crdb CHARACTER SET utf8mb4; USE crdb; CREATE TABLE IF NOT EXISTS data2 (id VARCHAR(64) PRIMARY KEY, value VARCHAR(255) NOT NULL);"
nginx 起不来:"listen" directive is not allowed here
这是 Nginx 配置冲突:我们自己的 conf.d/crapp.conf 已经监听 80,
而系统自带的 nginx.conf 里还有一个默认 server { } 块也监听 80,两个打架。
解决办法是把默认那整个 server 块删掉。注意:只注释 server { 这一行没用,
块里剩下的指令会裸露在外层,照样报错。本手册的 user-data 已用括号配对方式整块删除;手工修的话:
nginx -t # 先看报错在第几行
sudo sed -i '36,53d' /etc/nginx/nginx.conf # 行号按你的实际报错区间调整
sudo nginx -t # 再验证一次
sudo systemctl restart nginx
get_value 返回 0 或者数字不对
说明数据没存进去。先确认两个 post 都返回了 {"message":"ok"},然后直接查库:
# 查 DynamoDB
aws dynamodb scan --table-name cr-data1 --region cn-northwest-1
# 查 Aurora
mysql -h 你的Aurora端点 -u admin -p'123456789QWERTY' crdb \
-e "SELECT * FROM data2;"
外网访问不了,但服务器上 curl localhost 正常
安全组问题。EC2 → 安全组 → 入站规则,必须有:HTTP / TCP / 80 / 来源 0.0.0.0/0。
Session Manager 连不上
① 步骤 6 的 IAM 角色挂了吗(要有 AmazonSSMManagedInstanceCore)
② 实例开机满 2 分钟了吗(SSM 代理要时间注册)
③ 实例状态检查是不是"2/2 项检查已通过"
网站能开但一直显示旧版
自动发布没生效。检查三件事:
systemctl status crond # 必须是 active
cat /etc/crapp.d/bucket # 必须是你的桶名
tail -20 /var/log/autodeploy.log # 看有没有 detected 字样
代码附录
下面三个文件都可以直接下载。步骤 5 需要 user-data.sh(整段复制粘贴)。
| 文件 | 用在哪一步 | 下载 |
|---|---|---|
user-data.sh | 步骤 5 — 粘贴到"用户数据"框 | ⬇ 下载 |
api.py | 已内嵌在 user-data 里,一般不用单独下 | ⬇ 下载 |
cr-stack.template | CloudFormation 一键版(新手不推荐,会漏掉很多理解) | ⬇ 下载 |
user-data.sh 全文(步骤 5 要粘贴的)
DB_HOST=...、DB_PASS、DEPLOY_BUCKET
必须换成你自己的真实值(步骤 4 和步骤 2 里有说明)。
#!/bin/bash
# Cloud Raiser - EC2 开机自举(AL2023 健壮版)
# 关键改动:去掉 set -e、所有安装加 || true、直接用 dnf、自动建库建表、正确删除 nginx 默认 server 块
DB_HOST="database-1.cluster-cfud9bwy8iiy.rds.cn-northwest-1.amazonaws.com.cn"
DB_USER="admin"
DB_PASS="123456789QWERTY"
DB_NAME="crdb"
AWS_REGION="cn-northwest-1"
DEPLOY_BUCKET="cr-autodeploy-wsc57980405"
exec > >(tee /var/log/user-data.log) 2>&1
echo "==== start $(date) ===="
# ---------- 1. 装依赖(全程不中断)----------
dnf update -y || true
dnf install -y python3 python3-pip nginx gcc python3-devel || true
dnf install -y cronie || true
dnf install -y mariadb105 || true
pip3 install --quiet --upgrade pip || true
pip3 install --quiet flask gunicorn boto3 pymysql || true
echo "[1] deps done"
# ---------- 2. API 代码 ----------
mkdir -p /opt/crapp /opt/webapp /etc/crapp.d
cat > /opt/crapp/api.py <<'PYEOF'
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
Cloud Raiser · 浙江省选拔赛 - 云计算项目 - 架构完善模块 - 阶段1 Restful API
=========================================================================
端点:
POST /post_data1 把 {id,value} 写入 DynamoDB(表 cr-data1)
POST /post_data2 把 {id,value} 写入 Aurora MySQL(表 data2)
GET /get_value?id=01 从两库按 id 查 value,求和后返回 {"message":"579"}
GET /healthz 健康检查
部署方式:Gunicorn + Nginx(端口 80 反向代理到 5000;Web 应用 8082)
"""
import os
import json
import logging
from flask import Flask, request, jsonify
import boto3
import pymysql
from botocore.config import Config
# ---------------- 日志 ----------------
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler("/var/log/cr-api.log", encoding="utf-8"),
],
)
log = logging.getLogger("cr-api")
app = Flask(__name__)
# ---------------- DynamoDB ----------------
DDB_TABLE = os.environ.get("DDB_TABLE", "cr-data1")
AWS_REGION = os.environ.get("AWS_REGION", "cn-northwest-1")
ddb = boto3.resource(
"dynamodb",
region_name=AWS_REGION,
config=Config(retries={"max_attempts": 3, "mode": "standard"}),
)
table_ddb = ddb.Table(DDB_TABLE)
# ---------------- Aurora MySQL ----------------
DB_HOST = os.environ["DB_HOST"]
DB_USER = os.environ.get("DB_USER", "admin")
DB_PASS = os.environ["DB_PASS"]
DB_NAME = os.environ.get("DB_NAME", "crdb")
def db_conn():
return pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASS,
database=DB_NAME,
charset="utf8mb4",
connect_timeout=5,
read_timeout=10,
write_timeout=10,
)
def _ensure_db_and_table():
"""首次启动时自动创建 database 和 data2 表(建库阶段没建也能自愈)"""
# 1) 先连 MySQL server(不带 database)建库
cn = pymysql.connect(
host=DB_HOST,
user=DB_USER,
password=DB_PASS,
charset="utf8mb4",
connect_timeout=5,
read_timeout=10,
write_timeout=10,
)
try:
with cn.cursor() as c:
c.execute(f"CREATE DATABASE IF NOT EXISTS {DB_NAME} CHARACTER SET utf8mb4")
cn.commit()
finally:
cn.close()
# 2) 再连目标库建表
cn = db_conn()
try:
with cn.cursor() as c:
c.execute(
"""CREATE TABLE IF NOT EXISTS data2 (
id VARCHAR(64) PRIMARY KEY,
value VARCHAR(255) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4"""
)
cn.commit()
finally:
cn.close()
# 启动时建库建表
try:
_ensure_db_and_table()
log.info("data2 table ensured")
except Exception as e: # pragma: no cover
log.warning("ensure_table failed: %s", e)
# ---------------- 路由 ----------------
@app.route("/healthz", methods=["GET"])
def healthz():
return jsonify(ok=True)
@app.route("/post_data1", methods=["POST"])
def post_data1():
body = request.get_json(force=True, silent=True) or {}
_id = str(body.get("id", "")).strip()
val = body.get("value", "")
if not _id:
return jsonify(message="id required"), 400
table_ddb.put_item(Item={"id": _id, "value": str(val)})
log.info("DDB PUT id=%s val=%s", _id, val)
return jsonify(message="ok")
@app.route("/post_data2", methods=["POST"])
def post_data2():
body = request.get_json(force=True, silent=True) or {}
_id = str(body.get("id", "")).strip()
val = body.get("value", "")
if not _id:
return jsonify(message="id required"), 400
cn = db_conn()
try:
with cn.cursor() as c:
c.execute(
"""INSERT INTO data2 (id, value) VALUES (%s, %s)
ON DUPLICATE KEY UPDATE value=VALUES(value)""",
(_id, val),
)
cn.commit()
finally:
cn.close()
log.info("RDS PUT id=%s val=%s", _id, val)
return jsonify(message="ok")
@app.route("/get_value", methods=["GET"])
def get_value():
_id = str(request.args.get("id", "")).strip()
if not _id:
return jsonify(message="id required"), 400
ddb_val = (
table_ddb.get_item(Key={"id": _id})
.get("Item", {})
.get("value", "0")
)
rds_val = "0"
cn = db_conn()
try:
with cn.cursor() as c:
c.execute("SELECT value FROM data2 WHERE id=%s", (_id,))
row = c.fetchone()
if row:
rds_val = row[0]
finally:
cn.close()
try:
total = int(ddb_val) + int(rds_val)
except (TypeError, ValueError):
total = f"{ddb_val}{rds_val}"
log.info("GET id=%s ddb=%s rds=%s total=%s", _id, ddb_val, rds_val, total)
return jsonify(message=str(total))
# ---------------- 入口 ----------------
if __name__ == "__main__":
# 直接 python3 跑也支持;生产由 gunicorn 启动
app.run(host="127.0.0.1", port=5000)
PYEOF
echo "[2] api.py written"
# ---------- 3. systemd ----------
cat > /etc/crapp.env <<EOF
AWS_REGION=${AWS_REGION}
DDB_TABLE=cr-data1
DB_HOST=${DB_HOST}
DB_USER=${DB_USER}
DB_PASS=${DB_PASS}
DB_NAME=${DB_NAME}
EOF
chmod 600 /etc/crapp.env
cat > /etc/systemd/system/cr-api.service <<'SVCEOF'
[Unit]
Description=CR API (Flask via Gunicorn)
After=network.target
[Service]
WorkingDirectory=/opt/crapp
EnvironmentFile=/etc/crapp.env
ExecStart=/usr/bin/python3 -m gunicorn --workers 2 --threads 4 --bind 127.0.0.1:5000 api:app
Restart=always
RestartSec=3
[Install]
WantedBy=multi-user.target
SVCEOF
echo "[3] systemd unit written"
# ---------- 4. Nginx ----------
echo "${DEPLOY_BUCKET}" > /etc/crapp.d/bucket
mkdir -p /etc/nginx/conf.d
cat > /etc/nginx/conf.d/crapp.conf <<'NGXEOF'
server {
listen 80 default_server;
server_name _;
client_max_body_size 16m;
location = /post_data1 { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; }
location = /post_data2 { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; }
location = /get_value { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; }
location = /healthz { proxy_pass http://127.0.0.1:5000; proxy_set_header Host $host; }
location / { proxy_pass http://127.0.0.1:8082; proxy_set_header Host $host; }
}
NGXEOF
if [ -f /etc/nginx/nginx.conf ]; then
python3 - <<'PYFIX' || true
import re
p='/etc/nginx/nginx.conf'
s=open(p).read()
# 删除默认 server { ... } 块(含嵌套大括号),避免与 conf.d/crapp.conf 冲突
start=-1
for i in range(len(s)):
if s.startswith('server {', i) and (i==0 or not s[i-1].isalnum()):
start=i
break
if start>=0:
depth=1
i=start+len('server {')
while i<len(s) and depth>0:
if s[i]=='{': depth+=1
elif s[i]=='}': depth-=1
i+=1
s=s[:start]+' # default server block removed by user-data\n'+s[i:]
open(p,'w').write(s)
PYFIX
fi
echo "[4] nginx config written"
# ---------- 5. 自动发布(cron 轮询 S3 ETag)----------
cat > /opt/crapp/autodeploy.sh <<'WDEOF'
#!/bin/bash
LOCK=/var/lock/cr-autodeploy.lock
WORK=/opt/webapp
BUCKET=$(cat /etc/crapp.d/bucket 2>/dev/null)
[ -z "$BUCKET" ] && exit 0
[ -e "$LOCK" ] && exit 0
trap "rm -f $LOCK" EXIT
touch "$LOCK"
aws s3api get-object --bucket "$BUCKET" --key server.zip /tmp/server.zip.new --query ETag --output text > /tmp/etag.new 2>/dev/null || exit 0
ETAG=$(cat /tmp/etag.new | tr -d '"')
[ -e "$WORK/.last_etag" ] && [ "$(cat "$WORK/.last_etag")" = "$ETAG" ] && exit 0
echo "[autodeploy] new server.zip detected ($ETAG)"
rm -rf "$WORK/app" && mkdir -p "$WORK/app"
unzip -qo /tmp/server.zip.new -d "$WORK/app" || true
# 关键:unzip 不会还原 zip 里存的执行权限位,必须手动补,否则 run.sh 起不来(502)
chmod +x "$WORK/app/run.sh" 2>/dev/null || true
chmod -R a+rX "$WORK/app" 2>/dev/null || true
echo "$ETAG" > "$WORK/.last_etag"
printf '{"server_port": 8082}\n' > "$WORK/app/config.json"
pkill -f /opt/webapp/app || true
sleep 1
( cd "$WORK/app" && nohup ./run.sh > /var/log/webapp.log 2>&1 & ) || true
echo "[autodeploy] web app launched"
WDEOF
chmod +x /opt/crapp/autodeploy.sh
echo "* * * * * root /opt/crapp/autodeploy.sh >> /var/log/autodeploy.log 2>&1" > /etc/cron.d/cr-autodeploy || true
systemctl enable crond 2>/dev/null || true
systemctl restart crond 2>/dev/null || true
echo "[5] autodeploy installed"
# ---------- 6. 启动服务 ----------
systemctl daemon-reload
systemctl enable cr-api 2>/dev/null || true
systemctl enable nginx 2>/dev/null || true
systemctl restart cr-api 2>/dev/null || true
systemctl restart nginx 2>/dev/null || true
echo "[6] services started"
# ---------- 7. 自检 ----------
sleep 5
echo "--- nginx: $(systemctl is-active nginx) ---"
echo "--- cr-api: $(systemctl is-active cr-api) ---"
echo "--- healthz: $(curl -s -m 5 http://127.0.0.1/healthz) ---"
echo "==== done $(date) ===="