教學課程:使用 Amazon S3 觸發條件建立縮圖影像 - AWS Lambda

教學課程:使用 Amazon S3 觸發條件建立縮圖影像

在本教學課程中,您將建立 Lambda 函數並設定 Amazon Simple Storage Service (Amazon S3) 的觸發條件。Amazon S3 會針對上傳至 S3 儲存貯體的每個影像檔案調用 CreateThumbnail 函數。此函數會從來源 S3 儲存貯體讀取影像物件,並建立要儲存在目標 S3 儲存貯體中的縮圖影像。

注意

本教學課程需要中等水平的 AWS 和 Lambda 領域知識。建議您先嘗試 教學課程:使用 Amazon S3 觸發條件叫用 Lambda 函數

在本教學課程中,您會使用 AWS Command Line Interface (AWS CLI) 建立下列 AWS 資源:

Lambda 資源

  • Lambda 函數。您可以為函數程式碼選擇 Node.js、Python 或 Java。

  • 此函數的 .zip 檔案封存部署套件。

  • 授與叫用函數之 Amazon S3 許可的存取政策。

AWS Identity and Access Management (IAM) 資源

  • 具有相關許可政策的執行角色,以授與函數所需的許可。

Amazon S3 資源

  • 擁有通知組態的來源 S3 儲存貯體,可叫用函數。

  • 函數可節省調整大小的影像的目標 S3 儲存貯體。

Prerequisites

  • AWS account

    若要使用 Lambda 及其他 AWS 服務,您需要 AWS 帳戶。如果您沒有帳戶,請造訪 aws.amazon.com,並選擇 Create an AWS Account (建立 AWS 帳戶)。如需說明,請參閱如何建立和啟用新的 AWS 帳戶?

  • 命令列

    為了完成以下步驟,您需要命令列終端或 shell 來執行命令。命令和預期的輸出會列在不同的區塊中:

    aws --version

    您應該會看到下列輸出:

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

    對於長命令,逸出字元 (\) 用於將命令分割為多行。

    在 Linux 和 macOS 上,使用您偏好的 shell 和套件軟體管理工具。在 Windows 10 上,您可以安裝適用於 Linux 的 Windows 子系統,以取得 Ubuntu 和 Bash 的 Windows 整合版本。

  • AWS CLI

    在本教學課程中,您將使用 AWS CLI 指令建立和叫用 Lambda 函數。安裝 AWS CLI使用您的 AWS 憑證進行設定

  • 語言工具

    為您想要使用的語言安裝語言支援工具和套件管理員:Node.js、Python 或 Java。如需建議的工具,請參閱 程式碼撰寫工具

步驟 1. 建立 S3 儲存貯體並上傳範例物件

