构建一个仅在本地机器上运行的Web应用程序是一回事,而构建一个安全、能够连接真实数据库并且能让互联网上的任何人使用的Web应用程序则是完全不同的挑战。为此需要使用另一套工具。

大多数生产环境中的Web应用程序都有一些共同的需求:它们需要存储和检索数据,通过API暴露这些数据,要求用户在执行敏感操作之前进行身份验证,同时还需要将应用程序部署在可靠且响应速度快的环境中。

过去,要满足所有这些需求,通常需要管理服务器、配置数据库、处理身份验证相关基础设施,以及准备托管环境——而这些往往都是单独进行的、手动完成的任务。

AWS显著改变了这种模式。通过本教程中会使用的一系列服务(Lambda、DynamoDB、API Gateway、Cognito和CloudFront),你可以无需管理任何一台服务器,就能构建并部署一个功能完备、安全且全球分布式的应用程序。

每项服务都负责特定的任务:

  • DynamoDB用于存储数据

  • Lambda根据需求执行业务逻辑

  • API Gateway将函数以REST API的形式提供出来

  • Cognito负责用户身份验证

  • CloudFront通过HTTPS在全球范围内分发前端应用程序

AWS CDK(云开发工具包)将这些服务全部整合在一起,允许你用TypeScript代码来定义它们。你无需通过AWS控制台手动配置每一项资源,只需在一个文件中描述整个基础设施结构,然后通过一条命令即可完成部署。

完成本教程的学习后,你将能够构建出一个功能完备的供应商管理平台。用户可以注册、登录,然后创建、查看或删除供应商信息,所有数据都会安全地存储在AWS DynamoDB中,而所有的访问请求都会经过Amazon Cognito的身份验证机制进行保护。

你将构建什么

在本教程中,你将构建一个由两个页面组成的Web应用程序,经过身份验证的用户可以使用这个应用程序来:

  • 添加新的供应商信息(包括名称、类别和联系邮箱)

  • 实时查看所有保存的供应商信息

  • 从列表中删除某个供应商

  • 安全地登录或退出系统

前端应用程序是用Next.js构建的,后端服务则完全运行在AWS平台上:DynamoDB负责数据存储,Lambda函数处理业务逻辑,API Gateway提供REST API接口,Cognito负责身份验证,而CloudFront则通过HTTPS在全球范围内分发应用程序内容。

目录

适用人群

本教程适用于那些已经掌握基础JavaScript和React知识,但尚未使用过AWS的开发者。您不需要具备任何后端开发、云计算或DevOps方面的经验。在开始使用任何AWS相关功能之前,我都会先为大家详细解释这些概念。

先决条件

在开始学习之前,请确保您已经安装并准备好了以下工具:

  • Node.js 18或更高版本点击此处下载

  • npm:随Node.js一同提供

  • 代码编辑器:我推荐使用VS Code

  • 终端:macOS、Linux或Windows系统上的任意终端(在Windows系统中建议使用WSL)

  • AWS账户:您将在第一部分中创建一个AWS账户。虽然需要信用卡信息,但本教程中的所有内容都可以在免费 tier范围内完成。

  • 对React和TypeScript有基本了解

系统架构概述

在编写任何代码之前,先来了解一下这些组件是如何协同工作的。

当用户在React应用程序中点击“添加供应商”按钮时:

  1. 前端会从浏览器会话中读取用户的JWT认证令牌

  2. 然后通过POST请求将令牌发送到API Gateway,同时会在请求头中包含该令牌

  3. API Gateway会使用Cognito来验证该令牌的有效性。如果令牌无效或丢失,它会立即返回401错误并拒绝请求

  4. 如果令牌有效,API Gateway会将请求转发给createVendor Lambda函数

  5. Lambda函数会将新添加的供应商信息存储到DynamoDB中

  6. DynamoDB会确认数据写入成功,然后Lambda函数会返回成功响应

  7. 前端会重新获取供应商列表并更新用户界面

读取或删除供应商信息时,流程类似,只是使用的Lambda函数和HTTP方法会有所不同。

供应商追踪应用程序的架构图

应用程序的部署方式:您的React应用程序会被打包成静态网站文件,上传到S3存储桶中,然后通过CloudFront在全球范围内提供服务。后端基础设施(包括Lambda函数、API Gateway、DynamoDB和Cognito)则是使用AWS CDK以TypeScript语言进行定义的,只需执行一条命令即可完成部署。

第一部分:设置您的AWS账户及所需工具

在开始编写任何应用程序代码之前,您需要准备三样东西:一个AWS账户、适合自己使用的开发工具,以及能够让这些工具代表您与AWS进行通信的认证信息。

1.1 创建您的AWS账户

如果您还没有AWS账户:

  1. 访问https://aws.amazon.com

  2. 点击“创建AWS账户”按钮

  3. 按照注册提示完成操作,并添加一种支付方式

  4. 注册成功后,请登录AWS管理控制台

AWS提供了免费 tier,涵盖了本教程中使用的所有服务。在按照教程操作的过程中,正常使用这些服务是不会产生任何费用的。

1.2 安装AWS CLI和CDK

AWS CLI是一种命令行工具,它允许你通过终端与AWS进行交互:查看资源信息、配置凭证等等。

AWS CDK(云开发工具包)则是用于使用TypeScript代码来定义你的后端架构(包括数据库、Lambda函数和API)的工具。你无需通过AWS控制台逐一创建各种资源,只需在TypeScript文件中描述所需的内容,CDK就会为你完成相应的构建工作。

请同时安装以下两个工具:

# 在macOS上安装AWS CLI
curl "https://awscli.amazonaws.com/AWSCLIV2.pkg" -o "AWSCLIV2.pkg"
sudo installer -pkg AWSCLIV2.pkg -target /

# 对于Linux系统,请参考:https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-linux.html
# 对于Windows系统,请参考:https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2-windows.html

# 全局安装AWS CDK
npm install -g aws-cdk

请确认这两个工具已经成功安装:

aws --version
cdk --version

如果这两个命令都能显示出相应的版本号,那就说明你已经准备好了继续下一步操作。

1.3 配置你的AWS凭证(IAM账户)

这一步非常重要。你的终端需要一组凭证——比如用户名和密码——才能代表你在AWS中执行各种操作。

可以把你的根账户(也就是你注册时使用的账户)看作是整个AWS账户的“主钥匙”。但在日常开发工作中,绝对不应该使用根账户进行操作。相反,你应该创建一个独立的IAM用户,并为其配置单独的凭证。如果这些凭证被泄露了,你可以直接删除它们,而不会影响到根账户的安全。

