AWS 计算服务实战部署指南:从零搭建 EC2、Lambda、ECS、Auto Scaling 与 LightSail
> 关键词:AWS EC2部署教程、Lambda函数创建、ECS容器部署、Auto Scaling配置、LightSail建站、AWS CLI实战、云计算运维、AWS新手教程
- 引言:看懂文档和真正上手,隔着一条鸿沟
- EC2 实战:从零创建并加固一台生产级云服务器
- Lambda 实战:部署一个完整的无服务器 CRUD API
- ECS 实战:容器化 Node.js 应用从镜像到上线
- Auto Scaling 实战:为 EC2 应用穿上弹性盔甲
- LightSail 实战:5 分钟上线一个 WordPress 网站
- 五大服务部署复杂度对比
- 排坑指南:跨服务的三个致命错误
关键词:AWS EC2部署教程、Lambda函数创建、ECS容器部署、Auto Scaling配置、LightSail建站、AWS CLI实战、云计算运维、AWS新手教程
引言:看懂文档和真正上手,隔着一条鸿沟
很多开发者把 AWS 官方文档翻了一遍又一遍,选型对比图也烂熟于心,但一打开终端输入 aws ec2 run-instances,立刻卡在安全组规则、IAM 角色和网络配置的连环坑里。更糟糕的是,文档通常只告诉你"某个参数是做什么的",却很少告诉你"这个参数填错了会导致生产事故"。
本文不讨论"选哪个"——那是上篇文章的事。本文只做一件事:带你用 AWS CLI 一行行敲出每个计算服务的核心部署流程,从零到生产可用的最小配置。 所有命令基于 aws-cli v2,建议在 Linux/macOS 终端中边看边执行。每一节最后都附有实战中踩过的深坑和避坑方案,这些才是你在任何官方文档中都找不到的干货。
一、EC2 实战:从零创建并加固一台生产级云服务器
1.1 前期准备:确认 CLI 与密钥对
# 确认 AWS CLI 已安装并配置凭证
aws --version
aws sts get-caller-identity # 验证身份
# 创建 SSH 密钥对(新机器必备)
aws ec2 create-key-pair \
--key-name my-prod-key \
--query 'KeyMaterial' \
--output text > ~/.ssh/my-prod-key.pem
chmod 400 ~/.ssh/my-prod-key.pem
1.2 创建安全组:最小权限原则
安全组是 EC2 的第一道防火墙,默认拒绝所有入站流量。以下配置仅开放必要端口:
# 获取默认 VPC ID
VPC_ID=$(aws ec2 describe-vpcs \
--filters Name=isDefault,Values=true \
--query 'Vpcs[0].VpcId' --output text)
# 创建安全组
SG_ID=$(aws ec2 create-security-group \
--group-name web-server-sg \
--description "Web server security group" \
--vpc-id $VPC_ID \
--query 'GroupId' --output text)
# 添加规则:SSH(仅限你的 IP)、HTTP、HTTPS
MY_IP=$(curl -s https://checkip.amazonaws.com)
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 22 --cidr ${MY_IP}/32
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 80 --cidr 0.0.0.0/0
aws ec2 authorize-security-group-ingress --group-id $SG_ID \
--protocol tcp --port 443 --cidr 0.0.0.0/0
echo "安全组 ID: $SG_ID"
1.3 编写 User Data 启动脚本
User Data 在实例首次启动时以 root 身份执行,用于自动安装软件和配置环境:
cat > /tmp/user-data.sh << 'EOF'
#!/bin/bash
yum update -y
amazon-linux-extras enable nginx1
yum install -y nginx git htop
# 启动 Nginx 并设置开机自启
systemctl start nginx
systemctl enable nginx
# 写入一个简单首页
cat > /usr/share/nginx/html/index.html << 'HTML'
<!DOCTYPE html>
<html><body>
<h1>AWS EC2 实战部署成功!</h1>
<p>实例启动时间:$(date)</p>
</body></html>
HTML
# 安装 CloudWatch Agent 用于监控
yum install -y amazon-cloudwatch-agent
/opt/aws/amazon-cloudwatch-agent/bin/amazon-cloudwatch-agent-ctl \
-a fetch-config -m ec2 -s
EOF
1.4 启动实例并验证
# 查询最新的 Amazon Linux 2023 AMI
AMI_ID=$(aws ec2 describe-images \
--owners amazon \
--filters "Name=name,Values=al2023-ami-2023*-x86_64" \
--query 'reverse(sort_by(Images, &CreationDate))[0].ImageId' \
--output text)
# 启动实例
INSTANCE_ID=$(aws ec2 run-instances \
--image-id $AMI_ID \
--instance-type t3.micro \
--key-name my-prod-key \
--security-group-ids $SG_ID \
--user-data file:///tmp/user-data.sh \
--tag-specifications 'ResourceType=instance,Tags=[{Key=Name,Value=prod-web-01}]' \
--query 'Instances[0].InstanceId' \
--output text)
echo "实例 ID: $INSTANCE_ID"
# 等待实例进入 running 状态
aws ec2 wait instance-running --instance-ids $INSTANCE_ID
# 获取公网 IP
PUBLIC_IP=$(aws ec2 describe-instances \
--instance-ids $INSTANCE_ID \
--query 'Reservations[0].Instances[0].PublicIpAddress' \
--output text)
echo "公网 IP: $PUBLIC_IP"
echo "访问: http://$PUBLIC_IP"
1.5 EC2 部署中常见的三个坑
| 坑 | 现象 | 解决方案 |
|---|---|---|
| 安全组过于开放 | SSH 端口被暴力破解 | 将 0.0.0.0/0 替换为你的静态 IP 或使用 Session Manager |
| 未启用终止保护 | 误操作 terminate-instances | 生产实例必须 aws ec2 modify-instance-attribute --disable-api-termination |
| 忘记分配弹性 IP | 重启后公网 IP 变化 | 创建 EIP 并绑定:aws ec2 associate-address --instance-id $ID --allocation-id $EIP_ID |
二、Lambda 实战:部署一个完整的无服务器 CRUD API
2.1 编写 Lambda 函数代码(Python 3.12)
创建一个待办事项(Todo)API,演示 Lambda 如何处理 CRUD 操作:
# todo_lambda.py
import json
import os
import uuid
from datetime import datetime
import boto3
from boto3.dynamodb.conditions import Key
dynamodb = boto3.resource('dynamodb')
table = dynamodb.Table(os.environ['TABLE_NAME'])
def lambda_handler(event, context):
"""处理 API Gateway 转发来的 HTTP 请求"""
http_method = event.get('httpMethod', 'GET')
path_params = event.get('pathParameters', {}) or {}
try:
if http_method == 'GET' and path_params.get('id'):
return get_todo(path_params['id'])
elif http_method == 'GET':
return list_todos()
elif http_method == 'POST':
return create_todo(json.loads(event.get('body', '{}')))
elif http_method == 'PUT' and path_params.get('id'):
return update_todo(path_params['id'], json.loads(event.get('body', '{}')))
elif http_method == 'DELETE' and path_params.get('id'):
return delete_todo(path_params['id'])
else:
return response(405, {'error': 'Method not allowed'})
except Exception as e:
print(f"Error: {str(e)}")
return response(500, {'error': str(e)})
def create_todo(data):
item = {
'id': str(uuid.uuid4()),
'title': data.get('title', 'Untitled'),
'completed': False,
'createdAt': datetime.utcnow().isoformat()
}
table.put_item(Item=item)
return response(201, item)
def get_todo(todo_id):
result = table.get_item(Key={'id': todo_id})
if 'Item' not in result:
return response(404, {'error': 'Todo not found'})
return response(200, result['Item'])
def list_todos():
result = table.scan(Limit=50)
return response(200, result.get('Items', []))
def update_todo(todo_id, data):
update_expr = 'SET '
expr_values = {}
for key in ['title', 'completed']:
if key in data:
update_expr += f'{key} = :{key}, '
expr_values[f':{key}'] = data[key]
update_expr = update_expr.rstrip(', ')
table.update_item(
Key={'id': todo_id},
UpdateExpression=update_expr,
ExpressionAttributeValues=expr_values
)
return response(200, {'id': todo_id, 'updated': True})
def delete_todo(todo_id):
table.delete_item(Key={'id': todo_id})
return response(200, {'id': todo_id, 'deleted': True})
def response(status_code, body):
return {
'statusCode': status_code,
'headers': {
'Content-Type': 'application/json',
'Access-Control-Allow-Origin': '*'
},
'body': json.dumps(body, ensure_ascii=False)
}
2.2 创建 DynamoDB 表与 IAM 角色
# 创建 DynamoDB 表
aws dynamodb create-table \
--table-name todos \
--attribute-definitions AttributeName=id,AttributeType=S \
--key-schema AttributeName=id,KeyType=HASH \
--billing-mode PAY_PER_REQUEST
# 创建 IAM 执行角色
ROLE_ARN=$(aws iam create-role \
--role-name lambda-todo-role \
--assume-role-policy-document '{
"Version": "2012-10-17",
"Statement": [{
"Effect": "Allow",
"Principal": {"Service": "lambda.amazonaws.com"},
"Action": "sts:AssumeRole"
}]
}' --query 'Role.Arn' --output text)
# 附加 DynamoDB 和 CloudWatch Logs 权限
aws iam attach-role-policy --role-name lambda-todo-role \
--policy-arn arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess
aws iam attach-role-policy --role-name lambda-todo-role \
--policy-arn arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
# 等待角色传播(IAM 角色需要几秒钟才能被 Lambda 使用)
echo "等待 IAM 角色传播..."
sleep 10
2.3 打包并创建 Lambda 函数
# 打包代码为 zip
zip todo_lambda.zip todo_lambda.py
# 创建函数
FUNCTION_ARN=$(aws lambda create-function \
--function-name todo-api \
--runtime python3.12 \
--role $ROLE_ARN \
--handler todo_lambda.lambda_handler \
--zip-file fileb://todo_lambda.zip \
--environment Variables="{TABLE_NAME=todos}" \
--timeout 10 \
--memory-size 256 \
--query 'FunctionArn' --output text)
echo "函数 ARN: $FUNCTION_ARN"
# 创建函数 URL(快速测试,生产环境建议走 API Gateway)
aws lambda create-function-url-config \
--function-name todo-api \
--auth-type NONE
FUNC_URL=$(aws lambda get-function-url-config \
--function-name todo-api \
--query 'FunctionUrl' --output text)
echo "函数 URL: $FUNC_URL"
# 测试调用
curl -X POST $FUNC_URL \
-H "Content-Type: application/json" \
-d '{"title": "学习 AWS Lambda 实战"}'
2.4 Lambda 冷启动优化速查
| 策略 | 原理 | 额外成本 | 适用场景 |
|---|---|---|---|
| Provisioned Concurrency | 预置 N 个"热"实例 | 有(按预置数 × 时长计费) | 对延迟敏感的生产 API |
| 减小部署包体积 | 轻量依赖 → 更快初始化解包 | 无 | 所有场景 |
| 使用 SnapStart(Java) | 以快照启动替代冷初始化 | 有限 | Java 11+ 函数 |
| 定时 Ping | CloudWatch 定时事件保持实例温热 | 调用费(极少) | 非关键路径 |
三、ECS 实战:容器化 Node.js 应用从镜像到上线
3.1 准备示例应用
// app.js — 一个简单的 Express API
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
app.get('/', (req, res) => {
res.json({
service: 'ECS Fargate Demo',
version: '1.0.0',
timestamp: new Date().toISOString(),
instance: process.env.HOSTNAME || 'unknown'
});
});
app.get('/health', (req, res) => {
res.status(200).json({ status: 'healthy' });
});
app.listen(PORT, () => {
console.log(`Server running on port ${PORT}`);
});
# Dockerfile
FROM node:20-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY app.js .
EXPOSE 3000
CMD ["node", "app.js"]
// package.json
{
"name": "ecs-demo",
"version": "1.0.0",
"dependencies": {
"express": "^4.18.2"
}
}
3.2 构建镜像并推送到 ECR
# 登录 ECR
ACCOUNT_ID=$(aws sts get-caller-identity --query 'Account' --output text)
REGION=$(aws configure get region)
aws ecr get-login-password --region $REGION | \
docker login --username AWS --password-stdin ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com
# 创建仓库
aws ecr create-repository --repository-name ecs-demo-app
# 构建并推送
docker build -t ecs-demo-app .
docker tag ecs-demo-app:latest ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/ecs-demo-app:latest
docker push ${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/ecs-demo-app:latest
3.3 注册任务定义并创建 Fargate 服务
# 注册任务定义
aws ecs register-task-definition \
--family ecs-demo-task \
--network-mode awsvpc \
--requires-compatibilities FARGATE \
--cpu 512 \
--memory 1024 \
--execution-role-arn arn:aws:iam::${ACCOUNT_ID}:role/ecsTaskExecutionRole \
--container-definitions "[{
\"name\": \"demo-app\",
\"image\": \"${ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/ecs-demo-app:latest\",
\"portMappings\": [{\"containerPort\": 3000, \"protocol\": \"tcp\"}],
\"essential\": true,
\"logConfiguration\": {
\"logDriver\": \"awslogs\",
\"options\": {
\"awslogs-group\": \"/ecs/demo-app\",
\"awslogs-region\": \"${REGION}\",
\"awslogs-stream-prefix\": \"ecs\"
}
}
}]"
# 创建 ECS 集群
aws ecs create-cluster --cluster-name demo-cluster
# 创建 ECS 服务
aws ecs create-service \
--cluster demo-cluster \
--service-name demo-service \
--task-definition ecs-demo-task \
--desired-count 2 \
--launch-type FARGATE \
--network-configuration "awsvpcConfiguration={
subnets=[$(aws ec2 describe-subnets --query 'Subnets[0:2].SubnetId' --output text | tr '\t' ',')],
assignPublicIp=ENABLED
}"
echo "服务已创建,等待任务启动..."
3.4 ECS Fargate 配置速查表
| 参数 | 可选范围 | 推荐值(小型 Web 应用) |
|---|---|---|
| task CPU | 256–16384 (单位:1/1024 vCPU) | 512 (0.5 vCPU) |
| task Memory | 512–30720 (MiB) | 1024 (1 GB) |
| desired count | 1–N | 2 (跨 AZ 高可用) |
| 健康检查路径 | 自定义 | /health(返回 200) |
| 日志保留期 | 1–3650 天 | 30 天(平衡调试与成本) |
四、Auto Scaling 实战:为 EC2 应用穿上弹性盔甲
4.1 创建启动模板
启动模板是 Auto Scaling 的"模具",定义了每个新实例的规格:
aws ec2 create-launch-template \
--launch-template-name web-asg-template \
--version-description "v1 - Amazon Linux 2023 + Nginx" \
--launch-template-data '{
"ImageId": "'"$AMI_ID"'",
"InstanceType": "t3.micro",
"KeyName": "my-prod-key",
"SecurityGroupIds": ["'"$SG_ID"'"],
"UserData": "'"$(base64 -w0 /tmp/user-data.sh)"'",
"TagSpecifications": [{
"ResourceType": "instance",
"Tags": [{"Key": "Name", "Value": "web-asg-instance"}]
}]
}'
4.2 创建 Auto Scaling Group(ASG)
# 获取两个不同可用区的子网 ID
SUBNET_IDS=$(aws ec2 describe-subnets \
--filters Name=default-for-az,Values=true \
--query 'Subnets[0:2].SubnetId' \
--output text | tr '\t' ',')
echo "子网列表: $SUBNET_IDS"
# 创建 ASG
aws autoscaling create-auto-scaling-group \
--auto-scaling-group-name web-asg \
--launch-template "LaunchTemplateName=web-asg-template,Version=\$Latest" \
--min-size 2 \
--max-size 6 \
--desired-capacity 2 \
--vpc-zone-identifier "$SUBNET_IDS" \
--health-check-type ELB \
--health-check-grace-period 120 \
--default-cooldown 180 \
--tags "Key=Environment,Value=production,PropagateAtLaunch=true"
4.3 配置目标追踪伸缩策略
目标追踪策略会自动调整实例数量,将指定指标维持在目标值附近:
# 基于平均 CPU 使用率的伸缩策略
aws autoscaling put-scaling-policy \
--auto-scaling-group-name web-asg \
--policy-name cpu-target-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ASGAverageCPUUtilization"
},
"TargetValue": 60.0,
"DisableScaleIn": false
}'
# 基于 ALB 请求数的伸缩策略(需先绑定 ALB)
aws autoscaling put-scaling-policy \
--auto-scaling-group-name web-asg \
--policy-name request-count-tracking \
--policy-type TargetTrackingScaling \
--target-tracking-configuration '{
"PredefinedMetricSpecification": {
"PredefinedMetricType": "ALBRequestCountPerTarget"
},
"TargetValue": 1000.0
}'
echo "伸缩策略已就绪:CPU 目标 60%,每目标请求数 1000"
4.4 Auto Scaling 关键参数解释
| 参数 | 含义 | 生产环境建议值 |
|---|---|---|
min-size | 最少保持的实例数 | ≥2(避免单点故障) |
max-size | 最多扩容到的实例数 | 根据预算设上限 |
desired-capacity | 初始目标实例数 | 介于 min 和 max 之间 |
health-check-grace-period | 新实例豁免健康检查的时间(秒) | 120–300(取决于启动耗时) |
default-cooldown | 两次伸缩活动之间的冷却期(秒) | 180–300(防止频繁振荡) |
cooldown vs step adjustment | 简单伸缩用冷却,步进伸缩更精确 | 步进伸缩适合大流量场景 |
五、LightSail 实战:5 分钟上线一个 WordPress 网站
5.1 一条命令创建 LightSail 实例
LightSail 的核心哲学是简单——不需要 VPC、安全组、IAM 角色等概念:
# 创建 WordPress 实例(预装好的蓝图)
aws lightsail create-instances \
--instance-names wp-blog \
--availability-zone us-east-1a \
--blueprint-id wordpress \
--bundle-id small_1_0 \
--user-data 'WORDPRESS_SITE_TITLE=我的技术博客
WORDPRESS_ADMIN_USER=admin
[email protected]'
# 查询实例状态
aws lightsail get-instance --instance-name wp-blog \
--query 'instance.{Name:name,State:state.name,IP:publicIpAddress}'
5.2 配置静态 IP 与防火墙
# 分配静态 IP(重启不会变)
aws lightsail allocate-static-ip --static-ip-name wp-blog-ip
aws lightsail attach-static-ip \
--static-ip-name wp-blog-ip \
--instance-name wp-blog
# 开放 443 端口(HTTPS)
aws lightsail open-instance-public-ports \
--instance-name wp-blog \
--port-info fromPort=443,toPort=443,protocol=TCP
# 查询当前防火墙规则
aws lightsail get-instance-port-states --instance-name wp-blog
5.3 创建自动快照
LightSail 提供自动快照功能,每天自动备份,保留最近 7 天:
# 启用自动快照
aws lightsail enable-add-on \
--resource-name wp-blog \
--add-on-request addOnType=AutoSnapshot,autoSnapshotAddOnRequest=\{snapshotTimeOfDay="06:00"\}
# 手动创建即时快照(变更前必做)
aws lightsail create-instance-snapshot \
--instance-name wp-blog \
--instance-snapshot-name wp-blog-pre-change-$(date +%Y%m%d)
5.4 LightSail 套餐选择决策表
| 使用场景 | 推荐套餐 | 月费 | 关键考量 |
|---|---|---|---|
| 个人博客 / 作品集 | $3.50 (512 MB) | ~¥25 | 够用,CDN 分担流量 |
| 中小企业官网 | $5.00 (1 GB) | ~¥36 | WordPress + 静态缓存 |
| WooCommerce 独立站 | $10.00 (2 GB) | ~¥72 | 2 GB 内存支撑 PHP + MySQL |
| 多站点 / 社区论坛 | $20.00 (4 GB) | ~¥144 | 建议配合 Lightsail 负载均衡器 |
| API 后端 + 数据库 | $40.00 (8 GB) | ~¥288 | 此时应评估是否迁移到 EC2 |
六、五大服务部署复杂度对比
| 维度 | EC2 | Lambda | ECS Fargate | Auto Scaling | LightSail |
|---|---|---|---|---|---|
| 首次上手时间 | 15–30 分钟 | 10–20 分钟 | 30–60 分钟 | 20–40 分钟(在 EC2 基础上) | 3–5 分钟 |
| 需要了解的概念数 | ~10 (VPC/SG/AMI/EBS/ENI...) | ~4 (角色/触发器/层/并发) | ~8 (集群/任务/服务/ECR...) | ~6 (模板/策略/冷却/钩子...) | ~3 (实例/静态IP/快照) |
| CLI 命令数(最小部署) | 5–8 条 | 4–6 条 | 8–12 条 | 5–8 条 | 1–3 条 |
| 是否需要 Docker | 否 | 否 | 是 | 否(但通常配合容器) | 否 |
| 自动化部署工具 | CloudFormation / CDK / Terraform | SAM / Serverless Framework | Copilot / CDK | CloudFormation | 控制台为主 |
七、排坑指南:跨服务的三个致命错误
7.1 将 EC2 的思维套用到 Lambda
最常见的错误是在 Lambda 函数中写 setInterval、维护全局连接池、或期望两次调用之间"记住"状态。Lambda 每次调用可能运行在完全不同的容器实例上,函数外的全局变量不可靠。
错误做法:
let connection = null; // ← 全局变量,冷启动后可能被复用
正确做法:
在 handler 内部创建连接,或使用 parameter store / DynamoDB 存储状态
7.2 忘记为 Fargate 任务配置日志驱动
如果任务定义的 containerDefinitions 中没有 logConfiguration,容器崩溃后你完全看不到任何错误日志——CloudWatch 里空空如也,ECS 控制台也只显示 STOPPED (Essential container in task exited)。
7.3 Auto Scaling 缩容过于激进
同时设置低的 default-cooldown(如 60 秒)和激进的目标追踪策略,会导致"抖振"(flapping)——流量轻微下降立即缩容,缩容后剩余实例负载飙升又触发扩容,循环往复。生产环境冷却期建议不低于 180 秒,并结合步进伸缩使用多个告警阈值。
结语:从"看会了"到"真会了"
在这篇文章中,我们用纯 CLI 命令走完了 AWS 五大计算服务的完整部署路径:从 EC2 实例的创建与安全加固,到 Lambda 无服务器 API 的编码与上线;从 ECS 容器化应用的镜像构建与服务部署,到 Auto Scaling 的弹性策略配置,最后用 LightSail 三行命令跑起一个 WordPress。
所有这些命令都可以直接复制到你的终端执行(替换占位符后),不需要任何鼠标点击。如果你跟着每一节的操作走下来,现在你已经拥有一台运行中的 EC2 服务器、一个可调用的 Lambda CRUD 接口、一个双副本的 Fargate 容器服务、一套自动伸缩的 Web 集群,以及一个可直接发文章的 WordPress 站点。这五个组件放在一起,本身就是一套微缩版的 AWS 生产架构。
真正的云计算能力,不来自记忆几百个服务名称,而来自反复踩坑后形成的那一套可复用的自动化脚本。 把本文的每个命令敲一遍,你就完成了从"看会了"到"真会了"的质变。下一步,将这些手动命令封装成 CloudFormation 模板或 Terraform 模块,实现基础设施即代码,那才是迈向高级云架构师的关键一步。
本文由 5.chengzicloud.cloud 提供,点击访问首页了解更多