深入 Serverless—让 Lambda 和 API Gateway 支持二进制数据

阅读数:19 2019 年 11 月 15 日 08:00

深入Serverless—让Lambda 和 API Gateway支持二进制数据

1. 概述

Serverless 即无服务器架构正在迅速举起,AWS Lambda 和 AWS API Gateway 作为 Serverless 架构主要的服务,正受到广泛关注,也有越来越多用户使用它们,享受其带来的便利。传统上来说,Lambda 和 API Gateway 主要用以实现 RESTful 接口,其响应输出结果是 JSON 数据,而实际业务场景还有需要输出二进制数据流的情况,比如输出图片内容。本文以触发式图片处理服务为例,深入挖掘 Lambda 和 API Gateway 的最新功能,让它们支持二进制数据,展示无服务器架构更全面的服务能力。

先看一个经典架构的案例——响应式主动图片处理服务。

Lambda 配合 S3 文件上传事件触发在后台进行图片处理,比如生成缩略图,然后再上传到 S3,这是 Lambda 用于事件触发的一个经典场景。

http://docs.aws.amazon.com/lambda/latest/dg/with-s3-example.html

在实际生产环境中这套架构还有一些局限,比如:

  • 后台运行的图片处理可能无法保证及时完成,用户上传完原图后需要立即查看缩略图时还没有生成。
  • 很多图片都是刚上传后使用频繁,一段时间以后就使用很少了,但是缩略图还不能删,因为也可能有少量使用,比如查看历史订单时。
  • 客户端设备类型繁多,一次性生成所有尺寸的缩略图,会消耗较多 Lambda 运算时间和 S3 存储。
  • 如果增加了新的尺寸类型,旧图片要再生成新的缩略图就比较麻烦了。

我们使用用户触发的架构来实现实时图片处理服务,即当用户请求某个缩略图时实时生成该尺寸的缩略图,然后通过 CloudFront 缓存在 CDN 上。这其实还是事件触发执行 Lambda,只是由文件上传的事件主动触发,变成了用户访问的被动触发。但是只有原图存储在 S3,任何尺寸的缩图都不生成文件不存储到 S3。要实现此架构方案,核心技术点就是让 Lambda 和 API Gateway 可以响应输出二进制的图片数据流。

总体架构图如下:

深入Serverless—让Lambda 和 API Gateway支持二进制数据

主要技术点:

  • 涉及服务都是 AWS 完全托管的,自动扩容,无需运维,尤其是 Lambda,按运算时间付费,省去 EC2 部署的繁琐。
  • 原图存在 S3 上,只开放给 Lambda 的读取权限,禁止其它人访问原图,保护原图数据安全。
  • Lambda 实时生成缩略图,尽管 Lambda 目前还不支持直接输出二进制数据,我们可以设置让它输出 base64 编码后的文本,并且不再使用 JSON 结构。配合 API Gateway 可以把 base64 编码后的文本再转换回二进制数据,最终就可以实现输出二进制数据流了。
  • 用 API Gateway 实现 图片访问的 URL。我们常见的 API Gateway 用来做 RESTful 的 API 接口,接口的 URL 形式通常是 /resource?parameter=value,其实还可以配置成不用 GET 参数,而把 URL 中的路径部分作参数映射成后端的参数。
  • 回源 API Gateway,缓存时间可以用户自定义,建议为 24 小时。直接支持 HTTPS,支持享用 AWS 全球边缘节点。
  • CloudFront 上还可使用 Route 53 配置域名,支持用户自己的域名。

相比前述的主动生成,被动触发生成有以下便利或优势:

  • 缩略图都不存储在 S3 上,节省存储空间和成本。
  • 方便给旧图增加新尺寸的缩略图。

2. 部署与配置

本例中使用的 Region 是 Oregon(us-west-2),有关文件从以下链接下载:

https://s3.amazonaws.com/snowpeak-share/lambda/awslogo.png

2.1 使用 IAM 设置权限

打开控制台:

https://console.aws.amazon.com/iam/home?region=us-west-2

创建一个 Policy,名叫 CloudWatchLogsWrite,用于确保 Lambda 运行的日志可以写到 CloudWatch Logs。内容是

{
"Version":
"2012-10-17",
"Statement": [
{
"Effect":
"Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents",
"logs:DescribeLogStreams"
],
"Resource": [
"arn:aws:logs:*:*:*"
]
}
]
}

创建 一个 Role,名叫 LambdaReadS3,用于 Lambda 访问 S3。Attach Poilcy 选 AmazonS3ReadOnlyAccess 和刚刚创建的 CloudWatchLogsWrite。