步骤1:创建一个IAM用户

  1. 登录AWS控制台,在顶部的搜索栏中输入“IAM”进行搜索

  2. 在左侧的侧边栏中,点击“Users”,然后选择“Create user”

  3. 将用户名称设置为cdk-dev。不要勾选“Provide user access to the AWS Management Console”——你只需要终端访问权限,不需要控制台访问权限

  4. 在权限设置页面中,选择“Attach policies directly”选项

IAM控制台中的“Attach policies directly”页面,其中已勾选AdministratorAccess选项

  1. 搜索“AdministratorAccess”选项,并将其旁边的复选框选中

关于权限设置需要注意的一点是:在正式的生产环境中,你会使用限制更严格的策略。但对于本教程来说,由于CDK需要创建多种类型的AWS资源,因此必须启用Administrator访问权限。

6. 点击页面底部的“Create user”按钮完成操作

步骤2:生成访问密钥

  1. 从“用户”列表中点击你新创建的cdk-dev账户。

  2. 进入“安全凭证”选项卡。

  3. 向下滚动到“访问密钥”部分,然后点击“创建访问密钥”。

  4. 选择“命令行接口(CLI)”,勾选确认框,然后点击“下一步”。

  5. 最后再次点击“创建访问密钥”。

重要提示:现在就将访问密钥ID和秘密访问密钥都复制下来。关闭此页面后,您将再也无法看到秘密访问密钥了。请将这两个值保存在密码管理工具或安全笔记中。

IAM控制台显示的创建访问密钥界面,其中包含访问密钥ID和秘密访问密钥

第三步:将终端连接到AWS

在您的终端中运行以下命令:

aws configure

系统会要求您输入四个值:

AWS访问密钥ID:     [请输入您的访问密钥ID]
AWS秘密访问密钥: [请输入您的秘密访问密钥]
默认区域名称:   us-east-1
默认输出格式: json

在本教程中,请使用us-east-1作为区域。完成这一步后,您后续运行的所有CDK和AWS CLI命令都会自动使用这些凭据。

第二部分:设置项目结构

您将采用单仓库结构——一个顶层文件夹中包含两个子项目:frontend用于存放React应用程序代码,backend用于存放AWS基础设施相关代码。这两个项目会独立部署,但会共存于同一个系统中。

2.1 创建工作区

mkdir vendor-tracker && cd vendor-tracker
mkdir backend frontend

2.2 初始化前端项目(Next.js)

进入frontend文件夹,然后运行以下命令:

cd frontend
npx create-next-app@latest .

按照提示选择相应的选项:

  • TypeScript –&gt> 是

  • ESLint –&gt> 是

  • Tailwind CSS –&gt> 是

  • src/目录 –&gt> 否

  • 应用路由器 –&gt> 是

  • 导入别名 –&gt> 否

2.3 初始化后端项目(CDK)

进入backend文件夹,然后运行以下命令:

cd ../backend
cdk init app --language typescript

这样就会生成一个基本的CDK项目。其中最重要的文件是backend/lib/backend-stack.ts,您需要在这个文件中用TypeScript代码定义所有的AWS基础设施相关配置。

另外,请安装esbuild,因为CDK会使用它来打包您的Lambda函数:

npm install --save-dev esbuild

2.4 在编写代码之前了解CDK的工作原理

CDK可能与您以前使用过的大多数工具有所不同。它的运作方式如下:

通常情况下,您需要通过AWS控制台来创建各种资源:在这里创建表格,在那里配置Lambda函数。而CDK允许您使用TypeScript代码来完成这些操作。

当你运行 `cdk deploy` 时,CDK会读取你的 TypeScript 文件,将其转换为 AWS CloudFormation 模板(这是一种用于描述基础设施的 AWS 内部格式),然后提交给 AWS。AWS 会根据这些模板创建你所定义的所有资源。

在本教程中,你会遇到一些术语:

  • Stack:指你定义的所有 AWS 资源的集合。你的 `BackendStack` 类就代表这样一个 Stack。

  • Construct:在 Stack 中创建的每一个单独的 AWS 资源(例如表格、Lambda 函数或 API)都被称为 Construct。

  • Deploy:运行 `cdk deploy` 会将你的 TypeScript 定义发送给 AWS,从而创建或更新相应的实际资源。

你主要需要操作的文件是 `backend/lib/backend-stack.ts`。可以把它看作是你整个后端系统的蓝图。

最终的项目结构会如下所示:

vendor-tracker/
├── backend/
│   ├── lambda/
│   │   ├── createVendor.ts
│   │   ├── getVendors.ts
│   │   └── deleteVendor.ts
│   ├── lib/
│   │   └── backend-stack.ts
│   └── package.json
└── frontend/
    ├── app/
    │   ├── layout.tsx
    │   ├── page.tsx
    │   └── providers.tsx
    ├── lib/
    │   └── api.ts
    ├── types/
    │   └── vendor.ts
    └── .env.local

第三部分:定义数据库(DynamoDB)

DynamoDB 是 AWS 提供的 NoSQL 数据库。它可以被看作是一种快速且可扩展的云中键值存储系统。在 DynamoDB 表中,每一条记录都必须拥有一个唯一的标识符,这个标识符被称为 分区键。对于你的供应商信息表来说,这个分区键就是 `vendorId`。

打开文件 `backend/lib/backend-stack.ts`,并将其中的全部内容替换为以下代码:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. DynamoDB 表
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: {
        name: 'vendorId',
        type: dynamodb.AttributeType.STRING,
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY, // 仅适用于开发环境
    });
  }
}

每行代码的作用:

  • partitionKey 指定了 `vendorId` 是每条记录的唯一标识符。没有任何两个供应商可以拥有相同的 vendorId

  • PAY_PER_REQUEST 表示只有当数据真正被读取或写入时,才会产生费用。如果表格处于空闲状态,则不会产生任何费用,因此这种配置非常适合用于学习用途。

  • RemovalPolicy.DESTROY 意味着当你运行 `cdk destroy` 时,该表格将会被删除。在实际的生产环境中,不建议使用这种设置。

第4部分:编写Lambda函数

Lambda函数其实就是你的服务器,但与传统服务器不同,它只有在被调用时才会运行。AWS会根据需求启动它,运行你的代码,然后将其关闭。你只需要为实际运行代码的时间支付费用。

你需要编写三个Lambda函数:

  • createVendor.ts:向DynamoDB中添加新的供应商信息

  • getVendors.ts:从DynamoDB中检索所有供应商信息

  • deleteVendor.ts:根据ID从DynamoDB中删除供应商信息

backend目录下创建一个新文件夹:

mkdir backend/lambda

