Github
提供了 docker 环境和简单的图片处理代码;欢迎 star、fork 或提交 pr 以改善该实践。仓库:点我
这是什么
类似阿里云 OSS 的图片处理服务、腾讯云的数据万象。
因为 AWS 并没有单独将图片处理单独发布服务,只提供了 lambda 配合 CloudFront 用于处理这类需求。因此我设计了一套方案用于实现图片处理的需求,本文档包含使用说明、设计方案。
有了本方案,前端可以在原有的 cdn 图片链接后面拼接参数,获取对应的缩略图、加水印等图片处理需求。
本方案涉及到的几个角色
CloudFront
Amazon 的 CDN 服务,在本方案用于缓存缩略图。下面简称 CF
Lambda@edge
Lambda 是 Amazon 的云函数,就是 serverless 那一套。
Lambda@edge 则是可以运行在 CloudFront 边缘的函数,比 Lambda 支持的能力要弱一些。例如 runtime 只支持 Node.js 和 Python;不支持 Layer,导致使用的图片库无法部署在 layer,只能全部打包到源码。
对图片的处理逻辑在这一层运行,是本方案的重点对象。下面简称 Lambda
S3
Amazon 的对象存储服务,类似阿里的 OSS。在本方案扮演 存储原图、缩略图;cdn 回源的角色
工作流程
注意:只有在 CF 行为规则配置了规则(例如 .jpg@ ),才会触发 lambda。直接访问原图的不经过 lambda,不必担心过度计费问题。
请参考上图,流程为:
- 步骤 0:用户请求图片链接:链接形如:http://cdn.com/image/abc.jpg@250w_60q.webp。其中 http://cdn.com/image/abc.jpg 是原图链接,@后面是处理参数,具体含义请参看使用手册
- 步骤 1:请求先到达 CF,检查 CF 有缓存则直接返回给用户,并且 etag 与客户端一致时响应 304 Not Modified
- 步骤 2:若 CF 未命中,则回源到 S3 获取图片。
- 步骤 3:S3 响应后,CF 事件会触发给 lambda,函数逻辑会对 response 进行判断:
- 如果 http code 为正常值,说明缩略图在 S3 已存在,那么不进行其它处理,直接将 response 返回
- 步骤 5:如果 http code 为异常值,说明 S3 没有该图,我们需要做以下处理:
- 将 URI 从 @开始剥离,前面的为原图 KEY,后面的为处理参数。我们使用原图 KEY 从 S3 桶(CF 事件的 request 链接可以提取到桶名)获取原图,然后使用处理参数对图片进行处理。这里使用 sharp 图片处理库 (底层使用 libvips,比 ImageMagic 性能要好)。
- 将处理好的图片存储到 S3,以便下次 CF 失效时,直接回源到 S3 获取。这里存储到 S3 时,给图片打了标签(createByLambda:1),方便在 S3 根据标签制定淘汰规则,避免过多缩略图长期存在 S3 造成浪费。
- 将图片以 base64 编码返回到 CF,content-encoding 为 base64。这里之所以使用 base64 是因为 lambda 只支持接收 json
- 步骤 4:CF 将源头的图片存储起来并响应给用户
前端使用说明
url 构成:https://cdn 域名 / 文件名 @<参数值 1>< 参数名 1>_< 参数值 2>< 参数名 2>. 期望转换的文件格式
例子:https://d1xxxxxxxx.cloudfront.net/foamzou/image/4951f0e35a37e5190e78798dcfcad984.jpg@1020w_160h_0e_1l_70q.webp
参数
w: 指定目标缩略图的宽度。1-4096
h: 指定目标缩略图的高度。1-4096
e: 缩放优先边。0:长边优先,1:短边优先,2:强制按指定宽高缩放。默认为 0
l: 目标缩略图大于原图是否处理。如果值是 1, 即不处理;是 0,表示处理。默认为 0
p: 比例百分比。 小于 100,即是缩小,大于 100 即是放大。1-1000。如果参数 p 跟 w、h 合用时,p 将直接作用于 w、h (乘以 p%)得到新的 w、h,例如 100w_100h_200p 的作用跟 200w_200h 的效果是一样的。默认为 100
q: 质量百分比。1-100。默认为 80
r: 是否使用渐进式 jpeg。0:不使用,1 使用。默认为 0。只有在输出格式为 jpg 时,该参数才会生效。注意:1. 小尺寸图片不建议使用,因为渐进式 jpeg 会比原图大;2. 能够使用 webp 尽量使用 webp;3. 渐进式展示图片的最佳方案应当是前端同时渲染小图和大图,动画过度到大图
.format 格式转换。目前支持 jpg, png, webp。若保留原文件格式,那么 .format 可以不添加
注意
当总面积超过 4096px * 4096px,或者单边长度超过 4096px * 4,那么不会对图片进行缩放处理
实践
在 S3 创建一个公有桶
Block 权限只勾选前两项
存储桶策略
{ "Version": "2012-10-17", "Statement": [ { "Sid": "PublicRead", "Effect": "Allow", "Principal": "*", "Action": "s3:GetObject", "Resource": "arn:aws:s3:::你的桶名/*" } ] }
CORS 配置
<?xml version="1.0" encoding="UTF-8"?> <CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/"> <CORSRule> <AllowedOrigin>http://*.your-domain.com</AllowedOrigin> <AllowedOrigin>https://*.your-domain.com</AllowedOrigin> <AllowedMethod>PUT</AllowedMethod> <ExposeHeader>ETag</ExposeHeader> <ExposeHeader>x-amz-meta-custom-header</ExposeHeader> <AllowedHeader>*</AllowedHeader> </CORSRule> </CORSConfiguration>
在全新的 AWS 账号接入这套方案 (部署)
注意:必须统一在 us-east-1 地区创建函数
准备工作
- 拉取最新的 lambda 代码。仓库:跳转到 Github,按照 readme 指引部署好 docker,从 docker 进入 lambda 目录,npm install 安装好依赖,将 lambda 整个目录压缩为 zip 文件,上传到 S3(为了代码安全,建议上传到私有桶), 记好 s3 文件链接,下面部署要用到
- 创建一个角色用于运行 lambda。该角色需要有以下权限(权限→ 附加策略)
- AmazonS3FullAccess
- AWSLambdaReadOnlyAccess
- AWSLambdaBasicExecutionRole
- 需要给角色添加信任关系(信任关系→ 编辑信任关系)
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Service": "lambda.amazonaws.com" }, "Action": "sts:AssumeRole" }, { "Effect": "Allow", "Principal": { "Service": "edgelambda.amazonaws.com" }, "Action": "sts:AssumeRole" } ] }
创建 Lambda
- 打开控制台 → Lambda → 函数 → 创建函数,选择从头开始创作;
- 填写信息:基本信息:函数名称填:cdnEdgeImageHandler ;运行时选择 Node.js 12.x,选择角色→ 使用现有角色→ 选择前面创建好的角色;点击【创建函数】按钮
- 上传代码:正常情况下会跳转到函数详情页面。“函数代码” 这行有个按钮 “操作”→ “从 AmazonS3 上传”,然后粘贴前边上传后返回的 zip 链接(即使是私有桶链接,也不需要加签名,只要桶属于本账号,Lambda 就能够读取)
- 设置运行时参数:基本设置→ 编辑,处理程序保持为 “index.handler”,内存调到 “384MB”,超时时间调为 15 秒。保存。
- 发布新版本:函数详情页顶部 “操作” 按钮 → 发布新版本 → 描述可选填 → 发布。此时页面刷新后会看到顶部 ARN 有一串字符串,后面部署需要用到。形如 “arn:aws:lambda:us-east-1:016655625412:function:cdnEdgeImageHandler:1”
接入新的 CDN 业务
- 创建 CloudFront(在已有的 CF 上接入请跳过这一步):选择分发方式,选择 Web,下一步→ 源域名:选择回源的域名;源路径:没有特别设置起始目录就留空;→ 点击创建按钮
- 添加 jpg 行为:点击到对应 CF 的详情页→ 行为 tab→ 创建行为→ 路径模式:填 /*.jpg@* ;Lambda 函数关联:CF 事件选择 “源响应”,Lambda 函数 ARN 填写函数详情页顶部的那串字符串(注意字符串最后需要带有 “: 版本 id”)。 → 点击创建按钮
- 如果还有其他图片类型,也需要像步骤 2 一样添加。或者希望对某一个路径下边的图片统一处理,路径模式填目录名也可以,例如 /images/*
- 给对应 S3 实例添加生命周期规则(使用者可自行选择是否制定该规则):本方案会将生成的缩略图上传到 S3,为了不浪费空间,可以定义规则将老文件删除。步骤:打开对应的 S3 实例配置页→ “管理” tab → 添加生命周期规则 → 填写规则名称 “Delete cdn cache” → 规则范围:限制在特定前缀或标签 → 输入框输入 “createByLambda”, 选择 “标签 “,输入标签值”1” 。此时会看到下方多出一个元素:标签 createByLambda | 1。→ 下一步:转换不用配置 → 下一步:配置过期:勾选当前版本和先前版本,下面两个天数都修改为 10 天(可按实际情况自行修改)→ 下一步,保存。
如何更新 Lambda
- 参考上边 “创建 Lambda” 第 3 和第 5 步,发布新版本。
- 函数详情页→ 顶部 “操作” 按钮→ 部署到 Lambda@edge → 选择 对此函数使用现有的 CloudFront 触发器 → 选择触发器(注意这里要选中对应 CF 实例的事件)→ 点击部署按钮
- 过一会检测效果。因为 CF 部署到全球节点需要点时间。PS:每次发布新版本到生产环境的 cdn,建议先部署到测试 CDN 进行测试。
调试技巧
- 先在开发环境调试好。代码仓库里有一份 event.json,可用于模拟 CF 的请求。
- 在部署到 edge 之前,先用 lambda 控制台里的 “测试事件” 先进行测试,建议把 event.json 里的内容粘贴过去作为测试事件。
- 利用好 console.log,日志会输出到 CloudWatch。但是你需要切换到特定的区域才能看到对应请求的日志。(在图片响应头的 x-amz-cf-pop 可以看到区域标识,可用该标识定位区域。ps. 香港节点比较坑,日志有时会跑到它临近的首尔、新加坡节点去)