AWS 无服务器速成课程 - 使用 Lambda 和 S3 动态调整图像大小

2025-06-07

AWS 无服务器速成课程 - 使用 Lambda 和 S3 动态调整图像大小

自从我开始写代码以来,处理大图片一直是我的痛点。最近,它开始对页面速度和 SEO 排名产生巨大影响。如果你的网站图片优化不佳,在Google Lighthouse上就不会有好的排名。如果排名不好,它就上不了 Google 首页。这真是糟透了。

关于谷歌第二页的 meme

TL;DR

我编写并开源了一段代码,只需一个简单的命令即可自动创建和部署图像大小调整函数以及 S3 存储桶。点击此处查看代码

但是如果您想跟随并学习如何自己做,请继续阅读。

我们从哪里开始?

幸运的是,有一种方法可以轻而易举地解决图像优化不佳的问题。今天我们将构建一个AWS Lambda函数来动态调整图像大小。

图片将存储在S3存储桶中,一旦收到请求,即可从中获取。如果您需要调整大小的版本,请请求图片并提供高度和宽度。这将触发一个函数。该函数将抓取现有图片,调整大小,然后将其返回到存储桶,并从存储桶中获取。

此场景只会针对给定尺寸的一组图像调整一次大小。后续所有针对该尺寸图像的请求都将从存储桶中处理。是不是很酷?这里有一张图,谁不喜欢图呢?

图表

因为我已经假设你已经知道如何使用无服务器框架,并且已经了解了无服务器DockerAWS的基础知识,所以我会直接进入正题。以下是我们将要做的事情的概述。

  • 创建项目结构
  • 创建机密文件
  • 编写 AWS Lambda 函数配置
  • 编写 AWS Lambda 函数源代码
  • 写入 S3 Bucket 配置
  • 使用 Docker 部署
  • 使用Dashbird进行测试

注意:继续本教程之前,请安装DockerDocker Compose 。

有趣的是,由于Sharp的存在,我们需要使用 Docker 来部署这项服务。这个 image-resize 模块的二进制文件需要在与其运行相同的操作系统上构建。由于 AWS Lambda 在 Amazon Linux 上运行,因此我们需要npm install在运行之前在 Amazon Linux 实例上安装这些软件包sls deploy

解决了这些问题之后,我们就可以开始构建一些东西了。

创建项目结构

解释这一复杂结构的最佳方式是用图像。

文件夹结构

文件夹为绿色,文件为蓝色。查看代码库了解更多信息。

创建机密文件

我们将在secrets文件夹中创建一个secrets.env文件来保存我们的秘密,并在 Docker 容器启动后将其注入其中。

SLS_KEY=XXX # replace with your IAM User key
SLS_SECRET=YYY # replace with your IAM User secret
STAGE=dev
REGION=us-east-1
BUCKET=images.yourdomain.com
Enter fullscreen mode Exit fullscreen mode

注意:该deploy.sh脚本将创建一个辅助secrets.json文件,仅用于保存我们的 API 网关端点的域名。

编写 AWS Lambda 函数配置

打开函数文件夹,然后创建package.json文件。粘贴以下内容。

{
  "name": "image-resize",
  "version": "1.0.0",
  "description": "Serverless image resize on-the-fly.",
  "main": "resize.js",
  "dependencies": {
    "serverless-plugin-tracing": "^2.0.0",
    "sharp": "^0.18.4"
  }
}
Enter fullscreen mode Exit fullscreen mode

无需运行任何安装,因为我们需要先运行 Docker 容器。请注意,我们添加了一个跟踪插件来启用X-Ray,因为它非常棒。

接下来,让我们创建serverless.yml文件来配置我们的功能。

service: image-resize-on-the-fly-functions

plugins:
  - serverless-plugin-tracing

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${env:STAGE, 'dev'}
  profile: serverless-admin # or change to 'default'
  tracing: true
  region: ${env:REGION, 'us-east-1'}
  environment:
    BUCKET: ${env:BUCKET}
    REGION: ${env:REGION}
  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:ListBucket"
      Resource: "arn:aws:s3:::${env:BUCKET}"
    - Effect: "Allow"
      Action:
        - "s3:PutObject"
      Resource: "arn:aws:s3:::${env:BUCKET}"
    - Effect: "Allow"
      Action:
        - "xray:PutTraceSegments"
        - "xray:PutTelemetryRecords"
      Resource:
        - "*"

functions:
  resize:
    handler: resize.handler
    events:
      - http:
          path: resize
          method: get
Enter fullscreen mode Exit fullscreen mode

正如您所看到的,我们从中获取了一堆值env,同时允许函数访问BUCKET我们指定的和一些 X-Ray 遥测。

