亚马逊AWS官方博客

利用 CloudFront Lambda@Edge 进行事务处理–按需生成缩略图并缓存

按需动态生成原始图片的缩略图,而不是预先全量生成所有原始图片的缩略图是一种普遍的需求。在AWS上,已提供多种方法来实现此需求,比如以下一些blog资源可以参考:

  1. 基于S3的图片处理服务
  2. Resize Images on the Fly with Amazon S3, AWS Lambda, and Amazon API Gateway
  3. Serverless Image Handler

其中,第一种方式和第第二种方式,利用S3捕获文件不存在的异常,将客户端的访问请求进行重定向到能进行缩略图生成的计算资源来获得所需要的缩略图,图像处理的逻辑可以是在EC2实例中运行,也可以在无服务器模式的Lambda函数中运行,两种模式本质是一样的。此种模式对客户端程序有一个重定向的操作,客户端将需要再次发起访问。

第三种方式是提供一个专用的图片处理API,后端再接入负责图片处理的Lambda函数,所有的缩略图生成需求统一发送到此API获取需要的缩略图。此种模式要求客户端代码中变更对缩略的HTTP请求编写方式。

以上几种方式存在一定程度的不足:

  1. 第一种和第二种实现方式中,需要一个重定向操作来将获取缩略图的请求重定向到生成图片的位置
    • 要求客户端能处理重定向,对浏览器客户端来说不存在问题,但对如手机APP程序来说,需要显式支持重定向的处理
    • 需要增加额外一次请求才能获取到图像,增加了请求的处理时间
    • 通过静态配置于S3的异常处理规则生成固定的重定向请求,不能生成不同规格的图像处理请求实现不同的图片处理需求
  2. 第三种的实现方式中,提供了一个专门用于图片处理的API
    • 需要修改代码中缩略图的获取方式,将所有缩略图的静态URL全部修改为通过指定API去生成,有额外的代码修改工作量

本文将探讨一种新的模式,利用Cloudfront Lambda@Edged来在线捕获缩略图请求并生成缩略图返回。在此模式下,用户不需要修改缩略图的获取方式,仍然以原始静态URL的方式获取缩略图,并在原始处理请求中生成缩略图返回,不会产生重定向操作,避免客户端对重定向的处理,同时可以在离用户更近的位置进行请求处理并缓存结果,能提高响应性能。

本文将描述在Cloudfront中动态生成缩略,缓存在Cloudfront中,并直接从Cloudfront返回给客户端的方案以及实现过程,同时可以选择是否回传到S3作进一步保存。在后续到达相同Cloudfront节点的请求将使用缓存在Cloudfront上的文件直接返回给客户端,不需要再次生成(除非缓存过期)。

本文将分为以下几个部分进行描述:

  • CloudFront的边缘计算能力Lambda@Edge介绍
  • 利用Lambda@Edge在线生成缩略图的设计思想
  • 利用Lambda@Edge在线生成缩略图的参考代码实现
  • 整套方案在AWS上的方案部署过程
  • 测试方法

CloudFront的边缘计算能力Lambda@Edge

Lambda是AWS的一种计算服务,运行在AWS的Region内,Lambda@Edge是一个AWS Lambda扩展,可以部署到AWS的CDN服务Cloudfront服务节点上,用于执行函数以自定义CloudFront提供的内容。AWS可以将执行代码部署到AWS全球的Cloudfront节点,而无需预置或管理服务器。Lambda@Edge会自动扩展,从每天几个请求到每秒数千个请求。在与客户端位置较近的AWS Cloudfront节点上(而不是源服务器) 上处理请求,可显著减少延迟并改善用户体验。

除了用于在线生成缩略图场景处,Lambda@Edge还可以用于更多的场景,如:

  • 通过为响应添加安全标头,提高用户和内容提供商的安全性。
  • 在离用户最近的位置建立Web应用程序。
  • 为用户和搜索引擎返回不同的内容,从而优化搜索体验。
  • 将不同的用户请求路由到不同的源站点。
  • 在边缘位置阻止爬虫和机器人程序进入源站点。
  • 将请求导入到不同版本的站点,进行A/B测试。

Lambda@Edge函数执行触发点

