教程:使用 Amazon S3 触发器创建缩略图 - Amazon Lambda
Amazon Web Services 文档中描述的 Amazon Web Services 服务或功能可能因区域而异。要查看适用于中国区域的差异,请参阅中国的 Amazon Web Services 服务入门

教程:使用 Amazon S3 触发器创建缩略图

在本教程中,您将创建 Lambda 函数并为 Amazon Simple Storage Service (Amazon S3) 配置触发器。Amazon S3为上传到 S3 存储桶的每个图像文件调用 CreateThumbnail 函数。该函数从 S3 源存储桶读取图像对象并在目标 S3 存储桶中创建要保存的缩略图。

注意

本教程需要适度的 Amazon 和 Lambda 领域知识水平。我们建议您先尝试 教程:使用 Amazon S3 触发器调用 Lambda 函数

在本教程中,您将使用 Amazon Command Line Interface (Amazon CLI) 创建以下 Amazon 资源:

Lambda 资源

  • Lambda 函数。您可以选择 Node.js、Python 或 Java 作为函数代码。

  • 函数的 .zip 文件存档部署包。

  • 授予 Amazon S3 调用函数的权限的访问策略。

Amazon Identity and Access Management (IAM) 资源

  • 拥有关联权限策略的执行角色,用于授予函数所需的权限。

Amazon S3 资源

  • 具有调用函数的通知配置的源 S3 存储桶。

  • 函数在其中保存已调整大小的图像的目标 S3 存储桶。

先决条件

  • 拥有 Amazon 账户

    要使用 Lambda、Amazon 和其他 Amazon 服务,您需要一个 Amazon 账户。如果您没有账户,请访问 www.amazonaws.cn,然后选择 Create an Amazon Account (创建 Amazon 账户)。有关说明,请参阅如何创建和激活新的 Amazon 帐户?

  • Command line

    要完成以下步骤,您需要命令行终端或 Shell 以运行命令。命令和预期输出在单独的数据块中列出:

    aws --version

    您应看到以下输出:

    aws-cli/2.0.57 Python/3.7.4 Darwin/19.6.0 exe/x86_64

    对于长命令,使用转义字符 (\) 将命令拆分到多行中。

    在 Linux 和 macOS 中,可使用您首选的外壳程序和程序包管理器。在 Windows 10 中,您可以 安装 Windows Subsystem for Linux,获取 Ubuntu 和 Bash 与 Windows 集成的版本。

  • Amazon CLI

    在本教程中,您将使用 Amazon CLI 命令创建和调用 Lambda 函数。安装 Amazon CLI使用您的 Amazon 凭据对其进行配置

  • 语言工具

    为您要使用的语言安装语言支持工具和软件包管理器:Node.js、Python 或 Java。有关推荐的工具,请参阅代码编写工具

步骤 1.创建 S3 存储桶并上传示例对象

按照以下步骤创建 S3 存储桶并上传对象。

  1. 打开 Amazon S3 控制台

  2. 创建两个 S3 存储桶。目标存储桶必须命名为 source-resized,其中 source 是源存储桶的名称。例如,名为 mybucket 的源存储桶和名为 mybucket-resized 的目标存储桶。

  3. 在源存储桶中,上传一个 .jpg 对象,例如 HappyFace.jpg

    在测试 Lambda 函数之前,必须创建此示例对象。使用 Lambda invoke 命令手动测试函数时,将示例事件数据传递给指定源存储桶名称并将 HappyFace.jpg 指定为新创建对象的函数。

步骤 2.创建 IAM 策略

创建定义 Lambda 函数权限的 IAM 策略。该函数必须具有以下权限:

  • 从源 S3 存储桶获取对象。

  • 将已调整大小的对象放入目标 S3 存储桶。

  • 将日志写入 Amazon CloudWatch Logs。

创建 IAM 策略

  1. 在 IAM 控制台中打开 Policies(策略)页

  2. 选择 Create policy

  3. 选择 JSON 选项卡,然后粘贴以下策略。请务必将 mybucket 替换为您之前创建的源存储桶的名称。

    { "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Action": [ "logs:PutLogEvents", "logs:CreateLogGroup", "logs:CreateLogStream" ], "Resource": "arn:aws-cn:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": "arn:aws-cn:s3:::mybucket/*" }, { "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": "arn:aws-cn:s3:::mybucket-resized/*" } ] }
  4. 选择下一步: 标签

  5. 选择 下一步: 审核

  6. Review policy(查看策略)下,为 Name(名称)输入 AWSLambdaS3Policy

  7. 选择 Create policy