编写 AWS Lambda 函数源代码

现在只剩下代码了。接下来我们来做。由于图像可能会变得非常大,我们不想冒险将几兆字节的数据加载到 lambda 函数的内存中,所以我将向您展示如何使用Node.js 流从 S3 流中读取图像,将其通过管道传输到 Sharp,然后再次将其以流的形式写回 S3。

准备好了吗?我们分两步来做。首先,定义创建流所需的辅助函数,然后创建 lambda 将调用的处理函数本身。

// resize.js

// require modules
const stream = require('stream')
const AWS = require('aws-sdk')
const S3 = new AWS.S3({
  signatureVersion: 'v4'
})
const sharp = require('sharp')

// create constants
const BUCKET = process.env.BUCKET
const URL = `http://${process.env.BUCKET}.s3-website.${process.env.REGION}.amazonaws.com`

// create the read stream abstraction for downloading data from S3
const readStreamFromS3 = ({ Bucket, Key }) => {
  return S3.getObject({ Bucket, Key }).createReadStream()
}
// create the write stream abstraction for uploading data to S3
const writeStreamToS3 = ({ Bucket, Key }) => {
  const pass = new stream.PassThrough()
  return {
    writeStream: pass,
    uploadFinished: S3.upload({
      Body: pass,
      Bucket,
      ContentType: 'image/png',
      Key
    }).promise()
  }
}
// sharp resize stream
const streamToSharp = ({ width, height }) => {
  return sharp()
    .resize(width, height)
    .toFormat('png')
}
Enter fullscreen mode Exit fullscreen mode

这里我们定义了流的常量和构造函数。需要注意的是,S3 没有使用流写入存储的默认方法。因此您需要创建一个。这可以通过stream.PassThrough()辅助函数来实现。上面的抽象将使用流写入数据,并在完成后解析一个 Promise。

接下来,我们来看一下处理程序。在上面的代码下方,粘贴这段代码。