6330a84b-77c3-4001-9783-5fedc89ae1c0

关于AWS SDK的说明

这三个Lambda函数都使用了AWS SDK v3@aws-sdk/client-dynamodb@aws-sdk/lib-dynamodb)。这是当前的标准版本。虽然也存在较旧版本的SDK(aws-sdk),但它已经被弃用,也不会被包含在Node.js 18的Lambda运行环境中,而你使用的正是这个版本。因此,请在整个开发过程中始终使用v3版本。

4.1 创建供应商信息处理Lambda函数

创建backend/lambda/createVendor.ts文件:

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, PutCommand } from "@aws-sdk/lib-dynamodb";
import { randomUUID } from "crypto";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async (event: any) => {
  try {
    const body = JSON.parse(event.body);

    const item = {
      vendorId: randomUUID(), // 生成一个唯一且不会重复的ID
      name: body.name,
      category: body.category,
      contactEmail: body.contactEmail,
      createdAt: new Date().toISOString(),
    };

    await docClient.send(
      new PutCommand({
        TableName: process.env.TABLE_NAME!, // 从环境变量中读取表名
        Item: item,
      })
    );

    return {
      statusCode: 201,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
      },
      body: JSON.stringify({ message: "供应商信息已创建", vendorId: item.vendorId }),
    };
  } catch (error) {
    console.error("创建供应商信息时出现错误:", error);
    return {
      statusCode: 500,
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ error: "无法创建供应商信息" }),
    };
  }
};

各部分的功能说明:

  • randomUUID()使用Node.js内置的crypto模块生成一个全球唯一的ID。这个方法不需要额外安装任何包,而且比Date.now()更可靠,因为后者在同一毫秒内收到两个请求时可能会生成重复的ID。

  • process.env.table_NAME从环境变量中读取DynamoDB表的名称。你可以在CDK堆栈中设置这个值,这样就可以避免在Lambda代码中硬编码表名了。

  • headers块对于实现CORS(跨源资源共享)是必需的。如果没有Access-Control-Allow-Origin头,浏览器会拒绝来自不同域名的响应;如果没有Access-Control-Allow-Headers头,后来为Cognito添加的Authorization头也会在浏览器的预检阶段被拒绝。

4.2 获取供应商信息的相关Lambda函数

创建文件backend/lambda/getVendors.ts

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, ScanCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async () => {
  try {
    const response = await docClient.send(
      new ScanCommand({
        TableName: process.env.TABLE_NAME!,
      })
    );

    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Content-Type": "application/json",
      },
      body: JSON.stringify(response.Items ?? []);
    };
  } catch (error) {
    console.error("获取供应商信息时出现错误:", error);
    return {
      statusCode: 500,
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ error: "无法获取供应商信息" }),
    };
  }
};

各部分的功能:

  • ScanCommand用于读取表中的所有数据,并将它们以数组的形式返回。对于一个学习项目来说,这种做法是可以接受的;但在一个包含数百万条记录的生产环境中,应该使用更加精确的QueryCommand来避免在每次请求时都读取整个表格。

  • response.Items ?? []如果表中没有任何数据,就会返回一个空数组,这样前端在遇到这种情况时就不会出现错误。

4.3 删除供应商信息的相关Lambda函数

创建文件backend/lambda/deleteVendor.ts

import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
import { DynamoDBDocumentClient, DeleteCommand } from "@aws-sdk/lib-dynamodb";

const client = new DynamoDBClient({});
const docClient = DynamoDBDocumentClient.from(client);

export const handler = async (event: any) => {
  try {
    const body = JSON.parse(event.body);
    const { vendorId } = body;

    if (!vendorId) {
      return {
        statusCode: 400,
        headers: { "Access-Control-Allow-Origin": "*" },
        body: JSON.stringify({ error: "需要提供vendorId" }),
      };
    }

    await docClient.send(
      new DeleteCommand({
        TableName: process.env.TABLE_NAME!,
        Key: { vendorId },
      })
    );

    return {
      statusCode: 200,
      headers: {
        "Access-Control-Allow-Origin": "*",
        "Access-Control-Allow-Headers": "Content-Type,Authorization",
        "Access-Control-Allow-Methods": "OPTIONS,POST,GET,DELETE",
      },
      body: JSON.stringify({ message: "供应商信息已成功删除" }),
    };
  } catch (error) {
    console.error("删除供应商信息时出现错误:", error);
    return {
      statusCode: 500,
      headers: { "Access-Control-Allow-Origin": "*" },
      body: JSON.stringify({ error: "无法删除供应商信息" }),
    };
  }
};

各部分的功能:

  • DeleteCommand用于删除那些vendorId与您提供的值相匹配的条目。如果该条目不存在,DynamoDB不会返回错误信息,而会直接忽略这一操作。

  • 在代码的开头添加了400错误处理机制,这样当调用者忘记提供vendorId时,系统会直接返回明确的错误信息,而不会让DynamoDB抛出令人困惑的内部错误。

第5部分:使用API Gateway构建API

API Gateway为您的Lambda函数提供了公共访问地址。如果没有它,浏览器就无法触发Lambda函数。可以把API Gateway看作是后端的“前门”:它接收HTTP请求,检查调用者是否具有访问权限,然后将请求路由到相应的Lambda函数,并将函数的响应返回给调用者。

现在您需要在backend/lib/background-stack.ts文件中将所有这些组件连接起来。

5.1 将Lambda函数和API Gateway添加到后台架构中

请用以下完整的代码替换backend/lib/background-stack.ts文件中的所有内容:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // 1. DynamoDB表
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: {
        name: 'vendorId',
        type: dynamodb.AttributeType.STRING,
      },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // 2. Lambda函数
    const lambdaEnv = { TABLE_NAME: vendorTable.tableName };

    const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
      entry: 'lambda/createVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
      entry: 'lambda/getVendors.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
      entry: 'lambda/deleteVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    // 3. 权限设置(最小权限原则)
    vendorTable.grantWriteData(createVendorLambda);
    vendorTable.grantReadData(getVendorsLambda);
    vendorTable.grantWriteData(deleteVendorLambda);

    // 4. API Gateway
    const api = new apigateway.RestApi(this, 'VendorApi', {
      restApiName: 'Vendor Service',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    const vendors = api.root.addResource('vendors');
    vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda));
    vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda));
    vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda));

    // 5. 输出结果
    new cdk.CfnOutput(this, 'ApiEndpoint', {
      value: api.url,
    });
  }
}

各部分的功能:

NodejsFunction是一种特殊的CDK构建工具,它会使用esbuild自动将你的Lambda代码及其所有依赖项打包成一个文件,然后再将其上传到AWS。这就是为什么在第二部分中你需要安装esbuild的原因。

