深入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之前曾长期从事互联网应用开发,先后在新浪、唯品会等公司担任架构师、技术总监等职位。对跨平台多终端的互联网应用架构和方案有深入的研究。