国内的云存储厂商(如阿里云,七牛云等)提供了比较方便的图片处理方案,只需要添加不同的图片访问参数值,就可以实现图片的缩放。AmazonS3并没有提供现成的方案,但我们可以通过Cloudfront+Lambda@Edge实现图片的缩放功能。
Lambda@Edge Lambda@Edge可以认为是运行在Cloudfront上的Lambda,不同的是Lambda@Edge只支持Nodejs
和Python
两种语言,而且Lambda@Edge仅可在 美国东部 (弗吉尼亚北部) us-east-1 配置。
我们可以在客户端访问Cloudfront的4个过程中配置Lambda:
查看器请求(Viewer Request):在检查是否命中Cloudfront缓存前执行,就是客户端每个请求都会执行
源请求(Origin Request):当Cloudfront缓存未命中,并且请求未到达源时执行
源响应(Origin Response):当Cloudfront缓存未命中,并且得到源响应后执行
查看器响应(Viewer Response):当Cloudfront命中缓存或者得到了源响应后执行,就是每个客户端请求都会执行
方案架构
Lambda配置 我们在这里配置了2个Lambda函数: 1.配置于源请求,用于检查请求图片地址是否合法,判断是否需要修改请求。 2.配置于源响应,用于生成缩略图。
配置于源请求的Lambda函数其实是可以省略的,此处我们希望缩略图能够统一存储,所以此函数主要用于修改请求地址,例如客户端请求图片为/a/b/c_200x200.jpg
,在Lambda函数处理后,请求至S3的地址会修改为/thumbnails/200x200/a/b/c.jpg
,将所有缩略图存储在thumbnails
文件夹下。
流程说明 1.客户端请求Cloudfront 2.Cloudfront上未命中缓存,请求S3之前执行Lambda函数,修改请求地址 3.获取S3响应。如果S3返回图片不存在,执行Lambda 4.Lambda下载原图并生成缩略图,转存至S3,将图片返回给Cloudfront 5.Cloudfront返回结果给客户端
Lambda函数 代码地址:https://github.com/iyichen/lambda-thumbnail
源请求函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 'use strict' ;const variables = { allowedDimension : [ {w :75 ,h :75 }, {w :200 ,h :200 }] }; exports .handler = (event, context, callback ) => { const request = event.Records [0 ].cf .request ; let fwdUri = request.uri ; let pathMatchFound = false ; for (let dimension of variables.allowedDimension ) { let size = "_" + dimension.w + "x" + dimension.h ; if (fwdUri.indexOf (size) > 0 ) { pathMatchFound = true ; break ; } } if (!pathMatchFound) { callback (null , request); return ; } let prefix, imageName, width, height, extension; const match = fwdUri.match (/(.*)\/(.*)_(\d+)x(\d+)\.(.*)/ ); try { prefix = match[1 ]; imageName = match[2 ]; width = match[3 ]; height = match[4 ]; extension = match[5 ]; } catch (err) { console .log ("parse error. path:" + fwdUri); callback (null , request); return ; } let matchFound = false ; for (let dimension of variables.allowedDimension ) { if (dimension.w == width && dimension.h == height){ width = dimension.w ; height = dimension.h ; matchFound = true ; break ; } } if (!matchFound){ callback (null , request); return ; } fwdUri = "/thumbnails/" + width + "x" + height + prefix + "/" + imageName + "." + extension; request.uri = fwdUri; callback (null , request); };
源请求函数中,我们定义了缩略图的规范(只允许200x200
和75x75
),避免生成一些无意义的缩略图,当符合请求规则后,将请求地址修改。如请求地址为/images/image_200x200.jpg
,经过函数处理后,实际请求S3的地址为/thumbnails/200x200/images/image.jpg
源响应函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 'use strict' ;const http = require ('http' );const https = require ('https' );const querystring = require ('querystring' );const AWS = require ('aws-sdk' );const S3 = new AWS .S3 ({ signatureVersion : 'v4' , }); const Sharp = require ('sharp' );exports .handler = (event, context, callback ) => { let response = event.Records [0 ].cf .response ; if (response.status == 403 ) { let request = event.Records [0 ].cf .request ; let path = request.uri ; console .log ("path not exist. path:" + path); if (!path.startsWith ("/thumbnails" )){ callback (null , response); return ; } let bucket; let host = request.origin .s3 .domainName ; let bucketIndex = host.indexOf ('.s3.amazonaws.com' ); if (bucketIndex > 0 ) { bucket = host.substring (0 , bucketIndex); } else { callback (null , response); return ; } let key = path.substring (1 ); let originalKey, match, width, height, requiredFormat, imageKey; let startIndex; match = key.match (/thumbnails\/(\d+)x(\d+)\/((.*)\.(.*))/ ); if (match == null || match.length != 6 ) { callback (null , response); return ; } try { width = parseInt (match[1 ], 10 ); height = parseInt (match[2 ], 10 ); imageKey = match[3 ]; requiredFormat = match[5 ].toLowerCase (); requiredFormat = (requiredFormat == "jpg" ? "jpeg" : (requiredFormat == "gif" ? "png" : requiredFormat)); originalKey = imageKey; } catch (err) { console .log ("parse error. path:" + path); callback (null , response); return ; } S3 .getObject ({ Bucket : bucket, Key : originalKey }).promise () .then (data => Sharp (data.Body ) .resize (width, height) .toFormat (requiredFormat) .toBuffer () ) .then (buffer => { S3 .putObject ({ Body : buffer, Bucket : bucket, ContentType : 'image/' + requiredFormat, CacheControl : 'max-age=2592000' , Key : key, StorageClass : 'STANDARD' }).promise () .catch (() => { console .log ("Exception while writing resized image to bucket. path:" + path); callback (null , response); return ; }); response.status = 200 ; response.body = buffer.toString ('base64' ); response.bodyEncoding = 'base64' ; response.headers ['content-type' ] = [{ key : 'Content-Type' , value : 'image/' + requiredFormat }]; response.headers ['last-modified' ] = [{ key : 'Last-Modified' , value : new Date ().toUTCString () }]; callback (null , response); }) .catch ( err => { console .log ("Exception while reading source image. path:" + path); callback (null , response); return ; }); } else { callback (null , response); } };
在源响应函数分成以下几步: 1.通过源响应的返回判断图片是否存在 2.如果图片不存在,解析请求地址,获取原图地址和尺寸 3.下载原图,生成缩略图,将缩略图传至S3 4.返回缩略图给Cloudfront
其他 设置生命周期 可以对缩略图配置生命周期,到期自动删除,可以节省存储空间
源响应函数中response.status=403 在S3中,如果用户只有s3:GetObject
权限,即使文件不存在,S3出于安全默认返回的也是403;如果需要返回404状态,需要赋予s3:ListBucket
权限。
Lambda提示Sharp报错 按照官网说明 ,指定Lambda环境为nodejs10.x
,修改安装命令:
1 2 rm -rf node_modules/sharp npm install --arch=x64 --platform=linux --target=10.15.0 sharp
Sharp输出GIF格式报错 sharp
不支持gif
,示例代码中是取gif
第一帧图片,缩放后图片虽然后缀为.gif
,其实格式为png
参考 Resizing Images with Amazon CloudFront & Lambda@Edge Lambda@Edge Design Best Practices Lambda@Edge开发人员指南 Leveraging external data in Lambda@Edge Lambda@Edge gotchas and tips Building a Real-Time Image Optimizer Using Lambda@Edge and Amazon CloudFront