请始终使用NodejsFunction,而不是基本的lambda.Function构造。基本版本需要你手动进行代码打包操作,这样在运行时就容易出现“模块未找到”的错误。

权限设置(最小权限原则):在AWS中,默认情况下没有任何资源能够与其他资源进行交互。除非你明确授予权限,否则Lambda函数无法访问DynamoDB、S3或其他任何服务。

这就是所谓的“最小权限原则”:系统中的每个组件只被赋予它所需要的权限,而不会获得额外的权限。grantWriteData允许Lambda函数写入和删除数据,grantReadData则允许Lambda函数读取数据。为每个函数分别设置权限,就能确保getVendors Lambda函数永远不会意外地删除数据。

CfnOutput会在cdk deploy命令执行完成后在终端中显示相应的结果。你可以使用ApiEndpoint这个URL来配置前端应用。

第6部分:将后端部署到AWS

你的基础设施已经完全通过代码定义好了,现在你将把它们部署到AWS上,并获得一个可使用的API地址。

6.1 初始化你的AWS环境

在首次使用CDK进行部署之前,AWS需要在你的账户中创建一个用于存储Lambda代码包及其他资源的S3桶。这个设置步骤被称为“初始化环境”,每个AWS账户在每个区域都只需要执行一次。

backend文件夹内运行以下命令:

cdk bootstrap

重要提示:初始化环境操作是针对特定区域进行的。如果你更换了AWS区域,就需要在该新区域内再次执行cdk bootstrap命令。

6.2 进行部署

运行以下命令:

cdk deploy

CDK会显示即将创建的所有资源的详细信息,并请求你确认是否继续。输入y并按回车键即可。

部署完成后,你会在终端中看到 Outputs部分,其中会包含API地址:

Outputs:
BackendStackApiEndpoint = https://abcdef123.execute-api.us-east-1.amazonaws.com/prod/

请将这个URL保存下来,后续构建前端应用时会需要用到它。

6.3 故障排除:如何查看AWS错误日志

在实际部署过程中,很少会有事情一次就能顺利完成。如果部署后出现了问题,以下方法可以帮助你找到具体的错误信息。

错误代码:502 Bad Gateway

502错误表示API网关接收到了你的请求,但Lambda函数在响应之前就已经崩溃了。最常见的原因就是缺少某个环境变量——例如,如果TABLE_NAME这个参数传入的值不正确,Lambda函数就无法找到相应的表。

要找到实际的错误信息,请使用CloudWatch Logs

  1. 登录AWS控制台,然后搜索CloudWatch。

  2. 在左侧边栏中,点击“Logs” -> “Log groups”。

CloudWatch左侧边栏,其中显示了log groups以及搜索字段,搜索内容为/aws/lambda/

  1. 找到名为/aws/lambda/BackendStack-CreateVendorHandler...的日志组。

  2. 点击最新的日志流。

  3. 阅读错误信息,它会清楚地告诉你出了什么问题。

以下是两种常见的错误信息及其解决方法:

  • Runtime.ImportModuleError:你的Lambda函数找不到某个模块。请确保在CDK堆栈中使用了NodejsFunction(而不是lambda.Function)。NodejsFunction会自动打包依赖项;而lambdaFUNCTION则不会。

  • AccessDeniedException:你的Lambda函数试图访问DynamoDB,但没有相应的权限。请检查你的堆栈中是否为该Lambda函数配置了正确的grantWriteDatagrantReadData权限。

第7部分:构建React前端界面

你的后端服务已经上线了。现在你需要构建与它交互的React用户界面。

7.1 定义“供应商”类型

在编写任何API或组件代码之前,首先需要在TypeScript中定义“供应商”这个概念。这样可以在整个前端代码中确保类型安全。

创建文件frontend/types/vendor.ts

export interface Vendor {
  vendorId?: string; // 创建时可选——Lambda函数会自动生成这个ID
  name: string;
  category: string;
  contactEmail: string;
  createdAt?: string;
}

之所以将vendorId?标记为可选,是因为在创建新的“供应商”对象时,此时还没有ID;而createVendor Lambda函数会生成这个ID。当通过API读取“供应商”信息时,vendorId字段一定是存在的。

7.2 创建API服务层

不要在React组件中直接编写fetch调用语句,而是将所有的API逻辑集中放在一个文件中。这种设计模式被称为服务层。这样做可以让组件结构更加清晰,并且便于统一修改所有API调用相关的代码。

首先,在frontend文件夹下创建一个.env.local文件,用于存储API地址:

# frontend/.env.local
NEXT_PUBLIC_API_URL=https://abcdef123.execute-api.us-east-1.amazonaws.com/prod

请将其中的URL替换为cdk deploy命令输出结果中得到的ApiEndpoint值。Next.js要求在环境变量前加上NEXT_PUBLIC_前缀,这样才能让浏览器能够识别这个环境变量。

你可能会想:为什么不直接将URL硬编码到代码中呢?如果你将API地址直接粘贴到代码中并上传到GitHub,那么这个地址就会被公开。虽然单独的API地址并不会暴露你的数据(Cognito会对此进行保护),但将URL和敏感信息放在源代码控制之外仍然是良好的实践。请始终使用`.env.local`文件,并将其添加到`.gitignore`列表中。

确保`.env.local`文件在`.gitignore`列表中:

echo ".env.local" >> frontend/.gitignore

现在创建`frontend/lib/api.ts`文件:

import { Vendor } from '@/types/vendor';

const BASE_URL = process.env.NEXT_PUBLIC_API_URL!;

export const getVendors = async (): Promise => {
  const response = await fetch(`${BASE_URL}/vendors`);
  if (!response.ok) throw new Error('获取供应商信息失败');
  return response.json();
};

export const createVendor = async (vendor: Omit): Promise => {
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(vendor),
  });
  if (!response.ok) throw new Error('创建供应商信息失败');
};

export const deleteVendor = async (vendorId: string): Promise => {
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'DELETE',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ vendorId }),
  });
  if (!response.ok) throw new Error('删除供应商信息失败');
};

各部分的功能说明:

  • Omit表示createVendor函数可以接受没有ID或创建时间戳的供应商对象——这些信息是由服务器端生成的。

  • if (!response.ok) throw new Error(...)这一代码确保了任何HTTP错误(4xx或5xx码)都会以JavaScript错误的形式在组件中显示出来,这样你就可以向用户展示有意义的提示信息,而不会让程序默默地出现故障。

在第八部分中,你会对这些函数进行修改,以便在其中加入Cognito认证令牌。