Cloudfron作为客户端的源服务之间的中间节点,提供了4种执行Lmabda@Edge函数的时机,分别如图所示

  1. Viewer request:刚从客户端收到请求时触发函数的执行,此时还未检查对象在Cloudfront中的缓存,并且可以从触发的函数中直接返回结果到客户端。此处Cloudfront不会缓存客户端请求的对象。
  2. Origin request:先执行对缓存对象的检查,如果缓存对象的检查已经存在于Cloudfront中,则此事件不会触发函数的执行。如果请求的对象不在Cloudfront中,则会触发函数执行。可以在执行的函数被直接返回response给客户端,也可以在执行过后,继续转到请求到源端(Origin Server)。
  3. Origin response:收到来自Origin Server的响应,在将对象缓存到Cloudfront之前,该函数会执行,即使返回了错误,该函数仍然会执行。但如果请求的对象在Cloudfront中,或者是从Origin request生成的response,则此处的函数不会被执行。
  4. Viewer response:请求在返回到客户端之前,该函数会执行,无论请求的对象是否已经在Cloudfront缓存中,此函数都会被执行,但结果不会被Cloudfront缓存。以下情况中函数不会被执行:
    • 当源返回400或更高的HTTP状态码时。
    • 当返回自定义错误页面。
    • 此响应是在Viewer request中生成的。
    • 当Cloudfront将http请求重定向到https请求时。
      可以根据实际需求,通过以上四个Lambda@Edge函数触发点中的一个或者多个来插入需要的业务逻辑,从而满足不同的业务需求。

说明
在 CloudFront 事件触发执行 Lambda 函数时,必须先完成该函数,然后 CloudFront 才能继续操作。例如,如果 CloudFront Viewer request事件触发的 Lambda 函数完成运行之前,CloudFront 不会将respone返回到客户端或将请求转发到源服务。

按需要生成缩略图实现过程

用于生成缩略图的函数执行点选择

在以上四个触发点中,Viewer request是进入Cloudfront之前发生,Viewer response是离开Cloudfront之后发生,两者的结果都不会被缓存到Cloudfront中。在按需生成缩略图的场景中,生成的缩略图我们希望缓存在Cloudfront中以加快下次的访问速率,同时也避免再次重新生成缩略中(除非对象在缓存中失效),所以缩略图生成的触发点将在Origin request中和Origin response中选择。
对于Origin request中和Origin response,分别适用于以下场景:

  1. Origin request中产生的响应大小不能超过1MB,所以适用于生成的缩略图小于1MB的场景
  2. Origin Response中产生的响应大小没有限制,可用于生成的缩略图小于1MB的场景,也可用于生成的缩略图大于1MB的场景

在本文中提供的代码实现,可以无缝运行于两个触发点的Lambda@Edge函数之中,用户可以按需要将其部署在Origin request或者Origin Response触发点。


参考代码实现

以下部分介绍示例代码的实现逻辑,用户可以参考代码中对http/https的请求和响应处理方法,来实现自己的业务处理逻辑,不局限于用于图片处理这一种场景。

在Origin Request触发点进行缩略处理

在Origin request函数中生成缩略图中流程如下:
1. 用户发起缩略图获取请求。
2. 如果Cloudfront中没有缓存缩略图,则从源S3中取得原始图像,进行缩略图生成,并生成请求响应,并自动缓存在Cloudfront中。
3. 将请求响应返回给用户,整个流程结束。

使用相同Cloudfront区域的后续的请求直接从Cloudfront缓存中取得缩略中,不需要再次生成(除非对象在缓存中失效)。
引触发点生成的响应正文部分最大为1MB,也就是生成的缩略图大小不能超过1MB。

在Origin Response触发点进行缩略处理

在Origin response函数中生成缩略图流程如下:
1. 用户发起缩略图获取请求。
2. 如果在Cloudfront中未缓存该缩略图,请求会被转发给源可S3。如果有缓存,则直接返回给用户。
3. 如果源中没有缩略图文件,所以会返回404错误代码。
4. Lambda@Edge检查到源的错误响应,生成缩略图。可选地,将缩略图回传到源S3,为其他Cloudfront请求提供缩略图,其他Cloudfront上将不会再次生成缩略图。
5. 将响应返回给客户端,并自动缓存到Cloudfront中。

