AWS-使用Lambda@edge转换Cloudfront图片
【前言】
国内云厂商存放图片的oss或者cos可以很方便的转换图片格式和大小等,
一般都是在url后拼接转换参数即可,
AWS居然没有提供这个服务,需要自己写lambda@edge函数实现,
本文完整的介绍下如果使用lambda@edge实现图片转换的功能。
【参考】
开始是想找github开源方案实现,
找到一个还不错的: https://github.com/EsunR/s3-image-handler ,
但实际部署遇到了一些问题,
后来想预期解决部署的问题,
还不如自己写一个,反正逻辑比较简单,
如果不想自己折腾可以用上面的方案试试,整体看 还是比较完整的。
【原理】
简单说一下AWS实现图片转换的原理,
在cloudfront上部署lambda@edge函数,
在函数内自己处理图片转换功能。
【角色】
首先需要为你的lambda@edge函数创建一个角色,
地址: https://us-east-1.console.aws.amazon.com/iam/home
创建一个aws服务的角色,案例选择lambda,

添加权限时搜索添加CloudWatchLogsFullAccess和AmazonS3FullAccess,
创建角色时填入名称Lambda_S3ImageHandler,完成创建,
完成创建后点击详情-信任关系,添加一行,
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Service": [
"edgelambda.amazonaws.com",
"lambda.amazonaws.com"
]
},
"Action": "sts:AssumeRole"
}
]
}
到此,lambda对应的角色创建成功。
【原理简介】
lambda@edge其实也是lambda,不同点事部署到了cdn节点的lambda,
对应的有4个函数,
viewer request,
viewer response,
origin request,
origin response,
每次发起cdn请求时,流程是
1.先到viewer request
2.如果没有命中cdn缓存,就到origin request,例如源是s3,就从s3获取文件,返回origin response,
3.最后返回viewer response,
4.如果命中缓存就忽略#2
要处理图片,其实只需要2个函数
1.viewer request,预处理一下参数
2.origin response,返回源文件时做处理
【上传函数】
登录
安装aws cli后,使用aws configure命令本地登录aws,
这里注意,区域要选择us-east-1,这个区域。
发布函数
本地新建一个publish.sh脚本,内容如下,
其中的xxxx需要修改为自己的accountId,
其中viewer相关的函数timeout最大是5s,memory最大是128M,
而origin相关的函数timeout最大是30s,memory最大是1024M,
runtime="nodejs18.x"
role_arn="arn:aws:iam::xxxx:role/Lambda_S3ImageHandler"
fucntion_hander="index.handler"
aws lambda create-function \
--function-name S3ImageHandler_ViewerRequest \
--zip-file fileb://dist/viewer-req.zip \
--handler $fucntion_hander \
--runtime $runtime \
--timeout 5 \
--memory-size 128 \
--role $role_arn
aws lambda create-function \
--function-name S3ImageHandler_OriginResponse \
--zip-file fileb://dist/origin-res.zip \
--handler $fucntion_hander \
--runtime $runtime \
--timeout 30 \
--memory-size 1024 \
--role $role_arn
执行脚本后发布成功,
在lambda控制台可以看到发布的函数,
地址: https://us-east-1.console.aws.amazon.com/lambda/home?region=us-east-1#/functions
注意要切为为us-east-1区域,

更新函数
本地新建一个脚本,用来更新函数,内容如下,
aws lambda update-function-code \
--function-name S3ImageHandler_ViewerRequest \
--zip-file fileb://dist/viewer-req.zip \
aws lambda update-function-code \
--function-name S3ImageHandler_OriginResponse \
--zip-file fileb://dist/origin-res.zip \
【绑定到cloudfront】
绑定viewer request
进入viewer request函数详情,
右上角有部署到lambda@edge,
这里注意要切到us-east-1区域,

按图示选填后点击部署,

部署过程可以去cloudfront中查看,
这里上次修改时间为部署中,
等部署完毕后会显示具体的时间。