遵照這些步驟建立 S3 儲存貯體,並上傳物件。

  1. 開啟 Amazon S3 主控台

  2. 建立兩個 S3 儲存貯體。必須命名目標儲存貯體為 source-resized,其中來源是來源儲存貯體的名稱。例如,名為 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:logs:*:*:*" }, { "Effect": "Allow", "Action": [ "s3:GetObject" ], "Resource": "arn:aws:s3:::mybucket/*" }, { "Effect": "Allow", "Action": [ "s3:PutObject" ], "Resource": "arn:aws:s3:::mybucket-resized/*" } ] }
  4. 選擇 Next: Tags (下一步:標籤)。

  5. 選擇 Next: Review (下一步:檢閱)。

  6. Review policy (檢閱政策) 下,針對Name (名稱),輸入 AWSLambdaS3Policy

  7. 選擇 Create policy (建立政策)

步驟 3. 建立執行角色

建立執行角色,授予您的 Lambda 函數存取 AWS 資源的許可。

若要建立執行角色

  1. 開啟 IAM 主控台中的 Roles (角色) 頁面

  2. 選擇 Create Role (建立角色)

  3. 建立具備下列屬性的角色:

    • Trusted entity (信任實體) - Lambda

    • Permissions policy (許可政策) - AWSLambdaS3Policy

    • Role name (角色名稱) – 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 程式碼實作 RequestHandler 程式庫中提供的 aws-lambda-java-core 介面。當您建立 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. 在名為 index.js 的目錄中將函數程式碼儲存為 lambda-s3

  3. 透過 npm 安裝 Sharp 程式庫。若為 Linux,請使用下列命令:

    npm install sharp

    此步驟後,您會有以下目錄結構:

    lambda-s3 |- index.js |- /node_modules/sharp └ /node_modules/...
  4. 建立具有函數程式碼和相依項目的部署套件。為 zip 命令設定 -r (遞迴) 選項,以壓縮子資料夾。

    zip -r function.zip .
Python

Dependencies

建立部署套件

  • 我們建議使用 AWS SAM CLI sam build 命令與 --use-container 選項,來建立包含以 C 或 C ++ 所撰寫程式庫 (例如 Pillow (PIL) 程式庫的部署套件。

Java

Dependencies

  • 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:iam::123456789012:role/lambda-s3-role

    如果您使用 AWS CLI 的第 2 版,則需要 cli-binary-format 選項。您也可以在 AWS CLI config 檔案中設定此選項。

    create-function 命令會將函數處理常式指定為 index.handler。此處理常式名稱會將函數名稱反映為 handler,並會將儲存處理常式程式碼的檔案名稱反映為 index.js。如需更多詳細資訊,請參閱 AWS LambdaNode.js 中的 函數處理常式。指令會指定 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:iam::123456789012:role/lambda-s3-role

    如果您使用 AWS CLI 的第 2 版,則需要 cli-binary-format 選項。您也可以在 AWS CLI config 檔案中設定此選項。

    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:iam::123456789012:role/lambda-s3-role

    如果您使用 AWS CLI 的第 2 版,則需要 cli-binary-format 選項。您也可以在 AWS CLI config 檔案中設定此選項。

    create-function 命令會將函數處理常式指定為 example.handler。函數可以使用 package.Class 的縮寫處理常式格式,因為函數會實作處理常式介面。如需更多詳細資訊,請參閱 AWS LambdaJava 中的 函數處理常式。指令會指定 java11 的執行時間。如需更多詳細資訊,請參閱 Lambda 執行時間

針對角色參數,請將 123456789012 取代為您的 AWS 帳戶 ID。前述範例命令會指定 10 秒逾時數值以作為函數組態。視您上傳的物件大小而定,您可能需要使用下列 AWS CLI 命令增加逾時值。

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

步驟 7. 測試 Lambda 函數

使用範例 Amazon S3 事件資料手動叫用 Lambda 函數。

測試 Lambda 函數

  1. 將下列 Amazon S3 範例事件資料儲存在名為 inputFile.txt 的檔案中。確保分別用來源 S3 儲存貯體名稱和 .jpg 物件金鑰替換 sourcebucketHappyFace.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:s3:::sourcebucket" }, "object":{ "key":"HappyFace.jpg", "size":1024, "eTag":"d41d8cd98f00b204e9800998ecf8427e", "versionId":"096fKKXTRTtl3on89fVO.nfljtsv6qko" } } } ] }
  2. 使用以下 invoke 命令叫用函數。請注意,命令要求非同步執行 (--invocation-type Event)。或者,您可以透過指定 RequestResponseinvocation-type 參數值來同步叫用函數。

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

    如果您使用 AWS CLI 的第 2 版,則需要 cli-binary-format 選項。您也可以在 AWS CLI config 檔案中設定此選項。

  3. 驗證目標 S3 儲存貯體中已建立縮圖。

步驟 8. 設定 Amazon S3 以發佈事件

完成組態,以便 Amazon S3 將物件建立的事件發佈至 Lambda 並叫用您的 Lambda 函數。請於本步驟執行以下操作:

  • 將許可新增至函數的存取政策,以允許 Amazon S3 叫用函數。

  • 將通知組態新增至您的來源 S3 儲存貯體。在通知組態中,您提供以下項目:

    • 您要 Amazon S3 為其發佈事件的事件類型。為此教學課程指定 Amazon S3 事件類型,讓 s3:ObjectCreated:* 能夠在建立物件時發佈事件。

    • 要叫用的函數。

新增許可至函數政策

  1. 執行下列 add-permission 命令,對 Amazon S3 服務主體 (s3.amazonaws.com) 授予許可,藉此執行 lambda:InvokeFunction 動作。請注意,許可已授與給 Amazon S3,只讓它在符合下列條件的前提下叫用函數:

    • 在特定 S3 儲存貯體上偵測到物件建立的事件。

    • 您的 AWS 帳戶擁有該 S3 儲存貯體。如果您刪除某個儲存貯體,則另一個 AWS 帳戶可以使用相同的 Amazon 資源名稱 (ARN) 建立儲存貯體。

    aws lambda add-permission --function-name CreateThumbnail --principal s3.amazonaws.com \ --statement-id s3invoke --action "lambda:InvokeFunction" \ --source-arn arn:aws: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 (建立事件通知),使用下列設定來設定通知:

    • Event name (事件名稱) - lambda-trigger

    • Event types (事件類型) - All object create events

    • Destination (目的地) - Lambda function

    • 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. 清除您的資源

除非您想要保留為此教學課程建立的資源,否則您現在便可刪除。透過刪除您不再使用的 AWS 資源,可為 AWS 帳戶避免不必要的費用。

若要刪除 Lambda 函數

  1. 開啟 Lambda 主控台中的 Functions (函數) 頁面

  2. 選擇您建立的函數。

  3. 選擇 Actions (動作),然後選擇 Delete (刪除)

  4. 選擇 Delete (刪除)。

刪除您建立的政策

  1. 開啟 IAM 主控台中的 Policies (政策) 頁面

  2. 選取您建立的政策 (AWSLambdaS3Policy)。

  3. 選擇 Policy actions (政策動作),然後 Delete (刪除)

  4. 選擇 Delete (刪除)。

若要刪除執行角色

  1. 開啟 IAM 主控台中的 Roles page (角色頁面)。

  2. 選取您建立的執行角色。

  3. 選擇 Delete role (刪除角色)。

  4. 選擇 Yes, delete (是,刪除)

刪除 S3 儲存貯體

  1. 開啟 Amazon S3 主控台

  2. 選擇您建立的儲存貯體。

  3. 選擇 Delete (刪除)。

  4. 在文字方塊中輸入儲存貯體的名稱。

  5. 選擇 Confirm (確認)