记下它的 ARN,比如 arn:aws:iam::111122223333:role/LambdaReadS3

2.2 使用 S3 配置原图存储

打开控制台

https://console.aws.amazon.com/s3/home?region=us-west-2

创建 Bucket,Bucket Name 需要填写全局唯一的,比如 img201703,Region 选 US Standard。通常图片的原图禁止直接访问,这里我们设置权限,仅允许 Lambda 访问。

Permissions 下点 Add bucket policy,使用 AWS Policy Generator :

Select Type of Policy 选 S3 bucket policy,

Principal 填写前述创建的 LambdaReadS3 的 ARN arn:aws:iam::111122223333:role/LambdaReadS3

Actions 下拉选中 GetObjext,

Amazon Resource Name (ARN) 填写刚刚创建的 bucket 的 ARN,比如arn:aws:s3:::image201703/*

然后点 Add Statement,最后再点 Generate Policy,生成类似

{
"Id": "Policy1411122223333",
"Version":
"2012-10-17",
"Statement": [
{
"Sid": "Stmt1411122223333",
"Action": [
"s3:GetObject"
],
"Effect":
"Allow",
"Resource":
"arn:aws:s3:::img201703/*",
"Principal": {
"AWS": [
"arn:aws:iam::111122223333:role/LambdaReadS3"
]
}
}
]
}

复制粘贴到 Bucket Policy Editor 里 save 即可。

验证 S3 bucket 配置效果。把前下载图片文件 awslogo.png 下载到自己电脑,然后把它上传到这个 bucket 里,测试一下。直接访问链接不能下载,需要右键菜单点“Download”才能下载,说明权限配置已经成功。

2.3 创建 Lambda 函数

AWS Lambda 管理控制台:

https://us-west-2.console.aws.amazon.com/lambda/home?region=us-west-2#/

点击 Create a Lambda function 按钮

Select runtime 菜单选 Node.js 4.3,然后点 Blank Function。

Configure triggers 页,点 Next。

Configure function 页,在 Name 栏输入 ImageMagick,Description 栏输入

Uses ImageMagick to perform simple image processing operations, such as resizing.

Lambda function code 里填写以下代码:

'use strict';

var AWS = require('aws-sdk');
const im = require('imagemagick');
const fs = require('fs');

const defaultFilePath = 'awslogo_w300.png';
// 样式名配置,把宽高尺寸组合定义成样式名。
const config = {
'w500':{'width':500,
'height':300},
'w300':{'width':300,
'height':150},
'w50':{'width':50,
'height':40}
};
// 默认样式名
const defaultStyle = 'w50';
// 完成处理后把临时文件删除的方法。
const postProcessResource = (resource, fn) => {
let ret = null;
if (resource) {
if (fn) {
ret = fn(resource);
}
try {

fs.unlinkSync(resource);
} catch (err) {
// Ignore
}
}
return ret;
};
// 生成缩略图的主方法
const resize = (filePathResize, style, data, callback) => {
// Lambda 本地写文件,必须是 /tmp/ 下
var filePathResize =
'/tmp/'+filePathResize;
// 直接用 Buffer 操作图片转换,源文件不写到本地磁盘,但是转换成的文件要写盘,所以最后再用 postProcessResource 把临时文件删了。
var resizeReq = {
srcData: data.Body,
dstPath: filePathResize,
width: style.width,
height: style.height
};
try {
im.resize(resizeReq,
(err) => {
if (err) {
throw err;
} else {

console.log('Resize ok: '+ filePathResize);
// 注意这里不使用 JSON 结构,直接输出图片内容数据,并且要进行 base64 转码。
callback(null,
postProcessResource(filePathResize, (file) => new
Buffer(fs.readFileSync(file)).toString('base64')));
}
});
} catch (err) {
console.log('Resize
operation failed:', err);
callback(err);
}
};

exports.handler = (event, context, callback) => {
var s3 = new AWS.S3();
// 改成刚刚创建的 bucket 名字,如 img201703
var bucketName = 'image201702';
// 从文件 URI 中截取出 S3 上的 key 和尺寸信息。
// 稳妥起见,尺寸信息应该规定成样式名字,而不是直接把宽高参数化,因为后者会被人滥用。
// 使用样式还有个好处,样式名字如果写错,可以有个默认的样式。
var filepath = (undefined ===
event.filepath ? defaultFilePath:event.filepath);
var tmp =
filepath.split('.');
var fileExt = tmp[1];
tmp = tmp[0].split('_');
var fileName = tmp[0];
var style = tmp.pop();
console.log(style);
var validStyle = false;
for (var i in config)
{
if (style == i)
{
validStyle = true;
break;
}
}
style = validStyle ? style :
defaultStyle;
console.log(style);
var fileKey =
fileName+'.'+fileExt;
var params = {Bucket:
bucketName, Key: fileKey};

// 从 S3 下载文件,成功后再回调缩图
s3.getObject(params,
function(err, data) {
if (err)
{
console.log(err,
err.stack);
}
else
{
resize(filepath,
config[style], data, callback);
}
});
};

注意一定要把

var bucketName = ‘image201702’;

改成刚刚创建的 bucket 名字,如

var bucketName = ‘img201703’;

这个 Lambda 函数就可以运行了。

Lambda function handler and role 部分的 Role 选择 Choose an existing role,然后 Existing role 选择之前创建的 LambdaReadS3。

Advanced settings:Memory (MB)* 选 512,Timeout 选 30 sec。

其它保持默认,点 Next;最后一页确认一下,点 Create Function。

提示创建成功。

点击 Test 按钮,测试一下。第一次测试时,会弹出测试使用的参数值,这些参数其实我们都不用,也不用管它,点击 Save and test 按钮测试即可。以后再测试就不会弹出了。

显示“Execution result: succeeded”表示测试成功了,右边的 Logs 链接可以点击,前往 CloudWatch Logs,查看详细日志。右下方的 Log output 是当前测试执行的输出。

深入Serverless—让Lambda 和 API Gateway支持二进制数据

可以看到这里 Execution result 下面显示的结果是一个长字符串,已经不是我们以往普通 Lambda 函数返回的 JSON 结构了。想做进一步验证的,可以把这个长字符串 base64 解码,会看到一个尺寸变小的图片,那样可以进一步验证我们运行成功。

2.4 配置 API Gateway

管理控制台

https://us-west-2.console.aws.amazon.com/apigateway/home?region=us-west-2#

2.4.1 配置 API

点击 Create API

API name 填写 ImageMagick。
Description 填写 Endpoint for Lambda using ImageMagick to perform simple image processing operations, such as resizing.

这时左侧导航链接会显示成 APIs > ImageMagick > Resources。点击 Actions 下拉菜单,选择 Create Resource。

Resource Name* 填写 filepath

Resource Path* 填写 {filepath},注意要包括大括号。然后点击 Create Resource 按钮。

这时刚刚创建的{filepath}应该是选中状态,再点击 Actions 下拉菜单,选择 Create Method,在当时出现的方法菜单里选择 GET,然后点后面的对号符确定。

然后在 /{filepath} – GET – Setup 页,Integration type 保持 Lambda Function 不变,Lambda Region 选 us-west-2,在 Lambda Function 格输入 ImageMagick,下拉的备选菜单中点中 ImageMagick,点击 Save。弹出赋权限提示,点击“OK”。

这时会显示出完整的“/{filepath} – GET – Method Execution”配置页。

点击右上角“Integration Request”链接,进入配置页,点击“Body Mapping Templates”左边的三角形展开之。

Request body passthrough 选择 When there are no templates defined (recommended)。

点击最下面“add mapping template”链接,“Content-Type”格,注意即使这里已经有提示文字 application/json,还是要自己输入 application/json,然后点击右边的对勾链接,下面会弹出模板编辑输入框,输入

{“filepath”: “$input.params(‘filepath’)”}

完成的效果如下图所示:

深入Serverless—让Lambda 和 API Gateway支持二进制数据

最后点击“Save”按钮。点击左上角 “Method Execution”链接返回。

点击左下角“Method Response”链接,HTTP Status 下点击第一行 200 左边的三角形展开之,“Response Headers for 200” 下点击 add header 链接,Name 格输入 Content-Type,点击右边的对勾链接保存。Response Body for 200 下已有一行 application/json,点击其右边的笔图标编辑,把值改成 image/png,点击右边的对勾链接保存。点击左上角 “Method Execution”链接返回。

点击右下角“Integration Response”链接,点击第一行 “- 200 Yes”左边的三角形展开之,“Content handling”选择 Convert to binary(if needed),然后点击“Save”按钮。这项配置是把 Lambda 返回的 base64 编码的图片数据转换成二进制的图片数据,是此架构的另一个技术重点。

Header Mappings 下已有一行 Content-Type,点击其右边的笔图标编辑,在“Mapping value”格输入’image/png’,注意要带上单引号,点击右边的对勾链接保存。

点击“Body Mapping Templates”左边三角形展开之,点击“application/json”右边的减号符,把它删除掉。点击左上角 “Method Execution”链接返回。

点击最左边的竖条 Test 链接,来到“/{filepath} – GET – Method Test”页,“{filepath}”格输入 awslogo_w300.png,点击 Test 按钮。右侧显示类似下面的结果

Request: /awslogo_w300.png

Status: 200

Latency: 247 ms

Response Body 是乱码是正常的,因为我们的返回内容就是图片文件本身。可以查看右下角 Logs 部分显示的详细执行情况,显示类似以下的日志表示执行成功。

Thu Mar 09 03:40:11 UTC 2017 : Method response body
after transformations: [Binary Data]

Thu Mar 09 03:40:11 UTC 2017 : Method response
headers: {X-Amzn-Trace-Id=Root=1-12345678-1234567890abcdefghijlkln,
Content-Type=image/png}

Thu Mar 09 03:40:11 UTC 2017 : Successfully completed
execution

Thu Mar 09 03:40:11 UTC 2017 : Method completed with
status: 200

2.4.2 部署 API

点击 Actions 按钮,在下拉菜单中点选 Deploy API,Deployment stage 选择 [New Stage],Stage name 输入 test,注意这里都是小写。

Stage description 输入 test stage

Deployment description 输入 initial deploy to test.

点击 Deploy 按钮。然后会跳转到 Test Stage Editor 页。

复制 Invoke URL: 后面的链接,比如

https://1234567890.execute-api.us-west-2.amazonaws.com/test

然后在后面接上 awslogo_w300.png,组成形如以下的链接

https://1234567890.execute-api.us-west-2.amazonaws.com/test/awslogo_w300.png

输入浏览器地址栏里访问,可以得到一张图片,表示 API Gateway 已经配置成功。

2.5 配置 CloudFront 分发

我们在 API Gateway 前再加上 CloudFront,通过 CDN 缓存生成好的图片,就可以实现不需要把缩略图额外存储,而又不用每次都为了图片处理进行计算。这里使用了 CDN 和其它使用 CDN 的思路一样,如果更新图片,不建议调用清除 CloudFront 的 API,而是从应用程序生成新的图片标识字符串,从而生成新的 URL 让 CloudFront 成为无缓存状态从而回源重新计算。

由于 API Gateway 仅支持 HTTPS 访问,而 CloudFront 同时支持 HTTP 和 HTTPS,所以我们可以配置成 CloudFront 前端同时支持 HTTP 和 HTTPS,但是实测发现 CloudFront 前端使用 HTTP 而回源使用 HTTPS 时性能不如前端和回源同为 HTTPS。所以这里我们也采用同时 HTTPS 的方式。

我们打开 CloudFront 的管理控制台

https://console.aws.amazon.com/cloudfront/home?region=us-west-2#

点击 Create Distribution 按钮,在 Web 下点击 Get Started。

Origin Domain Name,输入上述部署出来的 API Gateway 的域名,比如 1234567890.execute-api.us-west-2.amazonaws.com

Origin Path,输入上述 API Gateway 的 Stage 名,如 /test

Origin Protocol Policy 选择 HTTPS Only

Object Caching 点选 Customize,然后 Maximum TTL 输入 86400

Alternate Domain Names (CNAMEs) 栏本例使用自己的域名,比如 img.myexample.com。SSL Certificate 选择 Custom SSL Certificate (example.com),并从下面的证书菜单中选择一个已经通过 ACM 管理的证书。

注意,如果填写了自己的域名,那么下面的 SSL Certificate 就不建议使用默认的 Default CloudFront Certificate (*.cloudfront.net),因为很多浏览器和客户端会发现证书的域名和图片 CDN 的域名不一致会报警告。

其它项保持默认,点击 Create Distribution 按钮,然后回到 CloudFront Distributions 列表,这里刚刚创建的记录 Status 会显示为 In Progress,我们点击 ID 的链接,前进到详情页,可以看到 Domain Name 显示一个 CloudFront 分发的 URL,比如 cloudfronttest.cloudfront.net。大约 10 多分钟后,等待 Distribution Status 变成 Deployed,我们可以用上述域名来测试一下。注意测试用的 URL 不要包含 API Gateway 的 Stage 名,比如

https://1234567890.execute-api.us-west-2.amazonaws.com/test/awslogo_w300.png

那么 CloudFront 的 URL 应该是

https://cloudfronttest.cloudfront.net/awslogo_w300.png

尽管我们已经配置了自己的域名,但是这时自已的域名还未生效。我们还需要到 Route 53 去添加域名解析。

2.6 Route 53

最后我们使用 Route 53 实现自定义域名关联 CloudFront 分发。访问 Route 53 管制台

https://console.aws.amazon.com/route53/home?region=us-west-2

在 Hosted Zone 下点击一个域名,在域名列表页,点击上方 Create Record Set 按钮,页面右侧会弹出创建记录集的面板。

Name 栏输入 img。

Type 保持默认 A – IP4 Address 不变。

Alias 点选 Yes,在 Alias Target 输入前述创建的 CloudFront 分发的 URL cloudfronttest.cloudfront.net。

点击右下角 Create 按钮完成创建。

3. 效果验证

现在我们回到 CloudFront 控制台,等到我们的 Distribution 的 Status 变成 Deployed,先用 CloudFront 自身的域名访问一下。

https://cloudfronttest.cloudfront.net/awslogo_w300.png

顺利的话,会看到咱们的范例图片。再以自定义域名访问一下。

http://img.myexample.com/awslogo_w300.png

还是输出这张图片,那么到此就全部部署成功了。现在可以在 S3 的 bucket 里上传更多其它图片,比如 abc.png,然后访问时使用的 URL 就是

http://img.myexample.com/abc_w300.png

用浏览器打开调试工具,可以看到响应头里已经有

X-Cache: Hit from cloudfront

表示已经经过 CloudFront 缓存。

4. 监控

这个架构方案使用的服务都可以通过 CloudTrail 记录管理行为,使用 CloudWatch 记录用户访问情况。

4.1 Lambda 监控

在 Lambda 控制台点击我们的 ImageMagick 函数,然后点击选项卡最末一个 Monitoring,可以看到常用指标的简易图表。

深入Serverless—让Lambda 和 API Gateway支持二进制数据

点击任何一个图表,都可以前进到 CloudWatch 相关指标的指标详细页。然后我们还可以为各个指标配置相关的 CloudWatch Alarm,用以监控和报警。

点击 View logs in CloudWatch 链接,可以前往 CloudWatch Log,这里记录了这个 Lambda 函数每次执行的详细信息,包括我们的函数中自已输出的调试信息,方便我们排查问题。

4.2 API Gateway 监控

在 API Gateway 控制台找到我们的 API ImageMagick,点击它下面的 Dashboard。

深入Serverless—让Lambda 和 API Gateway支持二进制数据

如果部署了多个 Stage,注意左上角 Stage 菜单要选择相应的 Stage。同样下面展示的是常用图表,点击每个图表也可以前往 CloudWatch 显示指标监控详情。

4.3 CloudFront 日志

我们刚刚配置 CloudFront 时没有启用日志。如果需要日志,可以来到 CloudFront 控制台,点击我们刚刚创建的分发,在 General 选项页点击 Edit 按钮。在 Edit Distribution 页找到 Logging 项,选择 On,然后再填写 Bucket for Logs 和 Log Prefix,这样 CloudFront 的访问日志就会以文件形式存储在相应的 S3 的 bucket 里了。

5. 小结

我们这样一个例子使用了 Lambda 和 API Gateway 的一些高级功能,并串联了一系列 AWS 全托管的服务,演示了一个无服务器架构的典型场景。虽然实现的功能比较简单,但是 Lambda 函数可以继续扩展,提供更丰富功能,比如截图、增加水印、定制文本等,几乎满足任何的业务需求。相比传统的的计算能力部署,不论是用 EC2 还是 ECS 容器,都要自己管理扩容,而使用 Lambda 无需管理扩容,只管运行代码。能够让我们从繁琐的重复工作中解脱,而把业务集中到业务开发上,这正是无服务器架构的真正理念和优势。

作者介绍:

深入Serverless—让Lambda 和 API Gateway支持二进制数据

薛峰

AWS 解决方案架构师,AWS 的云计算方案架构的咨询和设计,同时致力于 AWS 云服务在国内和全球的应用和推广,在大规模并发应用架构、移动应用以及无服务器架构等方面有丰富的实践经验。在加入 AWS 之前曾长期从事互联网应用开发,先后在新浪、唯品会等公司担任架构师、技术总监等职位。对跨平台多终端的互联网应用架构和方案有深入的研究。

本文转载自 AWS 技术博客。

原文链接:
https://amazonaws-china.com/cn/blogs/china/deep-dive-in-serverless-lambda-and-api-gateway-support-binary-data/

欲了解 AWS 的更多信息,请访问【AWS 技术专区】

评论

发布