所有文章 > 日积月累 > 使用 AWS Lambda 和 AWS API Gateway 的无服务器 API 深度指南(第 2 部分)
使用 AWS Lambda 和 AWS API Gateway 的无服务器 API 深度指南(第 2 部分)

使用 AWS Lambda 和 AWS API Gateway 的无服务器 API 深度指南(第 2 部分)

在第一部分中,我们了解了身份验证、请求主体、状态代码、CORS 和响应标头。我们建立了一个连接 API-Gateway、Lambda 和 Cognito 的 AWS SAM 项目,以便用户可以注册和登录。

在本文中,我们将讨论第三方集成、数据存储和检索以及“从 Lambda 函数调用 Lambda 函数”(我们实际上不会这样做,而是了解一种实现类似操作的方法。

添加图片上传

我们将从图片上传开始。其工作原理如下:

  1. 通过我们的 API 请求预签名的 S3 URL
  2. 通过预签名的 URL 将图像直接上传到 S3

因此我们需要两个新的 SAM/CloudFormation 资源。一个 Lambda 函数,用于为我们的图像生成预签名 URL 和一个 S3 存储桶。

让我们更新 SAM 模板:

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: "A example REST API build with serverless technology"

Globals:

Function:
Runtime: nodejs8.10
Handler: index.handler
Timeout: 30
Tags:
Application: Serverless API

Resources:

ServerlessApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors: "'*'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
GatewayResponses:
UNAUTHORIZED:
StatusCode: 401
ResponseParameters:
Headers:
Access-Control-Expose-Headers: "'WWW-Authenticate'"
Access-Control-Allow-Origin: "'*'"
WWW-Authenticate: >-
'Bearer realm="admin"'

# ============================== Auth ==============================
AuthFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/auth/
Environment:
Variables:
USER_POOL_ID: !Ref UserPool
USER_POOL_CLIENT_ID: !Ref UserPoolClient
Events:
Signup:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signup
Method: POST
Auth:
Authorizer: NONE
Signin:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signin
Method: POST
Auth:
Authorizer: NONE

PreSignupFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async event => {
event.response = { autoConfirmUser: true };
return event;
};

UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ApiUserPool
LambdaConfig:
PreSignUp: !GetAtt PreSignupFunction.Arn
Policies:
PasswordPolicy:
MinimumLength: 6

UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: ApiUserPoolClient
GenerateSecret: no

LambdaCognitoUserPoolExecutionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt PreSignupFunction.Arn
Principal: cognito-idp.amazonaws.com
SourceArn: !Sub 'arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}'

# ============================== Images ==============================
ImageBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
CorsConfiguration:
CorsRules:
- AllowedHeaders:
- "*"
AllowedMethods:
- HEAD
- GET
- PUT
- POST
AllowedOrigins:
- "*"

ImageBucketPublicReadPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ImageBucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: "*"
Resource: !Join ["", ["arn:aws:s3:::", !Ref "ImageBucket", "/*" ]]

ImageFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/images/
Policies:
- AmazonS3FullAccess
Environment:
Variables:
IMAGE_BUCKET_NAME: !Ref ImageBucket
Events:
CreateImage:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /images
Method: POST

Outputs:

ApiUrl:
Description: The target URL of the created API
Value: !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
Export:
Name: ApiUrl

好的,我们最终得到了三个新资源。我们还需要一个BucketPolicy允许公开读取我们新图片存储桶的资源。

ImagesFunction一个 API 事件,因此我们可以使用它来处理 POST 请求。该函数获取 S3 访问策略和环境变量,因此它知道ImageBucket

我们需要为函数代码创建一个新文件functions/images/index.js

const AWS = require("aws-sdk");

exports.handler = async event => {
const userName = event.requestContext.authorizer.claims["cognito:username"];
const fileName = "" + Math.random() + Date.now() + "+" + userName;
const { url, fields } = await createPresignedUploadCredentials(fileName);
return {
statusCode: 201,
body: JSON.stringify({
formConfig: {
uploadUrl: url,
formFields: fields
}
}),
headers: { "Access-Control-Allow-Origin": "*" }
};
};

const s3Client = new AWS.S3();
const createPresignedUploadCredentials = fileName => {
const params = {
Bucket: process.env.IMAGE_BUCKET_NAME,
Fields: { Key: fileName }
};
return new Promise((resolve, reject) =>
s3Client.createPresignedPost(params, (error, result) =>
error ? reject(error) : resolve(result)
)
);
};

那么,这个函数中发生了什么?

userName首先,我们从对象中提取event。API-Gateway 和 Cognito 控制对我们函数的访问,因此我们可以确保event在调用该函数时对象中存在用户。

fileName接下来,我们根据随机数、当前时间戳和用户名创建一个唯一的。

辅助函数createPresignedUploadCredentials将创建预签名的 S3 URL。它将返回一个具有urlfields属性的对象。

我们的 API 客户端必须向发送 POST 请求url,并在其正文中包含所有字段和文件。

整合图像识别

现在,我们需要集成第三方图像识别服务。

当图像上传时,S3 将触发一个上传事件,该事件由 lambda 函数处理。

这使我们想到第一篇文章开头提出的一个问题。

如何从 Lambda 调用 Lambda?

简短的回答是:你不需要。

为什么?虽然您可以通过 AWS-SDK 直接从 Lambda 调用 Lambda,但这会带来一个问题。如果出现问题,我们必须实现所有需要发生的事情,例如重试等。此外,在某些情况下,调用 Lambda 必须等待被调用的 Lambda 完成,我们也必须为等待时间付费。

那么还有哪些替代方案呢?

Lambda 是一个基于事件的系统,我们的函数可以由不同的事件源触发。主要做法是使用另一个服务来执行此操作。

在我们的例子中,我们希望在文件上传完成时调用 Lambda,因此我们必须使用 S3 事件作为源。但也有其他事件源。

  • Step Functions让我们能够协调 Lambda 函数与状态机
  • SQS是一个队列,我们​​可以将结果推送到其中,以便其他 Lambda 可以获取它们
  • SNS是一种服务,它允许我们将一个 Lambda 结果并行分发到许多其他 Lambda。

我们向 SAM 模板添加了一个新的 Lambda 函数,该函数将被 S3 调用。

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: "An example REST API build with serverless technology"

Globals:

Function:
Runtime: nodejs8.10
Handler: index.handler
Timeout: 30
Tags:
Application: Serverless API

Resources:

ServerlessApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors: "'*'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
GatewayResponses:
UNAUTHORIZED:
StatusCode: 401
ResponseParameters:
Headers:
Access-Control-Expose-Headers: "'WWW-Authenticate'"
Access-Control-Allow-Origin: "'*'"
WWW-Authenticate: >-
'Bearer realm="admin"'

# ============================== Auth ==============================
AuthFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/auth/
Environment:
Variables:
USER_POOL_ID: !Ref UserPool
USER_POOL_CLIENT_ID: !Ref UserPoolClient
Events:
Signup:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signup
Method: POST
Auth:
Authorizer: NONE
Signin:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signin
Method: POST
Auth:
Authorizer: NONE

PreSignupFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async event => {
event.response = { autoConfirmUser: true };
return event;
};

UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ApiUserPool
LambdaConfig:
PreSignUp: !GetAtt PreSignupFunction.Arn
Policies:
PasswordPolicy:
MinimumLength: 6

UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: ApiUserPoolClient
GenerateSecret: no

LambdaCognitoUserPoolExecutionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt PreSignupFunction.Arn
Principal: cognito-idp.amazonaws.com
SourceArn: !Sub 'arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}'

# ============================== Images ==============================
ImageBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
CorsConfiguration:
CorsRules:
- AllowedHeaders:
- "*"
AllowedMethods:
- HEAD
- GET
- PUT
- POST
AllowedOrigins:
- "*"

ImageBucketPublicReadPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ImageBucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: "*"
Resource: !Join ["", ["arn:aws:s3:::", !Ref "ImageBucket", "/*" ]]

ImageFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/images/
Policies:
- AmazonS3FullAccess
Environment:
Variables:
IMAGE_BUCKET_NAME: !Ref ImageBucket
Events:
CreateImage:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /images
Method: POST

TagsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/tags/
Environment:
Variables:
PARAMETER_STORE_CLARIFAI_API_KEY: /serverless-api/CLARIFAI_API_KEY_ENC
Policies:
- AmazonS3ReadOnlyAccess # Managed policy
- Statement: # Inline policy document
- Action: [ 'ssm:GetParameter' ]
Effect: Allow
Resource: '*'
Events:
ExtractTags:
Type: S3
Properties:
Bucket: !Ref ImageBucket
Events: s3:ObjectCreated:*

Outputs:

ApiUrl:
Description: The target URL of the created API
Value: !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
Export:
Name: ApiUrl

我们添加了一个新的无服务器功能资源,该资源具有一个 S3 事件,当我们在 中创建新的 S3 对象时会调用该事件ImageBucket

我们的 Lambda 函数需要调用第三方 API,即 Clarifai API,这引出了下一个问题。

如何存储第三方 API 的凭证?

有很多方法可以做到这一点。

一种方法是使用 KMS 加密凭证。我们通过 CLI 对其进行加密,将加密密钥作为环境变量添加到 template.yaml 中,然后在 Lambda 内部使用 AWS-SDK 解密凭证,然后再使用它们。

另一种方法是使用AWS Systems Manager参数存储。此服务允许在 Lambda 中存储可通过 AWS-SDK 检索的加密字符串。我们只需为 Lambda 提供定义凭证存储位置的名称即可。

在这个例子中,我们将使用参数存储。

如果您尚未创建Clarifai帐户,现在是时候了。他们将为您提供 API 密钥,我们接下来需要将其存储到参数存储中。

aws ssm put-parameter \
--name "/serverless-api/CLARIFAI_API_KEY" \
--type "SecureString" \
--value "<CLARIFAI_API_KEY>"

该命令会将密钥放入参数存储并对其进行加密。

接下来,我们需要通过环境变量告诉我们的 Lambda 函数名称,并授予它getParameter通过 AWS-SDK 调用的权限。

Environment:
Variables:
PARAMETER_STORE_CLARIFAI_API_KEY: /serverless-api/CLARIFAI_API_KEY_ENC
Policies:
- Statement:
- Action: [ 'ssm:GetParameter' ]
Effect: Allow
Resource: '*'

让我们看看 JavaScript 方面的事情,为此,我们创建一个新文件functions/tags/index.js

const AWS = require("aws-sdk");
const Clarifai = require("clarifai");

exports.handler = async event => {
const record = event.Records[0];
const bucketName = record.s3.bucket.name;
const fileName = record.s3.object.key;
const tags = await predict(`https://${bucketName}.s3.amazonaws.com/${fileName}`);

await storeTagsSomewhere({ fileName, tags });
};

const ssm = new AWS.SSM();
const predict = async imageUrl => {
const result = await ssm.getParameter({
Name: process.env.PARAMETER_STORE_CLARIFAI_API_KEY,
WithDecryption: true
}).promise();

const clarifaiApp = new Clarifai.App({
apiKey: result.Parameter.Value
});

const model = await clarifaiApp.models.initModel({
version: "aa7f35c01e0642fda5cf400f543e7c40",
id: Clarifai.GENERAL_MODEL
});

const clarifaiResult = await model.predict(imageUrl);

const tags = clarifaiResult.outputs[0].data.concepts
.filter(concept => concept.value > 0.9)
.map(concept => concept.name);
return tags;
};

使用 S3 对象创建事件调用处理程序。只有一条记录,但我们也可以告诉 S3 将记录批量处理在一起。

然后我们为新创建的图像创建一个 URL 并将其提供给函数predict

predict函数使用我们的PARAMETER_STORE_CLARIFAI_API_KEY环境变量来获取参数存储中的参数名称。这使我们能够更改目标参数而无需更改 Lambda 代码。

我们解密了 API 密钥,就可以调用第三方 API。然后我们将标签存储在某处。

列出和删除带标签的图像

现在我们可以上传图像并且它们会被自动标记,下一步就是列出所有图像,按标签过滤它们,如果不再需要就删除它们。

让我们更新 SAM 模板!

AWSTemplateFormatVersion: "2010-09-09"
Transform: "AWS::Serverless-2016-10-31"
Description: "An example REST API build with serverless technology"

Globals:

Function:
Runtime: nodejs8.10
Handler: index.handler
Timeout: 30
Tags:
Application: Serverless API

Resources:

ServerlessApi:
Type: AWS::Serverless::Api
Properties:
StageName: Prod
Cors: "'*'"
Auth:
DefaultAuthorizer: CognitoAuthorizer
Authorizers:
CognitoAuthorizer:
UserPoolArn: !GetAtt UserPool.Arn
GatewayResponses:
UNAUTHORIZED:
StatusCode: 401
ResponseParameters:
Headers:
Access-Control-Expose-Headers: "'WWW-Authenticate'"
Access-Control-Allow-Origin: "'*'"
WWW-Authenticate: >-
'Bearer realm="admin"'

# ============================== Auth ==============================
AuthFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/auth/
Environment:
Variables:
USER_POOL_ID: !Ref UserPool
USER_POOL_CLIENT_ID: !Ref UserPoolClient
Events:
Signup:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signup
Method: POST
Auth:
Authorizer: NONE
Signin:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /signin
Method: POST
Auth:
Authorizer: NONE

PreSignupFunction:
Type: AWS::Serverless::Function
Properties:
InlineCode: |
exports.handler = async event => {
event.response = { autoConfirmUser: true };
return event;
};

UserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: ApiUserPool
LambdaConfig:
PreSignUp: !GetAtt PreSignupFunction.Arn
Policies:
PasswordPolicy:
MinimumLength: 6

UserPoolClient:
Type: AWS::Cognito::UserPoolClient
Properties:
UserPoolId: !Ref UserPool
ClientName: ApiUserPoolClient
GenerateSecret: no

LambdaCognitoUserPoolExecutionPermission:
Type: AWS::Lambda::Permission
Properties:
Action: lambda:InvokeFunction
FunctionName: !GetAtt PreSignupFunction.Arn
Principal: cognito-idp.amazonaws.com
SourceArn: !Sub 'arn:${AWS::Partition}:cognito-idp:${AWS::Region}:${AWS::AccountId}:userpool/${UserPool}'

# ============================== Images ==============================
ImageBucket:
Type: AWS::S3::Bucket
Properties:
AccessControl: PublicRead
CorsConfiguration:
CorsRules:
- AllowedHeaders:
- "*"
AllowedMethods:
- HEAD
- GET
- PUT
- POST
AllowedOrigins:
- "*"

ImageBucketPublicReadPolicy:
Type: AWS::S3::BucketPolicy
Properties:
Bucket: !Ref ImageBucket
PolicyDocument:
Statement:
- Action: s3:GetObject
Effect: Allow
Principal: "*"
Resource: !Join ["", ["arn:aws:s3:::", !Ref "ImageBucket", "/*" ]]

ImageFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/images/
Policies:
- AmazonS3FullAccess
Environment:
Variables:
IMAGE_BUCKET_NAME: !Ref ImageBucket
Events:
ListImages:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /images
Method: GET
DeleteImage:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /images/{imageId}
Method: DELETE
CreateImage:
Type: Api
Properties:
RestApiId: !Ref ServerlessApi
Path: /images
Method: POST

TagsFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/tags/
Environment:
Variables:
PARAMETER_STORE_CLARIFAI_API_KEY: /serverless-api/CLARIFAI_API_KEY_ENC
Policies:
- AmazonS3ReadOnlyAccess # Managed policy
- Statement: # Inline policy document
- Action: [ 'ssm:GetParameter' ]
Effect: Allow
Resource: '*'
Events:
ExtractTags:
Type: S3
Properties:
Bucket: !Ref ImageBucket
Events: s3:ObjectCreated:*

Outputs:

ApiUrl:
Description: The target URL of the created API
Value: !Sub "https://${ServerlessApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
Export:
Name: ApiUrl

我们添加了一些新的 API 事件ImagesFunction,现在也需要为其更新 JavaScript。

const AWS = require("aws-sdk");

exports.handler = async event => {
switch (event.httpMethod.toLowerCase()) {
case "post":
return createImage(event);
case "delete":
return deleteImage(event);
default:
return listImages(event);
}
};

const createImage = async event => {
const userName = extractUserName(event);
const fileName = "" + Math.random() + Date.now() + "+" + userName;
const { url, fields } = await createPresignedUploadCredentials(fileName);
return response({
formConfig: {
uploadUrl: url,
formFields: fields
}
}, 201);
};

const deleteImage = async event => {
const { imageId } = event.pathParameters;
await deleteImageSomewhere(imageId);
return response({ message: "Deleted image: " + imageId });
};

// Called with API-GW event
const listImages = async event => {
const { tags } = event.queryStringParameters;
const userName = extractUserName(event);
const images = await loadImagesFromSomewhere(tags.split(","), userName);
return response({ images });
};

// ============================== HELPERS ==============================

const extractUserName = event => event.requestContext.authorizer.claims["cognito:username"];

const response = (data, statusCode = 200) => ({
statusCode,
body: JSON.stringify(data),
headers: { "Access-Control-Allow-Origin": "*" }
});

const s3Client = new AWS.S3();
const createPresignedUploadCredentials = fileName => {
const params = {
Bucket: process.env.IMAGE_BUCKET_NAME,
Fields: { Key: fileName }
};

return new Promise((resolve, reject) =>
s3Client.createPresignedPost(params, (error, result) =>
error ? reject(error) : resolve(result)
)
);
};

删除功能很简单,但也回答了我们的一个问题。

如何使用查询字符串或路由参数?

const deleteImage = async event => {
const { imageId } = event.pathParameters;
...
};

API-Gateway 事件对象有一个pathParameters属性,该属性保存了我们在 SAM 模板中为该事件定义的所有参数。在我们的例子中,imageId因为我们定义了Path: /images/{imageId}

以类似方式使用的查询字符串。

const listImages = async event => {
const { tags } = event.queryStringParameters;
...
};

其余代码不太复杂。我们将使用偶数数据来加载或删除图像。

查询字符串将作为对象存储在queryStringParameters我们event对象的属性内。

结论

有时新技术会带来范式转变。无服务器就是其中一种技术。

它的全部功能通常归结为尽可能使用托管服务。不要自己动手加密、身份验证、存储或计算。使用云提供商已经实现的功能。

这里最大的问题往往是做事的方法有很多。没有唯一正确的方法。我们应该直接使用 KMS 吗?我们应该让系统管理器为我们处理事情吗?我们应该通过 Lambda 实现身份验证吗?我们应该使用 Cognito 来完成它吗?

我希望我能在某种程度上回答使用 Lambda 时出现的问题。

文章来源:In Depth Guide to Serverless APIs with AWS Lambda and AWS API Gateway (Part 2)

#你可能也喜欢这些API文章!