Logo Vincent
返回文章列表

AWS-使用Lambda@edge转换Cloudfront图片

工具
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图片

© 2026 vincentqiao.com . 保留所有权利。