亚马逊AWS官方博客

使用 Amazon CloudFront + Amazon S3 + AWS Lambda@Edge 动态调用业务接口生成图片

本方案通过与 Amazon CloudFront Origin-Response 和 AWS Lambda@Edge 函数相结合,当 Amazon CloudFront 缓存文件不存在回源 Amazon S3 时,Amazon S3 桶中文件不存在,AWS Lambda@Edge 中部署的 AWS Lambda 函数根据 Amazon S3 请求返回状态码,调用图片生成接口生成图片,AWS Lambda 函数获取业务接口生成的图片后,上传图片到 Amazon S3 桶,然后返回图片到 Amazon CloudFront 最终呈现给用户。

本方案主要解决在 Global 区域从其他云平台上的对象存储服务 OSS(Object Storage Service)迁移回源请求转发场景,Amazon CloudFront+AWS Lambda@Edge+ Amazon S3 相结合,现有业务迁移到亚马逊云科技,不修改原有代码的情况下,轻松实现回源文件不存在时,调用业务接口生成文件,Serverless 架构,无需维护基础资源,自动根据业务弹性扩缩容。

需求背景

客户业务需要当请求者访问 Bucket 中不存在的文件(Object)时,根据回源规则指定的源站获取该文件。获取到目标文件后,会将文件返回给请求者并保存到 Bucket 中。

流程如下:

因 S3 不能直接配置外部接口调用(业务接口程序),需要通过 CloudFront+Lambda@Edge 方式来实现该功能。

解决方案架构

具体架构如下:

Lambda@EdgeAmazon CloudFront 的一个功能,它可让您在靠近应用程序用户的地方运行代码,从而提高性能,降低延迟。使用 Lambda@Edge,您无需在全球多个地方预置或管理基础设施。您只需按使用的计算时间付费 — 代码未运行时不产生费用。

使用 Lambda@Edge,您可以将 Web 应用程序分布在全球并提高它们的性能(并且无需管理任何服务器),从而丰富您的 Web 应用程序。Lambda@Edge 根据 Amazon CloudFront 内容分发网络(CDN)生成的事件运行代码。您只需将代码上传到亚马逊云科技 Lambda 无服务器计算环境,后者将在靠近最终用户的亚马逊云科技边缘站点完成运行和扩展代码所需的一切操作,从而实现高可用性。

对于 CloudFront 分配中的每个缓存行为,您最多可添加四个触发器(关联),以便在发生特定 CloudFront 事件时触发 Lambda 函数执行。CloudFront 触发器可以基于四个 CloudFront 事件之一,如下图所示。

可用于触发 Lambda@Edge 函数的 CloudFront 事件如下:

  • 查看器请求:当 CloudFront 收到查看器的请求时及它检查请求的对象是否在 CloudFront 缓存中之前,该函数会执行。
  • 源请求:仅当 CloudFront 将请求转发给您的源时,该函数才会执行。当请求的对象位于 CloudFront 缓存中时,该函数不会执行。
  • 源响应:在 CloudFront 收到来自源的响应之后及它将对象缓存在响应中之前,该函数会执行。请注意,即使从源返回了错误,该函数仍会执行。
    在以下情况下该函数不会执行:

    • 当请求的文件位于 CloudFront 缓存中并且未过期时;
    • 当从由源请求事件触发的函数中生成响应时。
  • 查看器响应:在将请求的文件返回到查看器之前,该函数会执行。请注意,无论文件是否已在 CloudFront 缓存中,该函数都会执行。

Lambda 是一种无服务器的计算服务,让您无需预置或管理服务器、创建可感知工作负载的集群扩展逻辑、维护事件集成或管理运行时,即可运行代码。借助 Lambda,您几乎可以为任何类型的应用程序或后端服务运行代码,而且完全无需管理。只需将您的代码以 ZIP 文件或容器映像的形式上传,Lambda 便会自动、精确地分配计算执行能力,并根据传入的请求或事件运行您的代码,以适应任何规模的流量。您可以将您的代码设置为自动从 200 多个亚马逊云科技服务和 SaaS 应用程序触发,或者直接从任何 Web 或移动应用程序调用。您可以使用自己喜欢的语言(Node.js、Python、Go、Java 等)编写 Lambda 函数,并使用无服务器和容器工具(例如 SAM 或 Docker CLI)来构建、测试和部署您的函数。

实施步骤

Lambda 函数需要部署在 us-east-1,因为要发送至 CloudFront Edge 只有 us-east-1 区域支持这项操作。

步骤 1. 新建 S3 桶 chivas-test-1