exports.handler = async (event) => {
  const key = event.queryStringParameters.key
  // match a string like: '/1280x720/image.jpg'
  const match = key.match(/(\d+)x(\d+)\/(.*)/)
  // get the dimensions of the new image
  const width = parseInt(match[1], 10)
  const height = parseInt(match[2], 10)
  const originalKey = match[3]
  // create the new name of the image, note this has a '/' - S3 will create a directory
  const newKey = '' + width + 'x' + height + '/' + originalKey
  const imageLocation = `${URL}/${newKey}`

  try {
    // create the read and write streams from and to S3 and the Sharp resize stream
    const readStream = readStreamFromS3({ Bucket: BUCKET, Key: originalKey })
    const resizeStream = streamToSharp({ width, height })
    const { 
      writeStream, 
      uploadFinished 
    } = writeStreamToS3({ Bucket: BUCKET, Key: newKey })

    // trigger the stream
    readStream
      .pipe(resizeStream)
      .pipe(writeStream)

    // wait for the stream to finish
    const uploadedData = await uploadFinished

    // log data to Dashbird
    console.log('Data: ', {
      ...uploadedData,
      BucketEndpoint: URL,
      ImageURL: imageLocation
    })

    // return a 301 redirect to the newly created resource in S3
    return {
      statusCode: '301',
      headers: { 'location': imageLocation },
      body: ''
    }
  } catch (err) {
    console.error(err)
    return {
      statusCode: '500',
      body: err.message
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

查询字符串参数如下所示/1280x720/image.jpg。这意味着我们使用正则表达式匹配从参数中获取图像尺寸。newKey值设置为尺寸后跟 a/和图像的原始名称。这将创建一个1280x720以图像名称命名的新文件夹image.jpg。很酷。

一旦我们触发流并等待承诺解决,我们就可以注销数据并返回 301 重定向到存储桶中图片的位置。请注意,我们将在下一段中在存储桶上启用静态网站托管。

写入 S3 Bucket 配置

存储桶配置主要由默认的CloudFormation模板组成,所以如果你有点生疏,我建议你稍微复习一下。不过,它的要点其实很容易理解。打开存储桶文件夹并创建一个serverless.yml文件。

service: image-resize-on-the-fly-bucket

custom:
  secrets: ${file(../secrets/secrets.json)} # will be created in our deploy.sh script

provider:
  name: aws
  runtime: nodejs8.10
  stage: ${env:STAGE, 'dev'}
  profile: serverless-admin # or change to 'default'
  region: us-east-1
  environment:
    BUCKET: ${env:BUCKET}

resources:
  Resources:
    ImageResizeOnTheFly:
      Type: AWS::S3::Bucket # creating an S3 Bucket
      Properties:
        AccessControl: PublicReadWrite
        BucketName: ${env:BUCKET}
        WebsiteConfiguration: # enabling the static website option
          ErrorDocument: error.html
          IndexDocument: index.html
          RoutingRules: # telling it to redirect for every 404 error
            - 
              RedirectRule:
                HostName: ${self:custom.secrets.DOMAIN} # API Gateway domain
                HttpRedirectCode: "307" # temporary redirect HTTP code
                Protocol: "https"
                ReplaceKeyPrefixWith: "${self:provider.stage}/resize?key=" # route
              RoutingRuleCondition:
                HttpErrorCodeReturnedEquals: "404"
                KeyPrefixEquals: ""
    ImageResizeOnTheFlyPolicy:
      Type: AWS::S3::BucketPolicy # add policy for public read access
      Properties: 
        Bucket: 
          Ref: ImageResizeOnTheFly
        PolicyDocument: 
          Statement: 
            - 
              Action: 
                - "s3:*"
              Effect: "Allow"
              Resource:
                Fn::Join: 
                  - ""
                  - 
                    - "arn:aws:s3:::"
                    - 
                      Ref: ImageResizeOnTheFly
                    - "/*"
              Principal: "*"
Enter fullscreen mode Exit fullscreen mode

一旦我们在存储桶上启用了静态网站托管选项,它就会像任何网站一样运行。这使我们能够从中提供图片,并利用 404 错误重定向规则。

如果未找到图片,存储桶将触发 404 错误,并重定向到我们的 lambda 函数。然后将原始图片调整为请求的尺寸,并将其按请求的确切路径放回存储桶。

就像魔术一样!

使用 Docker 部署

精彩的部分来了!我们将创建一个Dockerfileanddocker-compose.yml文件来创建我们的 Amazon Linux 容器并加载.env值。这很简单。难点在于编写 bash 脚本来运行所有命令并部署我们的函数和存储桶。

从开始Dockerfile,这是您需要添加的内容。

FROM amazonlinux

# Create deploy directory
WORKDIR /deploy

# Install system dependencies
RUN yum -y install make gcc*
RUN curl --silent --location https://rpm.nodesource.com/setup_8.x | bash -
RUN yum -y install nodejs

# Install serverless
RUN npm install -g serverless

# Copy source
COPY . .

# Install app dependencies
RUN cd /deploy/functions && npm i --production && cd /deploy

#  Run deploy script
CMD ./deploy.sh ; sleep 5m
Enter fullscreen mode Exit fullscreen mode

由于 Amazon Linux 比较基础,我们需要先安装 gcc 和 Node.js。之后就和你见过的任何 Dockerfile 一样简单了。全局安装 Serverless Framework,复制源代码,在functions目录中安装 npm 模块,然后运行deploy.sh脚本。

docker-compose.yml文件实际上仅用于加载.env值。

version: "3"
services:
  image-resize-on-the-fly:
    build: .
    volumes:
      - ./secrets:/deploy/secrets
    env_file:
      - ./secrets/secrets.env
Enter fullscreen mode Exit fullscreen mode

就这样。Docker 部分完成了。我们来写一些 bash 吧。

从简单开始,我们将定义初始变量,配置无服务器安装并部署我们的功能。

# deploy.sh

# variables
stage=${STAGE}
region=${REGION}
bucket=${BUCKET}
secrets='/deploy/secrets/secrets.json'

# Configure your Serverless installation to talk to your AWS account
sls config credentials \
  --provider aws \
  --key ${SLS_KEY} \
  --secret ${SLS_SECRET} \
  --profile serverless-admin

# cd into functions dir
cd /deploy/functions

# Deploy function
echo "------------------"
echo 'Deploying function...'
echo "------------------"
sls deploy
Enter fullscreen mode Exit fullscreen mode

部署完成后,我们需要获取 API 网关端点的域名,并将其放入一个文件中,该文件将从bucketsecrets.json目录加载到我们的serverless.yml文件中。为此,我使用了一些正则表达式。将其添加到.deploy.sh

# find and replace the service endpoint
if [ -z ${stage+dev} ]; then echo "Stage is unset."; else echo "Stage is set to '$stage'."; fi

sls info -v | grep ServiceEndpoint > domain.txt
sed -i 's@ServiceEndpoint:\ https:\/\/@@g' domain.txt
sed -i "s@/$stage@@g" domain.txt
domain=$(cat domain.txt)
sed "s@.execute-api.$region.amazonaws.com@@g" domain.txt > id.txt
id=$(cat id.txt)

echo "------------------"
echo "Domain:"
echo "  $domain"
echo "------------------"
echo "API ID:"
echo "  $id"

rm domain.txt
rm id.txt

echo "{\"DOMAIN\":\"$domain\"}" > $secrets
Enter fullscreen mode Exit fullscreen mode

现在, secretssecrets.json目录中已经有了一个文件。剩下的就是运行 bucket 部署。请将这段代码粘贴到脚本的底部deploy.sh

cd /deploy/bucket

# Deploy bucket config
echo "------------------"
echo 'Deploying bucket...'
sls deploy

echo "------------------"
echo 'Bucket endpoint:'
echo "  http://$bucket.s3-website.$region.amazonaws.com/"

echo "------------------"
echo "Service deployed. Press CTRL+C to exit."
Enter fullscreen mode Exit fullscreen mode

好了!代码部分完成了。你相信吗,我们现在只需要运行一个命令?在项目根目录下打开一个终端窗口,运行:

$ docker-compose up --build
Enter fullscreen mode Exit fullscreen mode

让它发挥它的魔力,你会看到一切都自动创建了!请注意,终端将显示用于访问镜像的存储桶端点。

使用 Dashbird 进行测试

最后一步是检查一切是否正常。我们先上传一张图片到存储桶,这样就可以调整大小了。您可以先找到一张您喜欢并想要调整大小的图片,或者直接选这张

它相当大,大约 6MB。这是您要运行的上传命令。

$ aws s3 cp --acl public-read IMAGE s3://BUCKET
Enter fullscreen mode Exit fullscreen mode

因此,如果您的存储桶名称是images并且您的图像名称是the-earth.jpg,那么如果您从图像所在的目录运行命令,它应该看起来像这样。

$ aws s3 cp --acl public-read the-earth.jpg s3://images
Enter fullscreen mode Exit fullscreen mode

请记住,您需要在您的机器上安装AWS CLI,或者通过 AWS 控制台上传图像。

现在,尝试通过存储桶请求图片。在浏览器中输入 S3 存储桶的 URL。

http://BUCKET.s3-website.REGION.amazonaws.com/the-earth.jpg
Enter fullscreen mode Exit fullscreen mode

您将看到原始图像。但是,现在请为 URL 添加尺寸。

http://BUCKET.s3-website.REGION.amazonaws.com/400x400/the-earth.jpg
Enter fullscreen mode Exit fullscreen mode

这将触发 resize 函数并创建一个 400x400 像素的图片版本。创建过程需要几百毫秒,完成后,浏览器会重定向到新创建的图片。刷新 URL 时,你会发现浏览器正在获取新图片,而无需调用 resize 函数。

让我们检查Dashbird中的日志并确保一切正常运行。

Dashbird 日志

看起来不错,但我第一次尝试设置这个函数时确实犯了一些新手错误。其中之一就是在使用模块之前忘记引用它。幸运的是,我立即收到了一条警报,解释了错误所在。Slack的警报真是救命稻草。

松弛警报

我修复这个问题的方法是使用Live-tailing功能。它允许我检查调用日志,虽然有几秒钟的延迟,但这样我就可以调试问题了。真是太棒了。

活尾

总结

最后,我想指出,使用无服务器技术来支持你现有的基础设施是非常棒的。它与语言无关,而且易于使用

在 Dashbird,我们的核心功能使用容器集群,这些功能与我们的数据库进行大量交互,同时将其余所有功能卸载到 AWS 上的 lambda 函数、队列、流和其他无服务器服务。

当然,这里再次分享一下代码库,如果你想让更多人在 GitHub 上看到它,就点个星吧。只要按照那里的说明操作,就能立即启动并运行这个可以动态调整图片大小的微服务。

如果您想阅读我之前的一些无服务器思考,请转到我的个人资料加入我的无服务器时事通讯!

编写这段开源代码片段的过程真是太愉快了。写这篇文章的过程也相当不错!希望大家读起来就像我写这篇文章一样开心。如果你们喜欢,就拍拍那只小独角兽,这样 dev.to 上会有更多人看到这篇文章。下次再见,保持好奇心,享受乐趣吧。


本月的赞助商是 Zeet。

免责声明:Zeet将赞助下个月的这篇博文。前几天我试用了一下。它类似于无服务器架构,但可以运行整个后端。你可以自动托管和扩展应用程序。相当不错。


文章来源:https://dev.to/adnanrahic/a-crash-course-on-serverless-with-aws---image-resize-on-the-fly-with-lambda-and-s3-4foo
PREV
什么是部分水合?为什么大家都在谈论它?
NEXT
如何使用 Context 编写高性能 React 应用程序