7.3 构建主页面

现在来创建主页面组件。这个组件包含一个用于添加供应商信息的表单,以及一个用于显示当前所有供应商信息的列表。

将`frontend/app/page.tsx`文件的内容替换为以下内容:


      

供应商管理工具

你的供应商信息存储在AWS DynamoDB中。

{error && (
错误信息:{error}
)}
{/* ── 添加供应商表单 ── */}

添加新供应商

setForm({ ...form, name: e.target.value})} required /> setForm({ ...form, category: e.target.value})} required /> setForm({ ...form, contactEmail: e.target.value})} required />
{/* ── 供应商列表 ── */}

当前供应商列表

{vendors.length === 0 ? (

目前还没有供应商信息。请使用表单添加新的供应商。

) : ( vendors.map(v => (

名称:{v.name}

类别:{v.category} · 联系邮箱:{v.contactEmail}

))} )
); }

该组件的关键点:

  • 在代码顶部使用的 `'use client'` 是 Next.js 的一个指令。它告诉 Next.js,该组件使用了浏览器 API(`useState`、`useEffect` 以及事件处理程序),因此必须在浏览器中运行,而不能在服务器端进行预渲染。

  • 在 `handleSubmit` 函数中使用的 `e.preventDefault()` 可以阻止浏览器的默认表单提交行为,因为这种行为会导致页面完全重新加载,进而使 React 中的状态数据丢失。

  • 每次调用 `createVendor` 或 `deleteVendor` 方法后,都会再次执行 `loadVendors()` 函数。这个函数会从 DynamoDB 中重新获取最新数据,这样用户界面显示的内容就会与数据库中存储的实际数据保持一致。

7.4 在本地测试应用程序

启动你的 Next.js 开发服务器:

cd frontend
npm run dev

在浏览器中访问 http://localhost:3000,你应该会看到由两个面板组成的界面。试着添加一个供应商信息,确认它是否真的出现在列表中。

在 localhost:3000 上运行的 Supplier Tracker 应用程序,界面分为左右两栏:左侧是添加供应商信息的表单,右侧是空的供应商列表
在添加了一个供应商信息后,Supplier Tracker 应用程序的界面显示情况,列表中出现了该供应商的信息

验证与 AWS 的连接

打开 Chrome 开发工具(按 F12 键),然后点击“网络”选项卡。当你添加一个供应商信息时,应该会看到以下操作:

  • 向你的 AWS API 地址发送一个 POST 请求,请求会返回 201 状态码。

  • 还会收到一个 GET 请求,响应内容为更新后的供应商列表,状态码为 200

你也可以通过打开 AWS 控制台,然后导航到 DynamoDB –> Tables –> VendorTable –&gt> Explore table items 来验证数据是否已被成功保存。你的供应商信息应该会出现在那里。

第 8 部分:使用 Amazon Cognito 添加身份认证功能

目前,你的 API 是完全开放的,任何找到该 API 地址的人都可以添加或删除供应商信息。你可以使用 Amazon Cognito 来解决这个问题。

Cognito 是 AWS 提供的身份认证服务。它负责管理用户池——这个用户池是一个存储了注册用户的用户名和密码的数据库。当用户登录时,Cognito 会生成一个 JWT(JSON Web Token):这种经过加密处理的字符串能够证明用户的身份。你的 API Gateway 在处理每个请求时都会检查这个令牌;如果没有有效的令牌,用户就无法访问相关资源。

什么是 JWT? JSON Web Token 是一种格式类似于 eyJhbGci... 的字符串。它包含了关于用户的加密信息,并且由 Cognito 使用密钥进行了签名处理。

API Gateway能够在每次请求时都不需要与Cognito进行通信即可验证签名,因此令令牌校验过程变得非常快速。可以将它想象成一种防篡改的标识:任何人都能看到上面写着的名称,但只有Cognito生成的签名才能使该标识具有法律效力。

8.1 将Cognito添加到CDK堆栈中

打开backend/lib/backend-stack.ts文件,并对其进行修改以加入Cognito相关配置。以下是修改后的完整代码:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super(scope, id, props);

    // ─── 1. DynamoDB表 ────────────────────────────────────────────────────
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: { name: 'vendorId', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // ─── 2. Lambda函数 ──────────────────────────────────────────────────
    const lambdaEnv = { TABLE_NAME: vendorTable.tableName };

    const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
      entry: 'lambda/createVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
      entry: 'lambda/getVendors.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
      entry: 'lambda/deleteVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    // ─── 3. 权限设置 ───────────────────────────────────────────────────────
    vendorTable.grantWriteData(createVendorLambda);
    vendorTable.grantReadData(getVendorsLambda);
    vendorTable.grantWriteData(deleteVendorLambda);

    // ─── 4. Cognito用户池 ─────────────────────────────────────────────────
    const userPool = new cognito.UserPool(this, 'VendorUserPool', {
      selfSignUpEnabled: true,
      signInAliases: { email: true },
      autoVerify: { email: true },
      userVerification: {
        emailStyle: cognito.VerificationEmailStyleCODE,
      },
    });

    // 需要添加此配置以便托管Cognito的内部认证接口
    userPool.addDomain('VendorUserPoolDomain', {
      cognitoDomain: {
        domainPrefix: `vendor-tracker-${this.account}`,
      },
    });

    const userPoolClient = userPool.addClient('VendorAppClient');

    // ─── 5. API Gateway与授权器 ──────────────────────────────────────────
    const api = new apigateway.RestApi(this, 'VendorApi', {
      restApiName: 'Vendor Service',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
      this,
      'VendorAuthorizer',
      { cognitoUserPools: [userPool] }
    );

    const authOptions = {
      authorizer,
      authorizationType: apigateway AuthorizationType.COGNITO,
    };

    const vendors = api.root.addResource('vendors');
    vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda), authOptions);
    vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda), authOptions);
    vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda), authOptions);

    // ─── 6. 输出结果 ───────────────────────────────────────────────────
    new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
    new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
    new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
  }
}

在用户池列表中新创建的用户池(VendorUserPool...),其用户池ID清晰可见

发生了哪些变化:

  • CognitoUserPoolsAuthorizer会指示API Gateway在将任何请求传递给Lambda之前,先检查该请求是否包含有效的Cognito JWT令牌。如果令牌缺失或无效,API Gateway会立即以401 Unauthorized响应拒绝该请求,而根本不会触发你的Lambda函数。

  • authOptions这一配置会应用于GET、POST和DELETE这三种API方法,因此现在所有路由都受到了保护。

  • autoVerify: { email: true }这一设置会使得Cognito在用户通过验证码确认邮箱地址有效后,将该邮箱属性标记为已验证状态。不过系统并不会跳过发送验证邮件这一步骤,因为用户仍然会收到验证码。如果你在开发阶段希望跳过验证流程,可以在Cognito控制台中手动完成用户验证操作(具体方法详见第8.5节)。

  • 在下一次部署之后,你的终端界面将会显示两个新的CfnOutput参数值:UserPoolIdUserPoolClientId。你的前端应用程序需要这些信息才能与Cognito服务进行交互。

