- 智能时代的新技术实战
- InfoQ中文站
- 5056字
- 2020-06-26 06:03:52
深入Serverless—让Lambda和API Gateway支持二进制数据
by AWS Team
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
,
Actions下拉选中GetObjext,
Amazon Resource Name(ARN)填写刚刚创建的bucket的ARN,比如
然后点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); } }); };
注意一定要把
varbucketN am e=‘im age201702';
改成刚刚创建的bucket名字,如
varbucketN am e=‘im g201703';
这个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 配置
点击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<br/>after transformations: [Binary Data]
Thu Mar 09 03:40:11 UTC 2017: Method response<br/>headers:
{X-Amzn-Trace-Id=Root=1-12345678-1234567890abcdefghijlkln, <br/>
Content-Type=image/png}
Thu Mar 09 03:40:11 UTC 2017: Successfully completed<br/>execution
Thu Mar 09 03:40:11 UTC 2017: Method completed with<br/>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之前曾长期从事互联网应用开发,先后在新浪、唯品会等公司担任架构师、技术总监等职位。对跨平台多终端的互联网应用架构和方案有深入的研究。