可以通过 web 控制台、CLI、SDK 等方式创建存储桶。

步骤 2. 编辑存储桶策略

{
   "Version":"2012-10-17",
   "Statement":[
      {
         "Principal":{
            "AWS":"*"
         },
         "Effect":"Allow",
         "Action":[
            "s3:GetObject"
         ],
         "Resource":"arn:aws:s3:::chivas-test-1/*"}
   ]
}

如下图所示:

步骤 3. 创建 CloudFront 分配

创建分配,如图所示:

缓存行为配置,如下图所示:

关联函数配置,如下图所示:

其他配置完成后,点击创建分配按钮,完成分配创建。

步骤 4. 创建 Lambda 函数

Lambda 函数采用 Node.js,代码如下:

index.js
const querystring = require("querystring");
const httpreq = require("request");
const AWS = require("aws-sdk");
const S3 = new AWS.S3({
  signatureVersion: "v4"
});
const bucketName = " chivas-test-1";
const s3Basekey = "api/image/";
const backendAddress = "https://xxxxxxxx.com";
exports.handler = (event, context, callback) => {
  let response = event.Records[0].cf.response;
  if (response.status != 403) {
    callback(null, response);
    return;
  }
  let request = event.Records[0].cf.request;
  //console.log('event', JSON.stringify(event))
  let path = request.uri;
  let key = decodeURIComponent(s3Basekey+path.substring(1));
  console.log({ key });
  httpreq({
    url: backendAddress + path,
    method: 'GET',
    encoding: null
  }, (err, getRespone, body) => {
    if (!err && getRespone.statusCode === 200) {
      const contentType = getRespone.headers['content-type']
      S3.putObject({
        Body: body,
        Bucket: bucketName,
        ContentType: contentType,
        Key: key
      }, (err, data) => {
        if (err) {
          console.log(err, err.stack);
        }
        else {
          response.status = 200;
          response.body = body.toString("base64");
          response.bodyEncoding = "base64";
          if (contentType)
            response.headers["content-type"] = [{ key: "Content-Type", value: contentType }];
        }
      })
    } else {
      console.log('download error url', backendAddress + path);
    }
  });
  callback(null, response);
};

上述代码,中

const bucketName = “xxxxx”;

const backendAddress = “https://xxxxxxxx.com”;

修改成自己业务中的存储桶名和业务接口地址。

创建函数,如下图所示:

上传代码,也可以直接在 Lambda 控制台编写代码,本文采用上传本地代码方式:

上传完成,并发布代码:

配置角色权限:

配置角色策略:

{
    "Version": "2012-10-17",
    "Statement": [
        {
            "Effect": "Allow",
            "Principal": {
                "Service": [
                    "lambda.amazonaws.com",
                    "edgelambda.amazonaws.com"
                ]
            },
            "Action": "sts:AssumeRole"
        }
    ]
}

配置完成后,如下图所示:

添加 Lambda 触发器:

部署到 Lambda@Edge:

部署完成,查看版本信息,如下图所示:

部署完成后测试,当 CloudFront 回源到 S3 图片不存在时,Lambda@Edge 会根据 S3 返回状态码进行业务接口调用,业务接口返回图片后,Lambda@Edge 上传 S3,返回图片到请求端,如下图所示:

总结

使用 Lambda@Edge 动态调用业务接口生成图片的两个主要好处是:

  1. CloudFront+Lambda@Edge+S3 相结合,现有业务迁移到亚马逊云科技,不修改原有代码的情况下,轻松实现业务目标。
  2. 通过 Lambda 无服务器架构来简化基础架构。

参考文档

https://aws.amazon.com/cn/cloudfront/getting-started/

https://docs.aws.amazon.com/zh_cn/lambda/latest/dg/lambda-edge.html

https://aws.amazon.com/cn/blogs/china/automatically-create-and-update-lambda-edge-in-cloudfront/

https://aws.amazon.com/cn/blogs/networking-and-content-delivery/resizing-images-with-amazon-cloudfront-lambdaedge-aws-cdn-blog/

https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/lambda-examples.html

https://docs.aws.amazon.com/zh_cn/AmazonCloudFront/latest/DeveloperGuide/lambda-edge-how-it-works-tutorial.html

本篇作者

李龙

亚马逊云科技解决方案架构师,负责基于亚马逊云科技的解决方案咨询和设计,Serverless TFC 成员。特别关注教育和媒体领域的云服务和 AI 运用。

任耀洲

亚马逊云科技解决方案架构师,负责企业客户应用在亚马逊云科技的架构咨询和设计。在微服务架构设计、数据库等领域有丰富的经验。