请部署更新后的代码栈:

cd backend
cdk deploy

部署完成后,终端输出中会包含以下三个值:

 Outputs:
BackendStackApiEndpoint     = https://abc123.execute-api.us-east-1.amazonaws.com/prod/
BackendStack.UserPoolId      = us-east-1_xxxxxxxx
BackendStack.UserPoolClientId = xxxxxxxxxxxxxxxxxxxx

请将这三个值保存下来,后续步骤会需要用到它们。

8.2 安装并配置AWS Amplify

AWS Amplify是一个前端开发工具库,它可以帮你处理所有复杂的认证逻辑:它负责管理登录界面、在用户的浏览器中存储令牌、自动更新过期的令牌,并提供简单的API接口来获取当前用户的会话信息。

请将Amplify相关库文件安装到你的frontend文件夹中:

cd frontend
npm install aws-amplify @aws-amplify/ui-react

创建文件frontend/appproviders.tsx,该文件会使用你的Cognito配置信息来初始化Amplify。应用程序加载时,这个文件会自动执行一次:

'use client';

import { Amplify } from 'aws-amplify';

Amplify.configure(
  {
    Auth: {
      Cognito: {
        userPoolId: process.env.NEXT_PUBLIC_USER_POOL_ID!,
        userPoolClientId: process.env.NEXT_public_USER_POOL_CLIENT_ID!,
      },
    },
  },
  { ssr: true }
);

export function Providers({ children }: { children: React.ReactNode }) {
  return <>{children}</>;
}

请将你的Cognito用户名和ID添加到frontend/.env.local文件中:

NEXT_PUBLIC_API_URL=https://abc123.execute-api.us-east-1.amazonaws.com/prod
NEXT_public_USER_POOL_ID=us-east-1_xxxxxxxx
NEXT_PUBLIC_USER_POOL_CLIENT_ID=xxxxxxxxxxxxxxxxxxxx

请将其中的值替换为执行`cdk deploy`命令后得到的结果。

8.3 将相关组件集成到应用布局中

这一步非常关键。在任何组件尝试使用认证功能之前,必须先初始化Amplify。如果跳过这一步,`fetchAuthSession()`方法会抛出“Amplify未配置”的错误,导致所有功能都无法正常使用。

打开`frontend/app/layout.tsx`文件,将其修改为用`Providers`组件包裹整个应用结构:

import type { Metadata } from 'next';
import './globals.css';
import { Providers } from './providers';

export const metadata: Metadata = {
title: '供应商管理工具',
description: '使用AWS来管理您的供应商信息。',
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (


{children}


);
}

通过用``组件包裹`{children}`,可以确保在应用程序的任何子页面或组件渲染之前,Amplify就已经被正确配置好了。

8.4 使用withAuthenticator保护用户界面

现在需要对`Home`组件进行相应的处理,这样未登录的用户看到的将会是登录页面,而不是控制面板。

将`frontend/app/page.tsx`文件的内容替换为以下更新后的版本:

'use client';

import { useState, useEffect } from 'react';
import { withAuthenticator } from '@aws-amplify/ui-react';
import '@aws-amplify/ui-react/styles.css';
import { getVendors, createVendor, deleteVendor } from '@/lib/api';
import { Vendor } from '@/types/vendor';

// withAuthenticator会自动将`signOut`和`user`作为属性注入到函数中
function Home({ signOut, user }: { signOut?: () => void; user?: any }) {
const [vendors, setVendors] = useState:>>;
const [form, setForm] = useState({ name: '', category: '', contactEmail: '' });
const [loading, setLoading] = useState(false);
const [error, setError] = useState('');

const loadVendors = async () => {
try {
const data = await getVendors();
setVendors(data);
} catch {
setError('加载供应商信息失败。');
}
};

useEffect(() => {
loadVendors();
}, []);

const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault();
setLoading(true);
setError('');
try {
await createVendor(form);
setForm({ name: '', category: '', contactEmail: '' });
await loadVendors();
} catch {
setError('添加供应商信息失败。');
} finally {
setLoading(false);
}
};

const handleDelete = async (vendorId: string) => {
try {
await deleteVendor(vendorId);
await loadVendors();
} catch {
setError('删除供应商信息失败。');
}
};

return (


{/* ── 头部导航 ── */}

供应商管理工具

当前登录用户:{user?.signInDetails?.loginId}


{error && (

出现错误:{error}

)}

{/* ── 添加供应商表单 ── */}

添加新供应商





{/* ── 供应商列表 ── */}

当前供应商数量:{vendors.length}

{vendors.length === 0 ? (

目前还没有供应商信息。

) : (
vendors.map(v => (

供应商名称:{v.name}

类别:{v.category} · 联系邮箱:{v.contactEmail}

))}
)}


);
}

// 使用withAuthenticator包装Home组件后,未登录的用户将会看到Amplify提供的登录/注册界面,
// 而不是这个自定义的首页组件。
export default withAuthenticator(Home);
由Amplify生成的登录页面

8.5 在API调用中传递认证令牌

由于API Gateway现在要求每个请求都必须包含JWT令牌,因此你的fetch调用需要在Authorization头部中添加该令牌。如果没有这个令牌,所有请求都会返回401 Unauthorized错误。

请更新frontend/lib/api.ts文件,在其中加入用于处理令牌的辅助函数,并修改相关的fetch调用代码:

import { fetchAuthSession } from 'aws-amplify/auth';
import { Vendor } from '@/types/vendor';

const BASE_URL = process.env.NEXT_PUBLIC_API_URL!;

// 从当前激活的Amplify会话中获取用户的JWT令牌
const getAuthToken = async (): Promise => {
  const session = await fetchAuthSession();
  const token = session.tokens?.idToken?.toString();
  if (!token) throw new Error('没有激活的会话。请先登录。');
  return token;
};

export const getVendors = async(): Promise => {
  const token = await getAuthToken();
  const response = await fetch(`${BASE_URL}/vendors`, {
    headers: { Authorization: token },
  });
  if (!response.ok) throw new Error('获取供应商信息失败');
  return response.json();
};