步驟 3.创建执行角色

创建执行角色,向您的 Lambda 函数授予访问 Amazon 资源的权限。

创建执行角色

  1. 在 IAM 控制台中打开“角色”页面。

  2. 选择 Create role (创建角色)

  3. 创建具有以下属性的角色:

    • 可信任的实体Lambda

    • 权限策略AWSLambdaS3Policy

    • 角色名称lambda-s3-role

步骤 4.创建函数代码

在下面的代码示例中,Amazon S3 事件包含源 S3 存储桶名称和对象密钥键。如果对象是 .jpg 或 .png 图像文件,它会从源存储桶读取图像,生成缩略图,然后将缩略图保存到目标 S3 存储桶。

请注意以下几点:

  • 该代码假定目标存储桶存在,且其名称是源存储桶名称和 -resized 的联接。

  • 对于创建的每个缩略图文件,Lambda 函数代码将对象键名称派生为 resized- 和源对象键名称的联接。例如,如果源对象键名称为 sample.jpg,则代码会创建具有键 resized-sample.jpg 的缩略图对象。

Node.js

将以下代码示例复制到名为 index.js 的文件中。

例 index.js

// dependencies const AWS = require('aws-sdk'); const util = require('util'); const sharp = require('sharp'); // get reference to S3 client const s3 = new AWS.S3(); exports.handler = async (event, context, callback) => { // Read options from the event parameter. console.log("Reading options from event:\n", util.inspect(event, {depth: 5})); const srcBucket = event.Records[0].s3.bucket.name; // Object key may have spaces or unicode non-ASCII characters. const srcKey = decodeURIComponent(event.Records[0].s3.object.key.replace(/\+/g, " ")); const dstBucket = srcBucket + "-resized"; const dstKey = "resized-" + srcKey; // Infer the image type from the file suffix. const typeMatch = srcKey.match(/\.([^.]*)$/); if (!typeMatch) { console.log("Could not determine the image type."); return; } // Check that the image type is supported const imageType = typeMatch[1].toLowerCase(); if (imageType != "jpg" && imageType != "png") { console.log(`Unsupported image type: ${imageType}`); return; } // Download the image from the S3 source bucket. try { const params = { Bucket: srcBucket, Key: srcKey }; var origimage = await s3.getObject(params).promise(); } catch (error) { console.log(error); return; } // set thumbnail width. Resize will set the height automatically to maintain aspect ratio. const width = 200; // Use the sharp module to resize the image and save in a buffer. try { var buffer = await sharp(origimage.Body).resize(width).toBuffer(); } catch (error) { console.log(error); return; } // Upload the thumbnail image to the destination bucket try { const destparams = { Bucket: dstBucket, Key: dstKey, Body: buffer, ContentType: "image" }; const putResult = await s3.putObject(destparams).promise(); } catch (error) { console.log(error); return; } console.log('Successfully resized ' + srcBucket + '/' + srcKey + ' and uploaded to ' + dstBucket + '/' + dstKey); };
Python

将以下代码示例复制到名为 lambda_function.py 的文件中。

例 lambda_function.py

import boto3 import os import sys import uuid from urllib.parse import unquote_plus from PIL import Image import PIL.Image s3_client = boto3.client('s3') def resize_image(image_path, resized_path): with Image.open(image_path) as image: image.thumbnail(tuple(x / 2 for x in image.size)) image.save(resized_path) def lambda_handler(event, context): for record in event['Records']: bucket = record['s3']['bucket']['name'] key = unquote_plus(record['s3']['object']['key']) tmpkey = key.replace('/', '') download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey) upload_path = '/tmp/resized-{}'.format(tmpkey) s3_client.download_file(bucket, key, download_path) resize_image(download_path, upload_path) s3_client.upload_file(upload_path, '{}-resized'.format(bucket), key)
Java

Java 代码实现 aws-lambda-java-core 库中提供的 RequestHandler 接口。创建 Lambda 函数时,您可以将该类指定为处理程序(在此代码示例中为 example.handler)。有关使用接口提供处理程序的更多信息,请参阅处理程序接口

处理程序使用 S3Event 作为输入类型,它为函数代码从传入 Amazon S3 事件读取信息提供了便利的方法。Amazon S3 异步调用 Lambda 函数。由于您实现的接口要求您指定返回类型,因此处理程序使用 String 作为返回类型。

将以下代码示例复制到名为 Handler.java 的文件中。

例 Handler.java