在第2步中,生成的缩略图可以回传到S3中,后续使用其他Cloudfront区域的客户端可以直接从S3中取得缩略图,不需要再重新生成,
可以在保存缩略图的S3桶中设置生命周期规则自动删除过期缩略图,避免长期占用空间。

变量定义

Lambda@Edge和标准Lambda函数不一样,其不接受环境变量,所以在函数中进行和配置环境相关的变量定义


bucketName 定义原始文件所在的S3 bucket,如果缩略图被回传回S3,也作为缩略图所在bucket
rawDir 定义原始文件在S3 bucket中的目录路径
PUT_TO_S3 定义在Origin respone触发点进行缩略图生成的情况下,是否将文件回传回S3
Cloudfront_Origin_Path 如果缩略图要回传回S3,定义其在bucket中的起始位置,此参数取决于cloudfront中指定缩略图所属Origin Domain的Origin path参数

入口主函数lambda_handler定义

Lambda代码的主函数默认名称为lambda_handler(可以更改),在此函数中定义了主要的处理逻辑。

参数event包含了用户请求的完整信息,此函数主要就是解析event中的信息从而决定如何处理请求。如果是Origin respone触发的处理函数,event中还包含从源端返回的原始respone响应。event中包含的完整结构体数据可参考Lambda@Edge中的事件结构

用户请求需要的缩略图,设定的请求格式为 http://example.com/path/thumbnail-file?image=Origin-file&size=200×200

  1. 首先从查询字符串中使用image参数取得原始文件名,使用size取得需要的缩略图大小。
  2. 如果是Origin request触发的,则生成缩略图,并直接返回给客户端,并自动缓存在Cloudfront中。
  3. 如果是Origin respone触发的,如果Origin的响应表示正常,则直接返回原始响应,不作处理,否则生成缩略替换原始响应并返回给客户端,并自动缓存在Cloudfront中。并在PUTTOS3为TRUE时,回传文件回S3中。
缩略图生成函数resizes3image

本示例利用Pillow库来生处理图像大小转换,用户可以灵活采用自己选择的库来进行图像处理。
Pillow库是强大的Python图像处理库,包括:

  • 支持多种格式的图片,包括:BMP, DIB, EPS, GIF, ICNS, ICO, IM, JPEG, JPEG 2000, MSP, PCX, PNG, PPM, SGI, SPIDER, TGA, TIFF, WEBP, XBM
  • 图像存储功能:适合图像归档和图像批量处理,你可以使用它建立缩略图,转换格式,打印图片等
  • 图像显示:包含PhotoImage和BitmapImage以及Windows DIB Interface
  • 图像处理:包括点操作,使用内置卷积内核过滤,色彩空间转换。还支持更改图像大小、旋转、任意仿射变换
生成响应 generate_response

如果是Origin reqeust触发的函数执行,则从头构造一个响应结构体,如果是origin response触发的函数执行,则修改响应中的返回值。并将生成的缩略图作为body值返回到客户端。

由于图片文件是二进制数据,而Json不能序列化二进制数据,所以使用base64将图片的二进制数据进行encode,并添加Content-Encoding指示,在客户端收到后进行decode从而得到图片数据进行处理。

import json
import boto3
import PIL
from PIL import Image
from io import BytesIO
import os
from urllib.parse import parse_qs
import base64

# All the buketname and dir name should be lowercase to meet the s3 naming requirements.
bucketName = 'originalbucket'
rawDir = 'originaldir'
Cloudfront_Origin_Path='/output1/output2/'
PUT_TO_S3='FALSE'
   
def lambda_handler(event, context):
    # Get object
    cf = event['Records'][0]['cf']
    request = cf['request']
    params = {k: v[0] for k, v in parse_qs(request['querystring']).items()}
    image = params.get('image')
    size = params.get('size')
    rawObject = "{dir}/{key}".format(dir=rawDir, key=image)
    response = ''

#
    if 'response' in cf:
        trigger_point = 'RESPONSE'
    else:
        trigger_point = 'REQUEST'

    if trigger_point == 'REQUEST':
        buffer = resize_s3_image(bucketName, rawObject, size)