绑定origin response函数
绑定方法同viewer request
【查看日志】
在整个开发lambda@edge的过程中,
大量的时间花在了上面的部署函数,
因为要将lambda部署到cdn节点,一般需要几分钟,
另一部分比较耗时的就是找日志,
因为cdn每次命中的节点不同,
所以你要去各个节点找日志,过程很痛苦,
可以先在函数详情-监控中点击查看cloudwatch logs,
跳转到cloudwatch日志,

跳转过去后痛苦的事情就来了,
要在各个节点找日志,
按正常逻辑在国内请求,应该cdn会命中东京,香港或者新加坡,
然而好几次基本都是命中了法国,很神奇。

准确确定命中cdn区域,也就是去哪里查看日志,
编辑origin response函数,写如下面代码,
// handler
exports.handler = async (event, context, callback) => {
// res
const response = event.Records[0].cf.response;
response.headers['execution-region'] = [{ key: 'Execution-Region', value: process.env.AWS_REGION }];
};
添加上面的响应头后,在response header中可以看到具体区域,

如上本次请求命中了ap-southeast-1区域,也就是新加坡节点,
然后就可以在cloudwatch中切换到对应区域查看日志了。
【viewer request】
viewer request函数的处理比较简单,
只处理参数即可,
这里发现比较奇怪的现象是,
viewer request中的query string正常,
例如url为https://xxx.com/1.png?handler=yes&width=100,
则viewer request中的querystring会显示handler=yes&width=100,
但是等到origin response时querystring为空了,
所以需要在viewer request中处理下querystirng,
拼接到uri中,代码如下,
'use strict';
// qs
const qs = require('qs');
/**
* handler
* @param {s} event
* @param {*} context
* @param {*} callback
*/
exports.handler = (event, context, callback) => {
// req
const request = event.Records[0].cf.request;
try {
// qs
const originQuerystring = request.querystring;
if (!originQuerystring) {
callback(null, request);
return;
}
// qs obj
const qsObj = qs.parse(originQuerystring);
if (!qsObj || qsObj.handler !== 'yes') {
callback(null, request);
return;
}
// uri
const originUri = request.uri;
// add uri
qsObj.uri = originUri;
const finalURI = JSON.stringify(qsObj);
// go
request.uri = `/${finalURI}`;
callback(null, request);
return;
} catch (error) {
callback(null, request);
}
};
【origin response】
命中已处理图片
如果命中已经处理过的图片,逻辑如下
1.获取bucket name
2.判断s3中是否存在已经处理过的图片,例如1_width_100.png
3.如果存在,通过s3 getObject获取到object,
4.获取到object对应的buffer,并返回response
获取bucket
可以从request中的origin获取bucket名称,
const domainName = request.origin.s3.domainName;
const bucket = domainName.split('.')[0];
判断s3中的key是否存在
/**
* isExists
* @param {*} bucket
* @param {*} key
* @returns
*/
exports.isExists = async function (bucket, key) {
const methodName = 'isExists';
try {
// input
const input = {
Bucket: bucket,
Key: key,
};
logger.info(methodName, 'input', input);
// res
const command = new HeadObjectCommand(input);
await client.send(command);
logger.info(methodName, 'res', true);
return true;
} catch (error) {
logger.info(methodName, 'res', false);
return false;
}
};
获取s3中object
/**
* getObject
* @param {*} bucket
* @param {*} key
* @returns
*/
exports.getObject = async function (bucket, key) {
const methodName = 'getObject';
try {
// input
const input = {
Bucket: bucket,
Key: key,
};
logger.info(methodName, 'input', input);
const command = new GetObjectCommand(input);
return await client.send(command);
} catch (error) {
logger.info(methodName, 'error', error);
}
};
获取s3 object对应的buffer
/**
* getObjectBuffer
* @param {*} object
* @returns
*/
exports.getObjectBuffer = function (object) {
const methodName = 'getObjectBuffer';
return new Promise(function (resolve, reject) {
const byteArray = [];
object.Body.on('error', (error) => {
logger.info(methodName, 'error', error);
reject(error);
});
object.Body.on('data', (chunk) => {
byteArray.push(chunk);
});
object.Body.on('end', () => {
const buffer = Buffer.concat(byteArray);
logger.info(methodName, 'buffer');
resolve(buffer);
});
});
};
使用buffer返回s3 object作为response
/**
* handlerResponseImg
* @param {*} response
* @param {*} object
* @returns
*/
exports.handlerResponseImg = async function (response, buffer) {
const methodName = 'handlerResponseImg';
// base64
const base64String = buffer.toString('base64');
logger.info(methodName, 'base64String');
// response
response.status = '200';
response.statusDescription = 'OK';
response.body = base64String;
response.bodyEncoding = 'base64';
response.headers['content-type'] = [
{
key: 'Content-Type',
value: 'image/webp',
},
];
response.headers['content-length'] = [
{
key: 'Content-Length',
value: `${base64String.length}`,
},
];
response.headers['lambda-edge'] = [
{
key: 'Lambda-Edge',
value: 'success',
},
];
return response;
};
没有命中处理过的图片
如果s3中没有已经处理的图片,类似1_width_100.png,
则需要使用sharp处理图片,并存到s3中,然后返回response,
处理图片
/**
* formatImg
* @param {*} originObject
* @param {*} width
* @param {*} height
* @returns
*/
exports.formatImg = async function (originObject, width, height, bucket, finalKey) {
const methodName = 'formatImg';
try {
// resize options
const resizeOptions = {
fit: 'cover',
};
if (width) resizeOptions.width = parseInt(width);
if (height) resizeOptions.height = parseInt(height);
logger.info(methodName, 'resizeOptions', resizeOptions);
// format
const buffer = await getObjectBuffer(originObject);
const imgBuffer = await sharp(buffer).resize(resizeOptions).toFormat('webp').toBuffer();
// put
await putObject(bucket, finalKey, imgBuffer);
logger.info(methodName, 'putObject success');
return imgBuffer;
} catch (error) {
logger.info(methodName, 'error', error);
}
};
存到s3上
/**
* putObject
* @param {*} bucket
* @param {*} key
* @param {*} buffer
* @returns
*/
exports.putObject = async function (bucket, key, buffer) {
const methodName = 'putObject';
try {
// input
const input = {
Body: buffer,
Bucket: bucket,
Key: key,
};
const command = new PutObjectCommand(input);
return await client.send(command);
} catch (error) {
logger.info(methodName, 'error', error);
}
};
【sharp相关问题】
安装慢
由于sharp依赖文件比较大,
国内安装慢,可以用下面的方法安装,
npm_config_sharp_binary_host="https://npmmirror.com/mirrors/sharp" \
npm_config_sharp_libvips_binary_host="https://npmmirror.com/mirrors/sharp-libvips" \
npm i sharp
sharp包较大
安装sharp后,执行update脚本后提示,
lambda函数包过大,看了下node_modules下,
sharp安装了各平台的包,

这里按官方提示没有解决,手动写了脚本删除了无关平台包后解决,
// qiao
const { lsdir, rm } = require('qiao-file');
// cosnt
const dist = './dist/origin-res/node_modules/@img/';
const excludeNames = ['sharp-linux-x64', 'sharp-libvips-linux-x64', 'sharp-wasm32'];
// clean sharp
cleanSharp();
async function cleanSharp() {
// ls
const dirs = await lsdir(dist);
const excludeFolders = dirs.folders.filter((folder) => {
return folder.name.indexOf('sharp-') > -1;
});
// del
for (let i = 0; i < excludeFolders.length; i++) {
const folder = excludeFolders[i];
console.log(folder.name);
if (excludeNames.includes(folder.name)) {
console.log('skip');
} else {
const res = await rm(folder.path);
console.log('rm', res);
}
}
}
【最终效果】
经过处理后的图片,
1.按width resize了大小
2.转为了webp图片
