
说到把应用跑在 AWS Fargate 上,我之前折腾过不少次。AWS 官方文档确实有点散落各地,很多教程也只教到"Hello World"就结束了,真正上线生产环境要的东西(HTTPS、自定义域名、负载均衡、自动扩缩容)基本都略过了。这篇文章把整个流程串了一遍,从把 Docker 镜像推到 ECR 到配好 CI/CD 流水线,30 分钟左右能搞定一个生产级别的部署。内容挺实在的,分享给大家。
你的应用已经用 Docker 容器化了。接下来呢?
如果用的是 Railway 或 Render 这种 PaaS(平台即服务),直接 git push 就行,剩下的平台帮你搞定。这种便利是真实的,但就像我们在《Vercel、Railway 和 Render 的隐性成本》里讨论的,同样的工作量跑在 AWS 上要多花 3 到 5 倍的钱。
AWS Elastic Container Service(ECS)配合 Fargate 就是另一个选择。Fargate 在托管基础设施上运行你的 Docker 容器,不需要运维 EC2 实例,也不用管理集群。你定义想跑什么,AWS 负责"怎么跑"。
问题在于?文档分散在几十个 AWS 页面,大多数教程只到"Hello World"就停了,HTTPS、自定义域名、负载均衡、自动扩缩容这些生产环境真正需要的东西根本不涉及。
这篇指南不一样。我们从零开始部署一个真实的 Web 应用到生产级别,包括其他教程都跳过的每一个步骤。
学完这篇指南,你将拥有:
开始搭建。
开始之前,你需要准备这些东西:
| 要求 | 如何获取 |
|---|---|
| AWS 账号 | 注册,免费套餐包含一些 ECS 资源 |
| AWS CLI v2 |
brew install awscli(Mac)或安装指南
|
| Docker | Docker Desktop 或 OrbStack |
| 一个用 Docker 容器化的应用 | 如果没有,可以参考我们的Web 开发者 Docker 指南 |
| 一个域名(可选) | 用于第 7 步的 HTTPS 配置 |
配置 AWS CLI:
aws configure
# AWS Access Key ID: <你的访问密钥>
# AWS Secret Access Key: <你的私有密钥>
# Default region name: us-east-1
# Default output format: json
💡 提示: 生产环境建议用 IAM Identity Center(SSO)而不是长期有效的访问密钥。这个我们会在《IAM 角色详解》文章里详细讲。
开始点点之前,先看看我们要搭建的完整架构:
请求流程是这样的:
app.yourdomain.com)现在来逐个搭建这些组件。
Amazon Elastic Container Registry(ECR)是 AWS 的私有 Docker 镜像仓库,ECS 从这里拉取镜像。
aws ecr create-repository \
--repository-name my-web-app \
--region us-east-1 \
--image-scanning-configuration scanOnPush=true \
--encryption-configuration encryptionType=AES256
scanOnPush=true 开启每次推送时自动扫描漏洞,免费且强烈推荐。
# 1. 获取你的 AWS 账号 ID
AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query "Account" --output text)
# 2. 让 Docker 通过 ECR 认证
aws ecr get-login-password --region us-east-1 | \
docker login --username AWS --password-stdin \
${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com
# 3. 构建镜像(x86 架构,Fargate 默认使用的)
docker build --platform linux/amd64 -t my-web-app .
# 4. 标记为 ECR 镜像
docker tag my-web-app:latest \
${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest
# 5. 推送
docker push \
${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest
⚠️ 使用苹果芯片的用户(M1/M2/M3):
--platform linux/amd64这个参数很关键。没有它的话,构建的是 ARM 镜像,在 x86 Fargate 上可能会崩溃。也可以用--platform linux/arm64然后把 Fargate 任务配置为 ARM(Graviton 处理器),这样便宜 20%。
aws ecr describe-images \
--repository-name my-web-app \
--region us-east-1
应该能看到你的镜像及其摘要和大小。
ECS 集群是任务和服务的逻辑分组。用 Fargate 的话,集群本质上就是个命名空间——没有 EC2 实例需要管理。
aws ecs create-cluster \
--cluster-name my-app-cluster \
--setting name=containerInsights,value=enabled
Container Insights 会把 CPU、内存和网络指标添加到 CloudWatch,生产环境调试时很有用。
就这么简单。一条命令,集群就准备好了。
ECS 需要一个 IAM 角色来代替你从 ECR 拉取镜像和发送日志到 CloudWatch。
cat > ecs-trust-policy.json << 'EOF'
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": "ecs-tasks.amazonaws.com"
},
"Action": "sts:AssumeRole"
}
]
}
EOF
# 创建角色
aws iam create-role \
--role-name ecsTaskExecutionRole \
--assume-role-policy-document file://ecs-trust-policy.json
# 附加托管策略
aws iam attach-role-policy \
--role-name ecsTaskExecutionRole \
--policy-arn arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy
这授权 ECS 可以:
任务定义是你容器的蓝图。它定义了运行哪个镜像、分配多少 CPU/内存、暴露哪些端口、以及日志发到哪里。
cat > task-definition.json << EOF
{
"family": "my-web-app",
"networkMode": "awsvpc",
"requiresCompatibilities": ["FARGATE"],
"cpu": "256",
"memory": "512",
"executionRoleArn": "arn:aws:iam::${AWS_ACCOUNT_ID}:role/ecsTaskExecutionRole",
"containerDefinitions": [
{
"name": "web",
"image": "${AWS_ACCOUNT_ID}.dkr.ecr.us-east-1.amazonaws.com/my-web-app:latest",
"portMappings": [
{
"containerPort": 3000,
"hostPort": 3000,
"protocol": "tcp"
}
],
"essential": true,
"environment": [
{
"name": "NODE_ENV",
"value": "production"
},
{
"name": "PORT",
"value": "3000"
}
],
"logConfiguration": {
"logDriver": "awslogs",
"options": {
"awslogs-group": "/ecs/my-web-app",
"awslogs-region": "us-east-1",
"awslogs-stream-prefix": "ecs",
"awslogs-create-group": "true"
}
},
"healthCheck": {
"command": ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:3000/health || exit 1"],
"interval": 30,
"timeout": 5,
"retries": 3,
"startPeriod": 60
}
}
]
}
EOF
aws ecs register-task-definition \
--cli-input-json file://task-definition.json