export const createVendor = async (
  vendor: Omit>
): Promise => {
  const token = await getAuthToken();
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      Authorization: token,
    },
    body: JSON.stringify(vendor),
  });
  if (!response.ok) throw new Error('创建供应商信息失败');
};

export const deleteVendor = async (vendorId: string): Promise => {
  const token = await getAuthToken();
  const response = await fetch(`${BASE_URL}/vendors`, {
    method: 'DELETE',
    headers: {
      'Content-Type': 'application/json',
      Authorization: token,
    },
    body: JSON.stringify({ vendorId }),
  });
  if (!response.ok) throw new Error('删除供应商信息失败');
};

getAuthToken的作用是:

fetchAuthSession()会从浏览器中读取当前登录用户的会话信息。在用户登录后,Amplify会将该会话信息存储在内存以及localStorage中。

session.tokens?.idToken就是API Gateway的Cognito授权系统所需要的数据。将这个令牌作为Authorization头部发送出去,就可以告诉API Gateway:“这个请求是由已认证的用户发出的。”

8.6 解决Cognito相关问题

注册后出现“未确认”用户状态的问题

当新用户通过Amplify UI进行注册时,Cognito会将该账户标记为未确认状态,直到用户验证自己的电子邮件地址。系统会向用户的邮箱发送验证码,用户输入验证码后,账户状态才会变为已确认,此时用户就可以登录了。

