返回文章列表

無伺服器架構 API 建置與 AWS 實作

本文探討無伺服器架構的核心概念,並使用 AWS Lambda、API Gateway 和 DynamoDB,搭配 AWS CDK 以 TypeScript 建構一個具備完整 CRUD 功能的無伺服器 RESTful API。文章涵蓋 API 設計、Lambda 函式開發、DynamoDB 資料函式庫整合、以及

Web 開發 雲端運算

無伺服器架構讓開發者擺脫伺服器管理的負擔,專注於程式碼的開發與佈署。本文將以 AWS 平台為例,示範如何結合 API Gateway、Lambda 和 DynamoDB,建構一個具備健康檢查、資料讀取和新增功能的無伺服器 API。透過 AWS CDK,我們可以簡化基礎設施的組態和管理,並使用 TypeScript 撰寫程式碼,提升開發效率。文章將逐步引導讀者建立 Lambda 函式、設定 API Gateway 路由、整合 DynamoDB 資料函式庫,並說明 CORS 設定的必要性,最後完成一個可實際運作的無伺服器應用程式。

無伺服器架構(Serverless)解析與 AWS 實作

前言

無伺服器架構(Serverless)是一種雲端運算模式,讓開發者能夠在無需管理伺服器的情況下建置和執行應用程式。本文將探討無伺服器架構的核心概念,並透過 AWS Lambda 和 API Gateway 實作一個無伺服器的 API。

什麼是無伺服器架構?

無伺服器架構是一種事件驅動的運算模式,開發者只需撰寫和佈署程式碼,無需擔心底層基礎設施的管理。這種模式提供了更高的靈活性、可擴充套件性和成本效益。

使用 AWS Lambda 和 API Gateway 建立 API

步驟一:建立和設定 REST API

首先,我們需要使用 AWS API Gateway 建立一個 RESTful API。API Gateway 是一個受管服務,簡化了 API 的建立、發布、維護、監控和安全性的過程。

const backEndSubDomain = process.env.NODE_ENV === 'Production' ? config.backend_subdomain : config.backend_dev_subdomain;
const restApi = new RestApi(this, 'chapter-7-rest-api', {
  restApiName: `chapter-7-rest-api-${process.env.NODE_ENV || ''}`,
  description: 'serverless api using lambda functions',
  domainName: {
    certificate: acm.certificate,
    domainName: `${backEndSubDomain}.${config.domain_name}`,
    endpointType: EndpointType.REGIONAL,
    securityPolicy: SecurityPolicy.TLS_1_2,
  },
  deployOptions: {
    stageName: process.env.NODE_ENV === 'Production' ? 'prod' : 'dev',
  },
});

步驟二:建立健康檢查路徑

接下來,我們需要建立一個健康檢查路徑,以確保 API 在佈署後能夠正常運作。

const healthcheck = restApi.root.addResource('healthcheck');
const rootResource = restApi.root;
healthcheck.addMethod('GET', healthCheckLambdaIntegration);
healthcheck.addCorsPreflight({
  allowOrigins: ['*'],
  allowHeaders: ['*'],
  allowMethods: ['*'],
  statusCode: 204,
});

步驟三:整合 Lambda 函式

API Gateway 允許我們使用 Lambda 函式作為端點的處理程式。我們需要建立一個 Lambda 函式,並將其與 API 端點整合。

export class HealthCheckLambda extends Construct {
  public readonly func: NodejsFunction;

  constructor(scope: Construct, id: string, props: any) {
    super(scope, id);
    this.func = new NodejsFunction(scope, 'health-check-lambda', {
      runtime: Runtime.NODEJS_16_X,
      entry: path.resolve(__dirname, 'code', 'index.ts'),
      handler: 'handler',
      timeout: Duration.seconds(60),
      environment: {},
      logRetention: logs.RetentionDays.TWO_WEEKS,
    });
  }
}

#### 內容解密:

此段程式碼定義了一個名為 HealthCheckLambda 的類別,用於建立一個 Lambda 函式。該函式使用 Node.js 16.x 版本執行,執行位於 code/index.ts 的程式碼,並設定了處理函式為 handler。逾時時間被設定為 60 秒,且未傳遞任何環境變數。

使用 AWS CDK 進行無伺服器應用程式開發

設定 Lambda 函式的 Log Retention

在建立 Lambda 函式時,設定日誌保留(logRetention)是非常重要的。日誌保留決定了 CloudWatch 將為 Lambda 函式保留日誌的時間長度。每次 Lambda 執行時,都會在 CloudWatch 的日誌群組中建立一個日誌。建議設定保留政策,以避免日誌無限制地累積,從而產生不必要且不想要的成本。

Lambda 函式的程式碼解析

讓我們來檢查一下我們的 Lambda 函式所執行的程式碼:

export const handler = async () => {
  try {
    return httpResponse(200, JSON.stringify('OK'));
  } catch (error: any) {
    console.error(error);
    return httpResponse(400, JSON.stringify({ message: error.message }));
  }
};

這個程式碼非常簡單直接。它的主要目標是檢查 API 是否正常運作。

內容解密:

  1. handler 函式:這是 Lambda 函式的入口點。當 Lambda 被觸發時,它會執行這個函式。
  2. try-catch 區塊:用於捕捉任何在執行過程中發生的錯誤。如果發生錯誤,會傳回一個包含錯誤訊息的 HTTP 回應。
  3. httpResponse 函式:用於簡化 HTTP 回應的處理。它接收狀態碼和回應主體,並傳回一個包含適當標頭的 HTTP 回應。

httpResponse 函式的實作

export const httpResponse = (
  statusCode: number,
  body: string,
): IHttpResponse => ({
  body,
  headers: {
    'Access-Control-Allow-Origin': '*',
    'Content-Type': 'application/json',
    'Access-Control-Allow-Methods': 'GET,OPTIONS,POST',
  },
  statusCode,
});

內容解密:

  1. httpResponse 引數:接收狀態碼和回應主體。
  2. 傳回的 HTTP 回應:包含回應主體、適當的標頭(如 CORS 設定)和狀態碼。
  3. CORS 設定:允許來自任何來源的請求,並支援 GET、OPTIONS 和 POST 方法。

將 Lambda 函式與 API Gateway 整合

infrastructure/lib/constructs/API-GW/index.ts 檔案中,我們建立了一個 Lambda 函式例項,並使用 LambdaIntegration 方法將其與 API Gateway 整合。

const healthCheckLambda = new HealthCheckLambda(
  this,
  'health-check-lambda-api-endpoint',
  {},
);

const healthCheckLambdaIntegration = new LambdaIntegration(
  healthCheckLambda.func,
);

healthcheck.addMethod('GET', healthCheckLambdaIntegration);

內容解密:

  1. 建立 Lambda 函式例項:使用 HealthCheckLambda 建構函式建立一個新的 Lambda 函式。
  2. 建立 Lambda 與 API Gateway 的整合:使用 LambdaIntegration 方法將 Lambda 函式與 API Gateway 的特定路徑和方法(這裡是 GET 方法)繫結。

建立 GET 和 POST 路由以進行 DynamoDB 操作

為了使我們的 API 更為實用,我們需要建立兩個新的端點:一個用於從 DynamoDB 表中取得所有資料,另一個用於向表中插入資料。

取得 DynamoDB 資料的 Lambda 函式

首先,讓我們建立一個用於取得 DynamoDB 表中所有資料的 Lambda 函式。

export const handler = async () => {
  try {
    const tableName = process.env.TABLE_NAME as string;
    const dynamoDB = new DynamoDB.DocumentClient({
      region: process.env.REGION as string,
    });
    const { Items }: DynamoDB.ScanOutput = await dynamoDB
      .scan({ TableName: tableName })
      .promise();
    return httpResponse(200, JSON.stringify({ todos: Items }));
  } catch (error: any) {
    console.error(error);
    return httpResponse(400, JSON.stringify({ message: error.message }));
  }
};

內容解密:

  1. tableNamedynamoDB:從環境變數中取得表名和區域,並初始化 DynamoDB DocumentClient。
  2. scan 操作:掃描指定的 DynamoDB 表並傳回其專案。
  3. 傳回結果:將掃描結果透過 httpResponse 傳回給客戶端。

設定和佈署無伺服器後端

到目前為止,我們已經成功地建立了一個基本的健康檢查端點,並瞭解瞭如何建立由 Lambda 提供支援的 API 端點。然而,我們的健康檢查端點的功能相當有限。在本文中,我們將探討如何從 Lambda 函式處理程式查詢和寫入資料到 DynamoDB,以構建更複雜和有用的 API 端點。

圖表翻譯:

@startuml
skinparam backgroundColor #FEFEFE
skinparam defaultTextAlignment center
skinparam rectangleBackgroundColor #F5F5F5
skinparam rectangleBorderColor #333333
skinparam arrowColor #333333

title 圖表翻譯:

rectangle "GET 請求" as node1
rectangle "觸發 Lambda" as node2
rectangle "掃描 DynamoDB" as node3
rectangle "傳回資料" as node4
rectangle "傳回結果" as node5

node1 --> node2
node2 --> node3
node3 --> node4
node4 --> node5

@enduml

此圖示展示了客戶端如何透過 API Gateway 發起 GET 請求,觸發 Lambda 函式掃描 DynamoDB,並最終將結果傳回給客戶端的流程。

使用 AWS CDK 進行無伺服器應用程式開發