# If triggerd by Original Rqeust, you may want to conitune forward to original server, so you should upload the file back to s3 then return request.
# For example, if your client does not accept base64 encoded picture files, you need to do so, In this case you can't use the original response trigger
#        return request
# Otherwise, return respone to viewer directly like flow:    
        return generate_response(response, buffer, trigger_point)

    if trigger_point == 'RESPONSE': 
        response = cf['response']
        if int(response['status'])>= 400 and int(response['status']) <= 599:
            buffer = resize_s3_image(bucketName, rawObject, size)
# You can decide whether to send the file back to s3 so that subsequent get operations can get the existing file directly . 
            if PUT_TO_S3 == 'TRUE':
                uri = request['uri']
                newObject = "{rootpath}{ouri}".format(rootpath=Cloudfront_Origin_Path.strip('/'), ouri = uri)
                upload_to_s3(bucketName, newObject, buffer)       
            return generate_response(response, buffer, trigger_point)
        else:
            return response
    


def resize_s3_image(bucket_name, objectKey, size):
    size_split = size.split('x')
    s3 = boto3.resource('s3')
    obj = s3.Object(
        bucket_name = bucket_name,
        key = objectKey,
    )
    obj_body = obj.get()['Body'].read()

    img = Image.open(BytesIO(obj_body))
    img = img.resize((int(size_split[0]),int(size_split[1])),PIL.Image.ANTIALIAS)

    buffer = BytesIO()
    img.save(buffer,'png')
    buffer.seek(0)
    return buffer

def upload_to_s3(bucket_name, objectKey, buffer):
    # Upload the generated objcect to s3
    s3 = boto3.resource('s3')
    obj = s3.Object(
        bucket_name = bucket_name,
        key = objectKey,
    )
    obj.put(Body=buffer,ContentType='image/png')
    
    # Construct Resopne.
def generate_response(response, buffer, trigger_point):
    if trigger_point == 'REQUEST':     
        response = {
            'status': '',
            'statusDescription': '',
            'headers': {
                'cache-control': [
                    {
                        'key': 'Cache-Control',
                        'value': 'max-age=100'
                    }
                ],
                "content-type": [
                    {
                        'key': 'Content-Type',
                        'value': 'image/png'
                    }
                ],
                'content-encoding': [
                    {
                        'key': 'Content-Encoding',
                        'value': 'base64'
                    }
                ]
            },
            'body': '',
            'bodyEncoding': 'base64'
        }

    response['status'] = '200'
#Json cannot serialize binary picture files, so first force-code them with base64 into text files that json thinks can be serialized, and then reverse-code at the browser end
    buffer.seek(0)
    response['body'] = base64.b64encode(buffer.read()).decode()
    response['bodyEncoding'] = 'base64'
    response['headers']['content-type'] = [{'key': 'Content-Type', 'value': 'image/png'}]
    response['headers']['content-encoding'] = [{'key': 'Content-Encoding', 'value': 'base64'}]

    if trigger_point == 'REQUEST':     
        response['statusDescription'] = 'Generated by CloudFront Original Request Function'
    if trigger_point == 'RESPONSE':     
        response['statusDescription'] = 'Generated by CloudFront Original Response Function'

    return response

 

在AWS上配置缩略图处理环境

本方案中采用的AWS服务包括:cloudfront,Lambda,S3,以下内容就各服务配置进行说明。当文件源不为S3时,此方案仍然适用。

S3配置说明

S3配置包括:

  1. 创建包含原始文件的存储桶,并建立好相应的路径。此两个值对应代码中的bucketName和rawDir变量
  2. 如果要将缩略图上传到S3,则建立缩略图在bucketName所指定的桶的起始路径,对应到代码中的CloudfrontOriginPath,此值同时也要配置到Cloudfornt中。同时也配置bucket中缩略图文件的生命周期管理规则,避免缩略图一直占用S3空间。
Cloudfront配置说明

