1. 为什么我三年内用 Amplify 重构了 7 个生产项目——一个全栈开发者的真实视角
你有没有过这种体验:凌晨两点,对着一个刚上线的 React 管理后台抓狂。前端页面跑得飞快,但用户一点击“导出报表”,整个 UI 就卡死三秒——因为后端 API 正在慢悠悠地从 RDS 里捞数据、拼 SQL、再塞进 JSON 返回。你翻着 CloudWatch 日志,发现 Lambda 冷启动耗时 842ms,DynamoDB 的读取容量单位(RCU)被爆到红色警报,而 Cognito 用户池里还躺着 37 个没验证邮箱的测试账号……这不是虚构场景,这是我 2021 年接手的第一个 Amplify 项目前的真实日常。
AWS Amplify 不是又一个“帮你封装几行代码”的玩具工具链。它是一套以开发者认知模型为起点重新设计的云开发操作系统。它的核心价值,不是“简化”,而是“对齐”——把前端工程师写 React 组件的直觉,和后端工程师定义数据模型的逻辑,在同一个 TypeScript 文件里完成映射;把 Git 提交这个动作,直接变成一次可审计、可回滚、带环境隔离的全栈部署;把“我要加个登录功能”这个模糊需求,压缩成amplify add auth后三分钟内生成的、符合 OWASP ASVS 4.0 标准的完整认证流。我见过太多团队在 Amplify 上栽跟头,不是因为工具不行,而是误把它当成了“低代码平台”。它恰恰相反——它要求你更早、更清晰地思考数据边界、权限模型和事件驱动逻辑。比如,当你在defineAuth()里写下allow: owner这一行时,你不是在配置一个开关,而是在声明一条业务契约:任何未通过 Cognito 身份令牌校验的请求,连 Lambda 的门都摸不到。这种思维转换,才是 Amplify 真正的门槛,也是它能释放生产力的根本原因。它不替你做决策,但它让每个决策的代价和收益变得无比透明。如果你正在评估是否要引入 Amplify,别问“它能不能做 XX 功能”,先问自己:“我的团队是否准备好用代码来定义安全策略,而不是靠 Dashboard 点点点?”
2. Amplify 的底层设计哲学:为什么它敢说“代码即架构”
2.1 从 Gen 1 到 Gen 2:一场静默的范式革命
很多老用户看到 Amplify Gen 2 的文档第一反应是懵的——怎么连schema.graphql文件都没了?@model指令也换成了@model()装饰器?这背后不是简单的语法糖升级,而是一次彻底的基础设施抽象层重铸。Gen 1 的本质,是一个强大的 GraphQL Schema 编译器。你写一个.graphql文件,Amplify CLI 在背后调用 AppSync 的 GraphQL Transformer,自动生成 DynamoDB 表结构、Lambda Resolver、Cognito 用户池配置,最后打包成 CloudFormation 模板。这个过程像一个黑箱:你给它输入,它吐出一堆 YAML,但中间发生了什么?谁在控制索引策略?如何微调 IAM 权限粒度?答案是:Transformer。你只能祈祷它的默认行为符合你的场景。
Gen 2 彻底打破了这个黑箱。它把基础设施的“编译权”交还给开发者。现在,defineData()函数不再生成模板,而是直接返回一个CDK Construct 实例。这意味着,你写的每一行 TypeScript,都在直接操作 AWS CDK 的底层对象。比如,你想为Post模型添加一个全局二级索引(GSI)用于按作者查询,Gen 1 里你得在@key指令里写一堆嵌套参数;而在 Gen 2 中,你直接调用 CDK 的dynamodb.Table.addGlobalSecondaryIndex()方法:
// amplify/data/resource.ts import { defineData, type DefineDataProps } from '@aws-amplify/backend'; import { Stack } from 'aws-cdk-lib'; import { Table } from 'aws-cdk-lib/aws-dynamodb'; export const data = defineData({ schema: ` type Post @model { id: ID! title: String! author: String! content: String } `, authorizationModes: { defaultAuthorizationMode: 'API_KEY', }, // 关键:直接访问 CDK Stack 和 Table 实例 overrides: (stack: Stack, resource: { table: Table }) => { resource.table.addGlobalSecondaryIndex({ indexName: 'byAuthor', partitionKey: { name: 'author', type: 'string' }, projectionType: 'ALL' }); } });这段代码的价值在于:它不再是“声明式配置”,而是“命令式编程”。你可以在这里插入任意 CDK 逻辑——添加 CloudWatch 告警、配置 KMS 密钥轮换策略、甚至集成 Step Functions 构建状态机。这种能力,让 Amplify 从一个“应用部署工具”跃升为“云原生应用构建平台”。我去年帮一家电商客户迁移订单系统时,就利用这个特性,在overrides里为订单表注入了 DynamoDB TTL(Time-To-Live)自动清理逻辑,并关联了 SNS 主题触发库存回滚。整个流程没有写一行 Lambda,全部在resource.ts里完成。这就是 Gen 2 的核心哲学:基础设施不是配置项,而是你应用代码的延伸部分,必须能被版本控制、单元测试和 CI/CD 流水线管理。
2.2 “代码即架构”的实操代价与回报
当然,这种自由不是没有代价的。最大的心智负担,是从“配置思维”切换到“编程思维”。在 Gen 1 里,amplify add api后你会得到一个预设好的 GraphQL API,开箱即用;在 Gen 2 里,amplify add api只是创建了一个空的resource.ts文件,你需要亲手定义defineApi()的所有参数。这导致初期学习曲线陡峭。我建议新手采用“渐进式暴露”策略:先用 Amplify CLI 生成一个最简defineData()骨架,然后只修改schema字符串,等熟悉了 GraphQL SDL 语法后,再逐步解锁authorizationModes、overrides等高级选项。千万别一上来就试图手写整个 CDK 架构——那相当于用汇编语言写 Web 应用。
另一个隐性成本是本地开发环境的调试复杂度。Gen 1 的amplify mock能完美模拟 AppSync 和 DynamoDB 的本地行为;Gen 2 的amplify sandbox则依赖于真实的 AWS 资源(虽然免费额度足够),这意味着你无法完全离线开发。我的解决方案是:在sandbox启动后,立刻用amplify pull将当前环境的资源状态同步到本地amplify/backend目录,这样即使网络中断,你也能基于已拉取的配置继续编码。更重要的是,永远不要在sandbox环境里做破坏性操作。我见过不止一个团队在sandbox里误删了生产数据库的备份策略,结果花了两天时间从 S3 Glacier 恢复快照。我的铁律是:sandbox只用于验证数据模型和 API 接口,所有涉及 IAM 角色、VPC 配置或生产级加密的操作,必须在amplify push --env dev的正式环境中进行,并且每次push前,必用amplify status和amplify diff对比变更清单。
2.3 为什么“Git 即部署”是全栈协作的终极解法
Amplify 的 Git 集成,远不止“推代码自动部署”这么简单。它解决的是全栈开发中一个根深蒂固的痛点:前后端交付节奏错位。传统模式下,前端团队写完 React 组件,得等后端团队部署好 API 才能联调;后端团队改了个字段名,得发邮件通知所有前端同事更新接口文档。Amplify 把这个链条压缩成一个原子操作:git push origin main。这一行命令背后,是三个并行发生的自动化流水线:
- 前端构建流水线:检测到
main分支推送,自动拉取代码,运行npm run build,将产物上传至 S3,并通过 CloudFront 全球分发。 - 后端部署流水线:解析
amplify/backend目录下的 TypeScript 定义,生成 CDK 模板,执行cdk deploy,更新 DynamoDB 表结构、AppSync API、Cognito 用户池等所有资源。 - 环境同步流水线:将本次部署的后端资源 ARN(如 API Gateway 的 URL、DynamoDB 表名)自动注入到前端构建环境变量中,确保前端代码在构建时就能拿到最新的后端地址。
这带来的协作效率提升是颠覆性的。我们团队曾用 Amplify 开发一个内部 HR 系统,前端和后端由同一组人负责。当产品经理提出“员工档案页需要增加紧急联系人字段”时,开发同学的完整工作流是:
- 在
amplify/data/resource.ts的schema字符串里添加emergencyContact: String字段; - 在
src/components/EmployeeProfile.tsx里新增对应的表单控件和提交逻辑; git add . && git commit -m "feat: add emergency contact field" && git push origin main;- 5 分钟后,
dev.example.com环境已上线新功能,staging.example.com环境同步更新,prod.example.com环境等待手动审批。
整个过程没有跨团队沟通,没有 API 文档同步,没有环境配置文件冲突。Git 提交记录就是最权威的架构演进日志。这也是为什么我坚持认为,Amplify 最大的价值不在技术层面,而在组织层面——它让“全栈”从一个职位描述,变成了一个可落地的协作范式。
3. 核心功能深度拆解:从“能用”到“用透”的关键细节
3.1 数据建模:GraphQL SDL 不是银弹,但它是最好的起点
Amplify 的数据建模能力,常被误解为“只能做 CRUD”。实际上,它的 GraphQL Schema Definition Language(SDL)是一个极其灵活的建模语言,其表达力远超传统 ORM。关键在于理解@model、@auth、@key这三个核心指令的组合逻辑。
@model是基础,它告诉 Amplify:“这个类型需要持久化到 DynamoDB”。但真正决定数据访问性能和安全边界的,是@key和@auth。比如,一个博客系统的Post模型,如果只写:
type Post @model { id: ID! title: String! content: String! author: String! }这会产生一个主键为id的 DynamoDB 表,所有查询都必须通过id进行。但现实业务中,你几乎不会只按id查文章。你需要按作者查、按标签查、按发布时间范围查。这时@key就派上用场了:
type Post @model( # 主键:id @key(name: "byId", fields: ["id"]) # GSI1:按作者 + 发布时间排序 @key(name: "byAuthor", fields: ["author", "createdAt"], queryField: "postsByAuthor") # GSI2:按标签 + 发布时间排序 @key(name: "byTag", fields: ["tag", "createdAt"], queryField: "postsByTag") ) { id: ID! title: String! content: String! author: String! tag: String! # 新增标签字段 createdAt: AWSDateTime! # 自动填充时间戳 }这段定义会生成一个 DynamoDB 表,包含一个主分区键id,以及两个全局二级索引(GSI):byAuthor(分区键author,排序键createdAt)和byTag(分区键tag,排序键createdAt)。queryField参数则会在自动生成的 GraphQL 查询中暴露postsByAuthor和postsByTag两个字段。这才是真正的“按需建模”。
而@auth则是安全的基石。它不是简单的“谁可以读”,而是精确到“谁在什么条件下可以读什么数据”。例如:
type Post @model( @auth(rules: [ # 所有用户可读公开文章 { allow: public, operations: [read], when: "status == 'published'" }, # 作者可读写自己的草稿 { allow: owner, ownerField: "author", operations: [read, create, update, delete] }, # 管理员可读写所有文章 { allow: groups, groups: ["admin"], operations: [read, create, update, delete] } ]) ) { id: ID! title: String! content: String! author: String! status: String! # 'draft' or 'published' }这个规则意味着:一个普通用户发起getPost(id: "xxx")查询时,Amplify 自动生成的 AppSync Resolver 会自动在查询条件中加入status == 'published'的过滤器;而当管理员查询时,这个过滤器会被忽略。这种细粒度的权限控制,是传统 RBAC(基于角色的访问控制)难以企及的。我建议在项目初期就花时间设计@auth规则,而不是等到上线前才补。因为一旦数据量上去,修改@auth规则可能触发全表扫描,影响性能。
3.2 认证与授权:Cognito 不是黑盒,而是你的安全策略执行引擎
Amplify 的Auth类封装了 Cognito 的复杂性,但这绝不意味着你可以忽视底层原理。很多团队踩坑,是因为把Auth.signUp()当成一个魔法函数,却不知道它背后触发了多少个 Cognito 流程。
当你调用Auth.signUp()时,实际发生的是:
- Cognito User Pool 接收注册请求,创建一个
CONFIRMATION_REQUIRED状态的用户; - 如果启用了邮件验证,Cognito 自动调用 SES(Simple Email Service)发送确认邮件;
- 用户点击邮件中的链接,触发 Cognito 的
ConfirmSignUpAPI; - 用户状态变为
CONFIRMED,此时才能调用Auth.signIn()。
这个流程中,最容易被忽略的是自定义验证逻辑。比如,你的业务要求新用户邮箱必须属于公司域名(@yourcompany.com)。在 Gen 1 中,你得写一个 PreSignUp Lambda Trigger;在 Gen 2 中,你可以在defineAuth()的overrides里直接注入:
// amplify/auth/resource.ts import { defineAuth, SecretValue } from '@aws-amplify/backend'; export const auth = defineAuth({ loginWith: { email: true, }, // 关键:自定义 PreSignUp 触发器 overrides: (authResource) => { authResource.addPreSignupTrigger({ handler: { code: ` exports.handler = async (event) => { const emailDomain = event.request.userAttributes.email.split('@')[1]; if (emailDomain !== 'yourcompany.com') { throw new Error('Only company emails are allowed'); } return event; }; `, }, }); } });这段代码会在用户注册前执行,直接拒绝非公司邮箱的注册请求。更重要的是,它被纳入了 CDK 的资源管理,可以和defineData()一样进行版本控制和测试。
另一个高频问题是多因素认证(MFA)的平滑启用。很多团队想“先上线,再加 MFA”,结果发现开启 MFA 后,所有现有用户首次登录都会被强制绑定 TOTP(时间验证码)。这会导致大量用户投诉。我的经验是:永远在项目初始化阶段就规划好 MFA 策略。如果业务允许,使用defineAuth()的mfa: { mode: 'OPTIONAL' },让用户在个人设置里自主开启;如果合规强制要求,则在amplify init时就选择mfa: { mode: 'ON' },并在上线前通过Auth.forgotPassword()流程,批量为所有用户生成初始恢复码。切记,MFA 策略一旦生效,就无法降级为OFF,这是 Cognito 的硬性限制。
3.3 存储:S3 不是文件柜,而是你的内容分发中枢
Amplify Storage 的Storage.put()看似简单,但其level参数(public/protected/private)背后,是整套精细的 S3 ACL(访问控制列表)和 Cognito Identity Pool 权限策略。理解这个机制,是避免“数据泄露”事故的关键。
level: 'public':文件上传到 S3 的public/前缀下,任何人都可以通过公开 URL 访问。适合静态资源(Logo、Banner 图)。level: 'protected':文件上传到protected/${userSub}/前缀下(userSub是 Cognito 用户唯一标识符),只有该用户或拥有cognito-identity.amazonaws.com:sub声明的其他服务才能访问。这是最常用的安全级别。level: 'private':文件上传到private/${userSub}/前缀下,仅该用户本人可通过Storage.get()获取,连管理员也无法直接访问。
很多人误以为protected级别是“用户私有”,其实不然。protected的设计初衷是“用户专属但可共享”。比如,一个用户上传了一张照片,他可以生成一个临时签名 URL,分享给朋友查看,而无需开放整个 S3 存储桶。这个签名 URL 的有效期、权限(只读/读写)都可以在Storage.get()时动态指定:
// 生成一个 1 小时有效的只读签名 URL const url = await Storage.get('user/jane/profile.jpg', { level: 'protected', expires: 3600, // 秒 download: false });而private级别则更严格,它依赖于 Cognito Identity Pool 的角色权限。当你调用Storage.get()时,Amplify 会自动将当前用户的 Cognito Identity ID 注入到请求中,S3 的 bucket policy 会检查这个 ID 是否匹配private/前缀下的路径。这意味着,即使你拿到了一个private文件的 URL,没有有效的 Cognito 令牌,S3 也会返回 403 Forbidden。
我在一个医疗影像应用中就吃过亏。最初所有患者报告都存protected,结果医生误操作,把一个protectedURL 发给了患者家属,家属点开就能看到所有报告。后来我们强制所有敏感报告走private级别,并在defineStorage()的access配置中,明确禁止guest角色访问private前缀:
// amplify/storage/resource.ts export const storage = defineStorage({ name: 'medical-reports', access: allow => ({ 'private/${user.sub}/*': [ allow.authenticated.to(['get', 'put', 'delete']) ], // 明确禁止 guest 访问 private 前缀 }) });这个配置确保了,即使前端代码出现 Bug,也无法绕过权限检查。安全,永远是设计出来的,而不是测试出来的。
3.4 函数与扩展:Lambda 不是万能胶,而是你的业务逻辑加速器
Amplify Functions 让你能在几秒钟内添加一个 Lambda,但这不等于你应该滥用它。我的黄金法则是:任何能在客户端完成的计算,绝不上 Lambda;任何能用 AppSync Resolver 完成的逻辑,绝不用独立 Lambda。Lambda 的价值,在于处理那些必须脱离 HTTP 请求生命周期的长时任务,或者需要访问其他 AWS 服务的复杂集成。
比如,一个常见的需求是“用户上传图片后,自动生成缩略图”。在 Amplify 中,最佳实践是:
- 用户通过
Storage.put()上传原图到public/前缀; - S3 的
ObjectCreated事件自动触发一个 Lambda(通过defineFunction()定义); - 该 Lambda 下载原图,用 Sharp 库生成缩略图,再调用
Storage.put()上传到thumbnails/前缀。
这个流程中,Lambda 是不可替代的,因为图片处理是 CPU 密集型任务,且耗时可能超过 API Gateway 的 30 秒限制。但如果需求是“用户提交表单时,校验邮箱格式”,这就应该在前端 React 组件里用正则完成,而不是调用一个 Lambda。
另一个关键点是环境变量的安全管理。defineFunction()支持environment参数,但所有环境变量都会以明文形式出现在 Lambda 控制台中。对于数据库密码、API 密钥等敏感信息,必须使用 AWS Secrets Manager:
// amplify/functions/generateThumbnail/resource.ts import { defineFunction } from '@aws-amplify/backend'; import { SecretValue } from 'aws-cdk-lib'; export const generateThumbnail = defineFunction({ name: 'generateThumbnail', environment: { // 从 Secrets Manager 安全读取 SECRET_KEY: SecretValue.secretsManager('my-app/thumbnail-key') } });这样,Lambda 代码里可以通过process.env.SECRET_KEY访问密钥,而密钥本身永远不会出现在 CDK 模板或 Lambda 控制台中。我见过太多团队把数据库密码硬编码在environment里,结果在 Git 历史中被轻易挖出。安全,始于对每一个字符串的敬畏。
4. 实战全流程:从零搭建一个实时协作笔记应用
4.1 项目初始化与环境规划
我们以一个“实时协作笔记应用”为例,完整走一遍 Amplify Gen 2 的开发流程。这个应用的核心需求是:用户登录后,可以创建多个笔记;每个笔记支持多人实时编辑;编辑历史可追溯;所有数据必须加密存储。
第一步,不是写代码,而是规划环境拓扑。我强烈建议采用“三环境模型”:
dev:开发环境,所有资源使用最小规格(如 DynamoDB On-Demand 模式),允许快速迭代;staging:预发布环境,资源配置与prod一致,但数据隔离,用于 UAT(用户验收测试);prod:生产环境,启用所有监控告警,数据备份策略完备。
创建项目:
# 1. 全局安装 CLI(确保 v12+) npm install -g @aws-amplify/cli # 2. 初始化项目(选择 React 框架) npx create-react-app notes-app cd notes-app amplify init # 3. 按提示选择: # ? Enter a name for the project: notes-app # ? Initialize the project with the above configuration? Yes # ? Choose your default editor: Visual Studio Code # ? Choose the type of app that you're building: javascript # ? What javascript framework are you using: react # ? Source Directory Path: src # ? Distribution Directory Path: build # ? Build Command: npm run-script build # ? Start Command: npm run-script start # 4. 创建三个环境 amplify env add dev amplify env add staging amplify env add prod关键点:amplify init时,CLI 会为你创建amplify/.config/project-config.json,其中包含了所有环境的元数据。这个文件必须纳入 Git 版本控制,它是环境一致性的基石。
4.2 数据模型定义:构建实时协作的基石
协作笔记的核心是Note和EditHistory两个模型。Note存储笔记内容,EditHistory记录每一次编辑的详细信息(谁、何时、改了什么)。这里我们利用 DynamoDB 的强一致性读取和 AppSync 的实时订阅能力:
// amplify/data/resource.ts import { defineData, type DefineDataProps } from '@aws-amplify/backend'; import { AuthResources } from '@aws-amplify/backend-auth'; export const data = defineData({ schema: ` type Note @model( @auth(rules: [ { allow: owner, ownerField: "ownerId" }, { allow: groups, groups: ["admin"] } ]) @key(name: "byOwner", fields: ["ownerId", "createdAt"]) ) { id: ID! title: String! content: String! ownerId: String! createdAt: AWSDateTime! updatedAt: AWSDateTime! } type EditHistory @model( @auth(rules: [ { allow: owner, ownerField: "noteId" }, // noteId 是外键,指向 Note.id { allow: groups, groups: ["admin"] } ]) @key(name: "byNote", fields: ["noteId", "timestamp"]) ) { id: ID! noteId: ID! userId: String! userName: String! timestamp: AWSDateTime! changeType: String! # 'create' | 'update' | 'delete' diff: String # JSON string of unified diff } `, authorizationModes: { defaultAuthorizationMode: 'AMAZON_COGNITO_USER_POOLS', apiKeyAuthorizationMode: { expiresInDays: 30 } } });这个设计的关键在于:
Note的@key(name: "byOwner", fields: ["ownerId", "createdAt"])创建了一个 GSI,使得我们可以高效查询某个用户的所有笔记,并按创建时间排序;EditHistory的@key(name: "byNote", fields: ["noteId", "timestamp"])创建了另一个 GSI,让我们能按noteId查询某篇笔记的所有编辑历史,并按时间戳排序;@auth规则确保了数据隔离:用户 A 只能看到自己创建的Note和EditHistory,无法访问用户 B 的数据。
4.3 实时订阅实现:WebSocket 不是魔法,而是可预测的管道
AppSync 的实时订阅,是 Amplify 最惊艳的能力之一。但要让它稳定工作,必须理解其背后的 WebSocket 生命周期管理。
在前端,我们使用API.graphql()的onCreateNote订阅来监听新笔记创建:
// src/App.tsx import { useEffect } from 'react'; import { API, graphqlOperation } from 'aws-amplify'; import { onCreateNote } from './graphql/subscriptions'; function App() { useEffect(() => { // 1. 创建订阅 const subscription = API.graphql( graphqlOperation(onCreateNote) ).subscribe({ // 2. 处理新数据 next: ({ provider, value }) => { console.log('New note created:', value.data.onCreateNote); // 更新本地状态或触发 UI 刷新 }, // 3. 处理错误(网络断开、权限失效等) error: (error) => { console.warn('Subscription error:', error); // 这里应实现重连逻辑 // Amplify 默认不自动重连,需手动处理 setTimeout(() => { // 重新订阅 subscription.unsubscribe(); // 重新执行上面的 subscribe 逻辑 }, 5000); } }); // 4. 组件卸载时取消订阅,防止内存泄漏 return () => { subscription.unsubscribe(); }; }, []); return <div>Real-time Notes</div>; } export default App;这个代码片段揭示了实时订阅的四个关键阶段:创建、处理、错误、清理。很多团队只写了next回调,结果在用户切换页面时,旧的订阅还在后台默默消耗 WebSocket 连接,最终触发 AppSync 的连接数配额限制。我的经验是:永远在useEffect的 cleanup 函数中显式调用unsubscribe()。
更进一步,为了应对网络抖动,我封装了一个useRealtimeSubscriptionHook:
// src/hooks/useRealtimeSubscription.ts import { useEffect, useRef, useState } from 'react'; import { API, graphqlOperation } from 'aws-amplify'; export function useRealtimeSubscription<T>( subscription: any, onNewData: (data: T) => void, onError?: (error: any) => void ) { const subscriptionRef = useRef<any>(null); const [isConnected, setIsConnected] = useState(false); useEffect(() => { const connect = () => { subscriptionRef.current = API.graphql( graphqlOperation(subscription) ).subscribe({ next: ({ value }) => { setIsConnected(true); onNewData(value.data); }, error: (error) => { setIsConnected(false); if (onError) onError(error); // 指数退避重连 setTimeout(connect, Math.min(1000 * Math.pow(2, 3), 30000)); } }); }; connect(); return () => { if (subscriptionRef.current) { subscriptionRef.current.unsubscribe(); } }; }, [subscription, onNewData, onError]); return { isConnected }; }这个 Hook 封装了重连逻辑和连接状态,让业务组件可以专注于数据处理,而不必关心底层连接管理。
4.4 部署与监控:让生产环境不再是个黑盒子
部署不是终点,而是运维的起点。Amplify 的amplify push命令会输出详细的 CDK 部署日志,但这些日志只在终端显示,无法长期追踪。我的标准做法是:
- 启用 CloudWatch Logs:在
amplify/backend/function/的每个 Lambda 函数中,显式配置日志组:
// amplify/functions/processEdit/resource.ts import { defineFunction } from '@aws-amplify/backend'; export const processEdit = defineFunction({ name: 'processEdit', logs: { retention: 'ONE_MONTH' // 日志保留一个月 } });- 配置 CloudWatch Alarms:为关键指标设置告警。比如,为
EditHistory表的WriteCapacityUtilization设置 80% 的阈值告警,防止突发流量打爆写入容量:
// amplify/data/resource.ts (在 overrides 中) import { Metric, ComparisonOperator, TreatMissingData } from 'aws-cdk-lib/aws-cloudwatch'; overrides: (stack: Stack, resource: { table: Table }) => { const writeUtilizationMetric = new Metric({ namespace: 'AWS/DynamoDB', metricName: 'WriteCapacityUtilization', dimensionsMap: { TableName: resource.table.tableName }, statistic: 'Average', period: cdk.Duration.minutes(1) }); writeUtilizationMetric.createAlarm(stack, 'HighWriteUtilization', { comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, threshold: 80, evaluationPeriods: 3, datapointsToAlarm: 3, treatMissingData: TreatMissingData.NOT_BREACHING }); }- 建立部署后验证脚本:每次
amplify push后,自动运行一个脚本,验证关键 API 是否可用:
# scripts/post-deploy-check.sh #!/bin/bash # 检查 AppSync API 是否响应 API_URL=$(amplify status | grep "GraphQL endpoint" | awk '{print $4}') if curl -s -o /dev/null -w "%{http_code}" "$API_URL" | grep -q "200"; then echo "✅ AppSync API is healthy" else echo "❌ AppSync API is down" exit 1 fi # 检查 Cognito 用户池是否可登录 COGNITO_POOL_ID=$(amplify status | grep "User Pool ID" | awk '{print $4}') if aws cognito-idp describe-user-pool --user-pool-id "$COGNITO_POOL_ID" &> /dev/null; then echo "✅ Cognito User Pool is healthy" else echo "❌ Cognito User Pool is down" exit 1 fi这个脚本可以集成到 CI/CD 流水线中,作为amplify push后的最后一个步骤。它让部署不再是“赌一把”,而是可验证、可回滚的确定性操作。
5. 高频问题排查与独家避坑指南
5.1 “amplify push 失败,但错误信息全是乱码”——CDK 构建缓存污染
这是 Gen 2 用户最常遇到的“玄学问题”。现象是:amplify push执行到cdk deploy阶段时,抛出类似TypeError: Cannot read property 'name' of undefined的错误,且堆栈信息指向node_modules/@aws-amplify/backend内部。根本原因,是 Amplify CLI 的构建缓存(位于amplify/backend/.build)与当前 Node.js 版本或依赖包不兼容。
解决方案(三步清除):
- 删除构建缓存:
rm -rf amplify/backend/.build - 清理 Node.js 缓存:
npm cache clean --force - 重新安装 Amplify CLI:
npm install -g @aws-amplify/cli@latest
提示:这个错误通常在你升级 Node.js 版本(如从 18.x 升到 20.x)或全局安装了新版本的 Amplify CLI 后出现。我的习惯是,每次升级 Node.js 后,都执行一次
amplify configure重新配置 CLI,确保所有环境变量和缓存路径正确。
5.2 “前端调用 Auth.signIn() 报错 ‘User does not exist’,但 Cognito 控制台里明明有用户”
这个问题的根源,几乎 100% 是Cognito User Pool 的 Region 配置错误。Amplify 的aws-exports.js文件中,aws_cognito_region字段必须与你的 User Pool 所在的 AWS 区域完全一致。常见错误是:User Pool 创建在us-east-1,但aws-exports.js里写成了us-west-2。
排查步骤:
- 登录 AWS 控制台,进入 Cognito 服务,找到你的 User Pool;
- 在 User Pool 的“概览”页,查看“Pool ARN”,ARN 的格式是
arn:aws:cognito-idp:us-east-1:123456789012:userpool/us-east-1_abc123def,其中us-east-1就是正确的 Region; - 打开
src/aws-exports.js,检查aws_cognito_region的值是否