package example; import java.awt.Color; import java.awt.Graphics2D; import java.awt.RenderingHints; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.imageio.ImageIO; import com.amazonaws.AmazonServiceException; import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.RequestHandler; import com.amazonaws.services.lambda.runtime.events.S3Event; import com.amazonaws.services.s3.AmazonS3; import com.amazonaws.services.s3.event.S3EventNotification.S3EventNotificationRecord; import com.amazonaws.services.s3.model.GetObjectRequest; import com.amazonaws.services.s3.model.ObjectMetadata; import com.amazonaws.services.s3.model.S3Object; import com.amazonaws.services.s3.AmazonS3ClientBuilder; public class Handler implements RequestHandler<S3Event, String> { private static final float MAX_WIDTH = 100; private static final float MAX_HEIGHT = 100; private final String JPG_TYPE = (String) "jpg"; private final String JPG_MIME = (String) "image/jpeg"; private final String PNG_TYPE = (String) "png"; private final String PNG_MIME = (String) "image/png"; public String handleRequest(S3Event s3event, Context context) { try { S3EventNotificationRecord record = s3event.getRecords().get(0); String srcBucket = record.getS3().getBucket().getName(); // Object key may have spaces or unicode non-ASCII characters. String srcKey = record.getS3().getObject().getUrlDecodedKey(); String dstBucket = srcBucket + "-resized"; String dstKey = "resized-" + srcKey; // Sanity check: validate that source and destination are different // buckets. if (srcBucket.equals(dstBucket)) { System.out .println("Destination bucket must not match source bucket."); return ""; } // Infer the image type. Matcher matcher = Pattern.compile(".*\\.([^\\.]*)").matcher(srcKey); if (!matcher.matches()) { System.out.println("Unable to infer image type for key " + srcKey); return ""; } String imageType = matcher.group(1); if (!(JPG_TYPE.equals(imageType)) && !(PNG_TYPE.equals(imageType))) { System.out.println("Skipping non-image " + srcKey); return ""; } // Download the image from S3 into a stream AmazonS3 s3Client = AmazonS3ClientBuilder.defaultClient(); S3Object s3Object = s3Client.getObject(new GetObjectRequest( srcBucket, srcKey)); InputStream objectData = s3Object.getObjectContent(); // Read the source image BufferedImage srcImage = ImageIO.read(objectData); int srcHeight = srcImage.getHeight(); int srcWidth = srcImage.getWidth(); // Infer the scaling factor to avoid stretching the image // unnaturally float scalingFactor = Math.min(MAX_WIDTH / srcWidth, MAX_HEIGHT / srcHeight); int width = (int) (scalingFactor * srcWidth); int height = (int) (scalingFactor * srcHeight); BufferedImage resizedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); Graphics2D g = resizedImage.createGraphics(); // Fill with white before applying semi-transparent (alpha) images g.setPaint(Color.white); g.fillRect(0, 0, width, height); // Simple bilinear resize g.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR); g.drawImage(srcImage, 0, 0, width, height, null); g.dispose(); // Re-encode image to target format ByteArrayOutputStream os = new ByteArrayOutputStream(); ImageIO.write(resizedImage, imageType, os); InputStream is = new ByteArrayInputStream(os.toByteArray()); // Set Content-Length and Content-Type ObjectMetadata meta = new ObjectMetadata(); meta.setContentLength(os.size()); if (JPG_TYPE.equals(imageType)) { meta.setContentType(JPG_MIME); } if (PNG_TYPE.equals(imageType)) { meta.setContentType(PNG_MIME); } // Uploading to S3 destination bucket System.out.println("Writing to: " + dstBucket + "/" + dstKey); try { s3Client.putObject(dstBucket, dstKey, is, meta); } catch(AmazonServiceException e) { System.err.println(e.getErrorMessage()); System.exit(1); } System.out.println("Successfully resized " + srcBucket + "/" + srcKey + " and uploaded to " + dstBucket + "/" + dstKey); return "Ok"; } catch (IOException e) { throw new RuntimeException(e); } } }

步骤 5.创建部署程序包

部署程序包是包含 Lambda 函数代码及其依赖项的 .zip 文件存档

Node.js

示例函数必须在部署包中包含 sharp 模块。

创建部署程序包

  1. 在 Linux 环境中打开命令行终端或 shell。确保本地环境中的 Node.js 版本与函数的 Node.js 版本相匹配。

  2. 在名为 lambda-s3 的目录中将函数代码保存为 index.js

  3. 使用 npm 安装 sharp 库。对于 Linux,请使用以下命令:

    npm install sharp

    完成此步骤后,您将拥有以下目录结构:

    lambda-s3 |- index.js |- /node_modules/sharp └ /node_modules/...
  4. 创建包含函数代码及其依赖项的部署包。为 zip 命令设置 -r (递归)选项以压缩子文件夹。

    zip -r function.zip .
Python

附属物

创建部署程序包

  • 我们建议使用 Amazon SAM CLI sam build 命令和 --use-container 选项创建包含使用 C 或 C++ 编写的库的部署程序包,例如 Pillow (PIL) 库。

Java

附属物

  • aws-lambda-java-core

  • aws-lambda-java-events

  • aws-java-sdk

创建部署程序包

步骤 6.创建 Lambda 函数

创建函数

  • 使用 create-function 命令创建 Lambda 函数。

    Node.js
    aws lambda create-function --function-name CreateThumbnail \ --zip-file fileb://function.zip --handler index.handler --runtime nodejs12.x \ --timeout 10 --memory-size 1024 \ --role arn:aws-cn:iam::123456789012:role/lambda-s3-role
    注意

    如果您使用的是 Amazon CLI 版本 2,请添加以下命令参数:

    --cli-binary-format raw-in-base64-out

    create-function 命令将函数处理程序指定为 index.handler。此处理程序名称将函数名称表示为 handler,存储处理程序代码的文件名为 index.js。有关更多信息,请参阅Node.js 中的 Amazon Lambda 函数处理程序。该命令指定了 nodejs12.x 的运行时。有关更多信息,请参阅 Lambda 运行时

    Python
    aws lambda create-function --function-name CreateThumbnail \ --zip-file fileb://function.zip --handler lambda_function.lambda_handler --runtime python3.8 \ --timeout 10 --memory-size 1024 \ --role arn:aws-cn:iam::123456789012:role/lambda-s3-role
    注意

    如果您使用的是 Amazon CLI 版本 2,请添加以下命令参数:

    --cli-binary-format raw-in-base64-out

    create-function 命令将函数处理程序指定为 lambda_function.lambda_handler。此处理程序名称将函数名称表示为 lambda_handler,存储处理程序代码的文件名为 lambda_function.py。有关更多信息,请参阅 Python 中的 Lambda 函数处理程序。该命令指定了 python3.8 的运行时。有关更多信息,请参阅 Lambda 运行时

    Java
    aws lambda create-function --function-name CreateThumbnail \ --zip-file fileb://function.zip --handler example.handler --runtime java11 \ --timeout 10 --memory-size 1024 \ --role arn:aws-cn:iam::123456789012:role/lambda-s3-role
    注意

    如果您使用的是 Amazon CLI 版本 2,请添加以下命令参数:

    --cli-binary-format raw-in-base64-out

    create-function 命令将函数处理程序指定为 example.handler。该函数可以使用 package.Class 的缩写处理程序格式,因为该函数实现了处理程序接口。有关更多信息,请参阅 Java 中的 Amazon Lambda 函数处理程序。该命令指定了 java11 的运行时。有关更多信息,请参阅 Lambda 运行时

对于角色参数,请将 123456789012 替换为您的 Amazon 账户 ID。之前的示例命令指定 10 秒超时值作为函数配置。根据上传的对象的大小,可能需要使用下面的 Amazon CLI 命令增大超时值:

aws lambda update-function-configuration --function-name CreateThumbnail --timeout 30

步骤 7.测试 Lambda 函数

使用示例 Amazon S3 事件数据手动调用 Lambda 函数。

测试 Lambda 函数

  1. 将下面的 Amazon S3 示例事件数据保存到名为 inputFile.txt 的文件中。请务必将 sourcebucketHappyFace.jpg 分别替换为源 S3 存储桶名称和 .jpg 对象键。

    { "Records":[ { "eventVersion":"2.0", "eventSource":"aws:s3", "awsRegion":"us-west-2", "eventTime":"1970-01-01T00:00:00.000Z", "eventName":"ObjectCreated:Put", "userIdentity":{ "principalId":"AIDAJDPLRKLG7UEXAMPLE" }, "requestParameters":{ "sourceIPAddress":"127.0.0.1" }, "responseElements":{ "x-amz-request-id":"C3D13FE58DE4C810", "x-amz-id-2":"FMyUVURIY8/IgAtTv8xRjskZQpcIZ9KG4V5Wp6S7S/JRWeUWerMUE5JgHvANOjpD" }, "s3":{ "s3SchemaVersion":"1.0", "configurationId":"testConfigRule", "bucket":{ "name":"sourcebucket", "ownerIdentity":{ "principalId":"A3NL1KOZZKExample" }, "arn":"arn:aws-cn:s3:::sourcebucket" }, "object":{ "key":"HappyFace.jpg", "size":1024, "eTag":"d41d8cd98f00b204e9800998ecf8427e", "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko" } } } ] }
  2. 使用以下 invoke 命令调用函数。请注意,该命令会请求异步执行 (--invocation-type Event)。或者,您可以通过将 RequestResponse 指定为 invocation-type 参数值来同步调用函数。

    aws lambda invoke --function-name CreateThumbnail --invocation-type Event \ --payload file://inputFile.txt outputfile.txt
    注意

    如果您使用的是 Amazon CLI 版本 2,请添加以下命令参数:

    --cli-binary-format raw-in-base64-out
  3. 验证目标 S3 存储桶中已创建缩略图。

步骤 8。配置 Amazon S3 以发布事件

完成配置,以使 Amazon S3 能够向 Lambda 发布对象创建事件并调用 Lambda 函数。在此步骤中,您将执行以下操作:

  • 向函数访问策略添加权限以允许 Amazon S3 调用该函数。

  • 向源 S3 存储桶添加通知配置。在通知配置中,您需要提供以下内容:

    • 需要 Amazon S3 发布事件的事件类型。在本教程中,指定 s3:ObjectCreated:* 事件类型,以便 Amazon S3 在创建对象时发布事件。

    • 要调用的函数。

向函数策略添加权限

  1. 运行下面的 add-permission 命令以向 Amazon S3 服务委托人 (s3.amazonaws.com) 授予执行 lambda:InvokeFunction 操作的权限。请注意,向 Amazon S3 授予权限,使其只能在满足以下条件时调用该函数:

    • 在特定的 S3 存储桶上检测到对象创建事件。

    • 该 S3 存储桶由您的 Amazon 账户拥有。如果删除一个存储桶,则另一个 Amazon 账户可能会创建具有相同 Amazon 资源名称 (ARN) 的存储桶。

    aws lambda add-permission --function-name CreateThumbnail --principal s3.amazonaws.com \ --statement-id s3invoke --action "lambda:InvokeFunction" \ --source-arn arn:aws-cn:s3:::sourcebucket \ --source-account account-id
  2. 通过运行 get-policy 命令验证函数的访问策略。

    aws lambda get-policy --function-name CreateThumbnail

要使 Amazon S3 将对象创建的事件发布到 Lambda,请在源 S3 存储桶上添加通知配置。

重要

此程序将 S3 存储桶配置为每次在该存储桶中创建对象时调用您的函数。请确保仅在源存储桶上配置此选项。不要让您的函数在源存储桶中创建对象,否则您的函数可能会导致自己在循环中被连续调用

配置通知

  1. 打开 Amazon S3 控制台

  2. 选择源 S3 存储桶的名称。

  3. 选择 Properties 选项卡。

  4. Event notifications(事件通知)下,选择 Create event notification(创建事件通知)以配置采用以下设置的通知:

    • 事件名称lambda-trigger

    • 事件类型All object create events

    • 目标Lambda function

    • Lambda 函数CreateThumbnail

有关事件配置的更多信息,请参阅 Amazon Simple Storage Service 控制台用户指南 中的使用 Amazon S3 控制台启用和配置事件通知

第 9 步。使用 S3 触发器测试

按如下所示测试设置:

  1. 使用 Amazon S3 控制台将 .jpg 或 .png 对象上传到源 S3 存储桶。

  2. 使用 CreateThumbnail Lambda 函数验证每个图像对象都已在目标 S3 存储桶中创建了缩略图。

  3. CloudWatch 控制台中查看日志。

第 10 步。清除资源

如果您不想保留为本教程创建的资源,可以立即将其删除。通过删除您不再使用的 Amazon 资源,可防止您的 Amazon 账户产生不必要的费用。

删除 Lambda 函数

  1. 打开 Lambda 控制台的“函数”页面

  2. 选择您创建的函数。

  3. 依次选择 ActionsDelete

  4. 选择删除

删除您创建的策略

  1. 打开 IAM 控制台的 Policies (策略) 页面。

  2. 选择您创建的策略 (AWSLambdaS3Policy)。

  3. 选择 Policy actions(策略操作)、Delete(删除)。

  4. 选择删除

删除执行角色

  1. 打开 IAM 控制台的“角色”页面

  2. 选择您创建的执行角色。

  3. 选择删除角色

  4. 选择 Yes, delete (是,删除)

删除 S3 存储桶

  1. 打开 Amazon S3 控制台

  2. 选择您创建的存储桶。

  3. 选择 Delete

  4. 在文本框中输入存储桶的名称。

  5. 选择 Confirm