Cloudfront的详细配置请参考官方手册

  1. 创建一个Distribution(分配),创建的分配ID为 E37FGMQAN71NQD.
  2. 在Distribution(分配)中创建一个新的源,如果没有Distribution,请先新建。
  3. 源域名选择前面创建的S3 bucket,源路径值为桶中存放缩略图的起始位置,对应到代码中的CloudfrontOriginPath。其余配置如图所示:
  4. 创建一个新的Behaviors(行为),一个Behavior代表了匹配路径下的回源规则
  5. 配置Behaviors(行为),Path Pattern(路径模式)为URL匹配这条Behavior的通配符,Origin选择为第2步中创建的源,以实现URL请求路径如果匹配Path Pattern就回源到选择的Origin的目的。Object Caching(对象缓存)设置为自定义,其值可以根据需要指定,Query String Forwarding and Caching(查询字符串转发与缓存)设置为”全部转发,基于全部进行缓存”,Lambda函数依赖于此值进行缩略图处理。其余值保持默认。
Lambda函数配置说明
  1. 在IAM服务中,为Lambda函数建立运行的Role,Policy包含对原始文件所在目录的读取权限,如果要上传缩略图到S3,则policy还要包含对缩略图所在路径的定入权限。角色的信任主体设置为lambda以及edgelambda,如图
  2. 打包python代码及Pillow库文件,并上传到S3中,用于Lambda函数的创建


$ ls
cloudfront-lambda-s3-picture.py
$ unzip ../Pillow-7.0.0-cp37-cp37m-manylinux1_x86_64.whl
$ zip cloudfront-lambda-s3-picture.zip -r .
$ aws s3 cp cloudfront-lambda-s3-picture.zip s3://xxx-bucket/ --profile virginia #上传到位于Virginia的桶中

  1. 在Lambda服务中建立函数,Lambda服务所在region选择且必须选择为Virginia,运行时选择为Python3.7,调整函数运行时间为30 sec.
  2. 在代码输入种类中选择从S3上传文件。
  3. 更新处理程序入口地址为”cloudfront-lambda-s3-picture.lambdahandler”,其中cloudfront-lambda-s3-picture为代码文件名,lambahandler是代码中的入口函数名称。
  4. 保存更改,并测试成功后,在Lambda的操作选项中,选择部署到Lambda@Edge.
  5. 在弹出的配置框中,分别指定前面创建的Distribution(配置)为前面创建的E37FGMQAN71NQD及,选择前面创建的路径匹配Behavior(行为)“/*.png”,选择CloudFront事件为源请求,代表从Origin Request中触发此函数。如果要从Origin Response中触发此函数,需要选择源响应。
  6. AWS将函数部署到Cloudfront节点上,在Cloudfront对应的Distribution(分配)中,查看部署状态,状态变为完成后,即可使用客户端进行测试。

使用客户端测试

推荐使用postman构建http的get请求:
xxx.cloudfront.net/Person-2-new.png?image=Person.png&size=200x200

  • xxx.cloudfront.net为域名。
  • Person-2-new.png为需要动态生成的缩略图名称,如果要回传到S3,则此值是S3的文件名。
  • image指定在rawDir路径下的原始文件名。
  • size指定缩略图的大小,长宽使用’x’连接。
  • 如果回传到S3中,文件的具体位置将在S3://${bucketName}/${cloudfrontOriginPath}/${URI},URI值来源于请求中的文件路径。

由于生成的图片缓存于Cloudfront中,再次获取此缩略图时,只用270ms即可取得缩略图。


总结

利用此种方式实现按需要生成缩略图有以下几方面的特点

  • 对用户透明,用户只需要按照正常的文件请求方式进行缩略图的请求,并可灵活指定缩略图大小。
  • 就近在离用户最近的Cloudfront节点中生成缩略图,直接返回给客户端。
  • 直接缓存于Cloudfront中,后续对相同缩略图的请求则不需要重新生成新的缩略图。

Lambda@Edge在使用过程中遇到的问题和调方式法可以参考Blog:浅谈AWS CloudFront在特殊场景下的配置和错误处理


附加参考资料

  1. 使用 Lambda@Edge 在边缘站点自定义内容
  2. Lambda@Edge 示例函数
  3. Lambda@Edge 中传递的事件结构体
  4. Lambda@Edge Design Best Practices
  5. 浅谈AWS CloudFront在特殊场景下的配置和错误处理
  6. Pillow项目介绍

本篇作者

岳亮

AWS中国区解决方案架构师,致力于为客户提供基于AWS云平台的解决方案,同时对私有云及开源也有所涉及,期望与同行互相交流。