亚马逊AWS官方博客

深入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可以响应输出二进制的图片数据流。

总体架构图如下:

主要技术点:

  • 涉及服务都是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是当前测试执行的输出。

可以看到这里 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’)”}

完成的效果如下图所示:

最后点击“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,可以看到常用指标的简易图表。

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

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

4.2 API Gateway 监控

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

如果部署了多个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无需管理扩容,只管运行代码。能够让我们从繁琐的重复工作中解脱,而把业务集中到业务开发上,这正是无服务器架构的真正理念和优势。

作者介绍:

薛峰

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