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

相关推荐

AWS-CertificateManager

【前言】 AWS Certificate Manager是AWS的证书托管系统, 如果配合AWS的LB一起使用,可以免费申请通配证书 【申请证书】 地址: https://uswest2.console.aws.amazon.com/acm/home?region=uswest2/welcome 点

AWS-CodeArtifact

【前言】 AWS CodeArtifact是托管构件的存储库,可以托管npm,maven等 【创建】 地址: https://uswest2.console.aws.amazon.com/codesuite/codeartifact/start?region=uswest2点击右侧的创建存储库按钮开

AWS-CodeBuild

【前言】 aws code build用来构建代码 【创建项目】 地址 https://uswest2.console.aws.amazon.com/codesuite/codebuild/start?region=uswest2 填写名称 点击右边的创建项目按钮,填写项目名称 选择构建来源 这里选

AWS-CodeCommit

【前言】 aws提供了类似github,gitlab的代码托管服务, 目前有一个场景是将内网gitlab的代码下载到aws ec2上, 之前的方案是将gitlab代码镜像到github, 见:Gitlab代码同步到Github 实测效果,github在阿里云ecs上访问经常会timeout, 阿里云

AWS-CodeDeploy

【前言】 aws code deploy,代码部署 【创建】 地址 https://uswest2.console.aws.amazon.com/codesuite/codedeploy/start?region=uswest2创建应用 填写名称和目标后创建, 这里的应用程序下可以创建很多部署组,

AWS-CodePipeline

【前言】 aws code pipeline是流水线工具, 类似开源的jenkins,以及个云的流水线工具 【创建流水线】 地址: https://uswest2.console.aws.amazon.com/codesuite/codepipeline/start?region=uswest2 s

AWS-Route53

【前言】 AWS Route 53是DNS解析服务, 本文将一个之前腾讯云托管的域名转移到AWS Route53上。 【AWS Route 53创建应用】 地址:https://useast1.console.aws.amazon.com/route53/v2/home?region=uswest2

AWS-S3

【前言】 aws s3是类似阿里云oss,腾讯云cos的存储服务 【创建存储桶】 地址: https://s3.console.aws.amazon.com/s3/getstarted?region=useast1 点击右侧的创建存储桶按钮, 填写名称,选择区域, acl默认禁用 公共访问和版本控制

Gitlab代码同步到Github

【背景】 公司的代码一直在内网的gitlab上, 包括日常的代码提交,代码review,代码项目管理,人员管理等, 目前有新业务需要部署到海外aws上, 也就是说需要从海外aws上拉取国内阿里云内网的gitlab代码, 常见的几个方案 1. 国内gitlab到国外aws走跨境专线 2. 手动将代码同

Mac安装mysql

【前言】 mac安装mysql 【下载】 下载社区版本的mysql, 地址: https://dev.mysql.com/downloads/mysql/ 这里选的mac arm dmg版本, 会跳转到新页面,选择直接下载, 【安装】 双击dmg, 双击pkg安装mysql, 按提示点击继续, 选择

Mac上多开微信客户端

【前言】 在日常生活中很多人有多个微信, 手机端的话通过两个手机或者安卓手机多开应用可以实现多开微信, Mac电脑端如何多开微信呢, 常见的方法是一个客户端, 一个网页端: https://wx.qq.com/ , 但是网页端体验肯定没有客户端好, 本文介绍下如何在Mac上多开微信客户端。 【创建快

Mysql授权某个IP访问

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