建立用於插入資料的 Lambda 函式

在完成前一步驟後,我們現在需要為 POST 請求建立 Lambda 函式,以便將專案插入到我們的 Table 中。前往 infrastructure/lib/constructs/Lambda/post/code/index.ts;你將找到以下程式碼:

export const handler = async (event: PostEvent) => {
  try {
    const { todo_name, todo_description, todo_completed } = JSON.parse(event.body).todo;
    const tableName = process.env.TABLE_NAME as string;
    const dynamoDB = new DynamoDB.DocumentClient({
      region: process.env.REGION as string,
    });
    const todo: Todo = {
      id: uuidv4(),
      todo_completed,
      todo_description,
      todo_name,
    };
    await dynamoDB.put({ TableName: tableName, Item: todo }).promise();
    return httpResponse(200, JSON.stringify({ todo }));
  } catch (error: any) {
    console.error(error);
    return httpResponse(400, JSON.stringify({ message: error.message }));
  }
};

內容解密:

  1. 首先,我們從請求主體中解析出必要的資料,如 todo_nametodo_descriptiontodo_completed
  2. 然後,我們初始化 DynamoDB.DocumentClient 以執行對表的所需操作,在本例中為 PUT 操作。
  3. 建立一個 todo 物件,其中包含一個使用 uuidv4() 函式生成的唯一 id,以及其他相關資料。
  4. 使用 dynamoDB.put() 方法將 todo 物件插入到指定的表中。
  5. 如果操作成功,則傳回一個包含新建 todo 專案的 HTTP 200 回應;如果發生錯誤,則傳回一個包含錯誤訊息的 HTTP 400 回應。

建立 Lambda 函式

接下來,我們需要在 infrastructure/lib/constructs/Lambda/post/index.ts 中建立 Lambda 函式:

const { dynamoTable } = props;
this.func = new NodejsFunction(scope, 'dynamo-post', {
  runtime: Runtime.NODEJS_16_X,
  entry: path.resolve(__dirname, 'code', 'index.ts'),
  handler: 'handler',
  timeout: Duration.seconds(60),
  environment: {
    NODE_ENV: process.env.NODE_ENV as string,
    TABLE_NAME: dynamoTable.tableName,
    REGION: process.env.CDK_DEFAULT_REGION as string,
  },
  logRetention: logs.RetentionDays.TWO_WEEKS,
});
dynamoTable.grantWriteData(this.func);

內容解密:

  1. 初始化一個新的 NodejsFunction,指定執行環境、進入點、處理函式和超時時間。
  2. 設定環境變數,包括 NODE_ENVTABLE_NAMEREGION
  3. 設定日誌保留期限為兩週。
  4. 賦予 Lambda 函式寫入 dynamoTable 的許可權。

簡化 Lambda 整合

除了前述方法外,還有一種更簡單的方式來整合 AWS Lambda,即使用 LambdaRestApi 建構函式。此函式設定了一個具有預設 Lambda 函式的 REST API,並使用 API Gateway 的貪婪代理選項 ({proxy+}) 和 ANY 方法。這意味著每個請求都將自動路由到此 Lambda 函式。

new LambdaRestApi(this, 'MyRestApi', {
  handler: lambda.func,
  restApiName: 'rest-api-name',
  defaultCorsPreflightOptions: {
    allowOrigins: ['*'],
    allowHeaders: ['*'],
    allowMethods: ['*'],
    statusCode: 204,
  },
});

內容解密:

  1. 初始化一個新的 LambdaRestApi,指定處理函式和 REST API 名稱。
  2. 設定預設的 CORS 預檢選項,以允許所有來源、標頭和方法。

建立和組態 DynamoDB 例項

在建立了 Lambdas 和 API Gateway 後,剩下的步驟是替換 RDS,建立一個 DynamoDB 表。

this.table = new Table(this, `Dynamo-Table-${process.env.NODE_ENV || ''}`, {
  partitionKey: { name: 'id', type: AttributeType.STRING },
  tableName: `todolist-${process.env.NODE_ENV?.toLowerCase() || ''}`,
  billingMode: BillingMode.PAY_PER_REQUEST,
  removalPolicy: RemovalPolicy.DESTROY,
});
new DynamoDBSeeder(this, `Dynamo-InlineSeeder-${process.env.NODE_ENV || ''}`, {
  table: this.table,
  seeds: Seeds.fromInline([
    {
      id: uuidv4(),
      todo_name: 'First todo',
      todo_description: "That's a todo for demonstration purposes",
      todo_completed: true,
    },
  ]),
});

內容解密:

  1. 建立一個新的 DynamoDB 表,指定分割鍵、表名、計費模式和移除策略。
  2. 使用 DynamoDBSeeder 建構函式在佈署時自動為表填充資料。