如果您在本地进行测试,并希望跳过电子邮件验证步骤,可以在AWS控制台中手动确认相关账户的状态:

  1. 打开AWS控制台,然后导航到Cognito页面

  2. 点击您的用户池(例如:VendorUserPool...

  3. 选择“Users”选项卡

  4. 点击用户的电子邮件地址

  5. 打开下拉菜单,然后选择“Confirm account”

Cognito用户列表中显示状态为‘未确认’的用户” height=
Cognito用户列表中显示状态为‘未确认’的用户” height=

部署后出现“401未经授权”错误

如果遇到了401错误,请检查以下两点:

  1. 打开Chrome开发者工具,进入“网络”选项卡,点击出现错误的请求,查看请求头信息。你应该能看到一个包含长字符串的Authorization头部字段。如果这个字段缺失,那么getAuthToken函数就会失败。请确认在providers.tsx文件中Amplify的配置是否正确,并且通过layout.tsx文件将其正确链接起来。

  2. 在您的CDK代码栈中,确保每个受保护的接口定义中都包含了authorizationType: apigatewayAuthorizationType.COGNITO这一项。如果缺少这个配置,即使已经定义了授权机制,API Gateway也可能不会检查令牌。

第9部分:使用S3和CloudFront部署前端应用

您的应用程序在本地环境下可以正常运行。现在,您要将它部署到一个真正的HTTPS地址上,这样世界各地的任何人都可以访问这个应用。

部署策略:Next.js会将您的React应用程序生成为一组静态的HTML、CSS和JavaScript文件,这些文件会被上传到S3存储桶中(AWS提供的文件存储服务)。而CloudFront则作为内容分发网络,将这些文件分发到全球各地的服务器上,并通过HTTPS协议提供访问服务。

9.1 配置Next.js以进行静态资源导出

打开frontend/next.config.js(或next.config.mjs)文件,在其中添加output: 'export'这一配置项:

/** @type {import('next').NextConfig} */
const nextConfig = {
  output: 'export', // 这种配置会生成静态的/out文件夹,而不是运行Node.js服务器
};

export default nextConfig;

关于“use client”配置与静态资源导出的关系:当设置output: 'export'时,Next.js会在编译阶段生成所有页面的静态文件。对于那些使用了仅适用于浏览器的API的组件(比如Amplify提供的withAuthenticator组件),必须在文件顶部添加use client指令。这样Next.js就会跳过对这些组件的服务器端渲染步骤,仅在浏览器环境中执行它们。

你已经在`page.tsx`文件中添加了`use client`这一代码。如果在未来遇到构建错误,提示“window未定义”之类的问题,请检查相关组件是否在文件顶部包含了`use client`这句话。

现在来构建前端代码:

cd frontend
npm run build

这样会生成一个名为`/out`的文件夹,其中包含你的整个网站的静态文件。请确认这个文件夹确实已经创建好了:

ls out
# 你应该能看到:index.html、_next/等文件。

9.2 将S3和CloudFront添加到CDK堆栈中

打开`backend/lib/backend-stack.ts`文件,然后添加相关的托管配置。以下是该文件的最终版本:

import * as cdk from 'aws-cdk-lib';
import { Construct } from 'constructs';
import * as dynamodb from 'aws-cdk-lib/aws-dynamodb';
import * as apigateway from 'aws-cdk-lib/aws-apigateway';
import * as cognito from 'aws-cdk-lib/aws-cognito';
import * as s3 from 'aws-cdk-lib/aws-s3';
import * as cloudfront from 'aws-cdk-lib/aws-cloudfront';
import * as origins from 'aws-cdk-lib/aws-cloudfront-origins';
import * as s3deploy from 'aws-cdk-lib/aws-s3-deployment';
import { NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs';

export class BackendStack extends cdk.Stack {
  constructor(scope: Construct, id: string, props?: cdk.StackProps) {
    super_scope, id, props);

    // 1. DynamoDB表
    const vendorTable = new dynamodb.Table(this, 'VendorTable', {
      partitionKey: { name: 'vendorId', type: dynamodb.AttributeType.STRING },
      billingMode: dynamodb.BillingMode.PAY_PER_REQUEST,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
    });

    // 2. Lambda函数
    const lambdaEnv = { TABLE_NAME: vendorTable.tableName };

    const createVendorLambda = new NodejsFunction(this, 'CreateVendorHandler', {
      entry: 'lambda/createVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const getVendorsLambda = new NodejsFunction(this, 'GetVendorsHandler', {
      entry: 'lambda/getVendors.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    const deleteVendorLambda = new NodejsFunction(this, 'DeleteVendorHandler', {
      entry: 'lambda/deleteVendor.ts',
      handler: 'handler',
      environment: lambdaEnv,
    });

    // 3. 权限设置
    vendorTable.grantWriteData(createVendorLambda);
    vendorTable.grantReadData(getVendorsLambda);
    vendorTable.grantWriteData(deleteVendorLambda);

    // 4. Cognito用户池
    const userPool = new cognito.UserPool(this, 'VendorUserPool', {
      selfSignUpEnabled: true,
      signInAliases: { email: true },
      autoVerify: { email: true },
      userVerification: {
        emailStyle: cognito.VerificationEmailStyle.CODE,
      },
    });

    userPool.addDomain('VendorUserPoolDomain', {
      cognitoDomain: { domainPrefix: `vendor-tracker-${this.account}` },
    });

    const userPoolClient = userPool.addClient('VendorAppClient');

    // 5. API网关与授权器
    const api = new apigateway.RestApi(this, 'VendorApi', {
      restApiName: 'Vendor Service',
      defaultCorsPreflightOptions: {
        allowOrigins: apigateway.Cors.ALL_ORIGINS,
        allowMethods: apigateway.Cors.ALL_METHODS,
        allowHeaders: ['Content-Type', 'Authorization'],
      },
    });

    const authorizer = new apigateway.CognitoUserPoolsAuthorizer(
      this,
      'VendorAuthorizer',
      { cognitoUserPools: [userPool] }
    );

    const authOptions = {
      authorizer,
      authorizationType: apigateway AuthorizationType.COGNITO,
    };

    const vendors = api.root.addResource('vendors');
    vendors.addMethod('GET', new apigateway.LambdaIntegration(getVendorsLambda), authOptions);
    vendors.addMethod('POST', new apigateway.LambdaIntegration(createVendorLambda), authOptions);
    vendors.addMethod('DELETE', new apigateway.LambdaIntegration(deleteVendorLambda), authOptions);

    // 6. S3存储桶(用于存放前端文件)
    const siteBucket = new s3.Bucket(this, 'VendorSiteBucket', {
      blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL,
      removalPolicy: cdk.RemovalPolicy.DESTROY,
      autoDeleteObjects: true,
    });

    // 7. CloudFront分发服务(提供HTTPS支持及内容分发)
    const distribution = new cloudfront.Distribution(this, 'SiteDistribution', {
      defaultBehavior: {
        origin: new origins.S3Origin(siteBucket),
        viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS,
      },
      defaultRootObject: 'index.html',
      errorResponses: [
        {
          // 将所有404错误页面重定向到index.html,以便React能够处理路由逻辑
          httpStatus: 404,
          responseHttpStatus: 200,
          responsePagePath: '/index.html',
        },
      ],
    });

    // 8. 将前端文件部署到S3存储桶
    new s3deploy.BucketDeployment(this, 'DeployWebsite', {
      sources: [s3deploy.Source.asset('../frontend/out')]
      », destinationBucket: siteBucket,
      distribution,
      distributionPaths: ['/*'], // 每次部署时都会清除CloudFront缓存
    });

    // 9. 输出结果
    new cdk.CfnOutput(this, 'ApiEndpoint', { value: api.url });
    new cdk.CfnOutput(this, 'UserPoolId', { value: userPool.userPoolId });
    new cdk.CfnOutput(this, 'UserPoolClientId', { value: userPoolClient.userPoolClientId });
    new cdk.CfnOutput(this, 'CloudFrontURL', {
      value: `https://${distribution.distributionDomainName}`,
    });
  }
}

托管基础设施的作用:

  • S3存储桶用于存放您的静态HTML、CSS和JavaScript文件。这些文件是私有的,用户无法直接访问它们。

  • CloudFront是一种内容分发网络服务,它位于S3之前。它会为您提供一个HTTPS地址,并将您的文件缓存到全球各地的边缘节点上,因此无论用户身处何地,应用程序都能快速加载。REDIRECT_TO_HTTPS功能会自动将所有HTTP请求转换为HTTPS请求。

  • 当发生404错误时,错误响应会返回index.html文件,而不是错误页面。这对于单页应用程序来说非常重要:如果用户直接访问像/vendors/123这样的路径,CloudFront找不到相应的文件,但返回index.html后,React应用程序就能正确处理路由请求。

  • distributionPaths: ['/*']这一设置会告诉CloudFront在每次部署后清除其所有的缓存数据,这样用户就能立即看到您应用程序的最新版本。

  • BucketDeployment是AWS CDK提供的一个功能,每当您运行cdk deploy命令时,它都会自动将frontend/out文件夹中的文件上传到S3存储桶中。

9.3 执行最终部署

首先,使用最新的环境变量构建前端代码:

cd frontend
npm run build

然后,从后端文件夹中部署所有内容:

cd ../backend
cdk deploy

部署完成后,从终端输出中复制CloudFrontURL地址:

 Outputs:
BackendStack.CloudFrontURL = https://d1234abcd.cloudfront.net

在浏览器中打开这个地址,您的应用程序就已经正式上线了——它通过HTTPS协议提供服务,并且已经实现了全球范围内的分布部署。

f8e14979-a667-4afc-bdd4-9afe4abd9593

您所构建的应用程序

现在,您已经拥有一个完整部署好的、具备生产环境级别的全栈应用程序。以下是您所构建的各个组件的功能概述:

层级 服务 功能
前端 Next.js + CloudFront 通过HTTPS在全球范围内提供React用户界面
认证 Amazon Cognito + Amplify 处理用户注册、登录以及JWT令牌的管理
API接口 API Gateway 路由处理HTTP请求并验证认证令牌
逻辑处理 AWS Lambda (×3) 根据需求创建、读取或删除供应商相关信息
数据库 DynamoDB 无额外开销地存储供应商信息
存储服务 S3 保存编译后的前端文件
基础设施 AWS CDK 以代码形式定义并部署上述所有组件

结论

你已经构建并部署了几乎所有云应用所依赖的基础架构模式:一个由数据库支持的安全API,并通过“基础设施即代码”的方式进行部署。你所完成的工作包括以下几点:

你搭建了一个专业的AWS开发环境,并配置了相应的IAM权限设置。你使用AWS CDK将整个后端基础设施定义成了TypeScript代码,这意味着你的数据库、API、Lambda函数以及认证系统都处于版本控制之下,而且可以通过一个命令即可完成部署。

你编写了三个Lambda函数,分别用于处理创建、读取和删除操作,每个函数都包含了适当的错误处理机制,并遵循了AWS SDK v3的标准架构。这些Lambda函数通过API Gateway与REST API相连,而所有的接口都使用了Amazon Cognito进行认证,因此只有已注册且经过验证的用户才能访问你的数据。

在前端层面,你使用Next.js构建了一个应用程序。该应用程序的服务层将API逻辑与UI组件清晰地分离开来,并通过AWS Amplify自动管理JWT令牌的生命周期。这样一来,用户无需编写任何与认证相关的UI代码,就能完成注册和登录流程。

最后,你将整个系统进行了部署:后端部分被部署到了AWS Lambda和DynamoDB上,而前端则作为静态网站通过CloudFront在全球范围内提供HTTPS服务。

本教程的完整源代码可以在GitHub上找到。你可以克隆这些代码,根据需要进行修改,然后将其作为自己项目的参考资料使用。

Comments are closed.