使用 Amazon S3 对象 Lambda 在检索图像时动态添加水印

教程

概览

借助 Amazon S3 对象 Lambda,您可以将自己的代码添加到 S3 GET、HEAD 和 LIST 请求中,以便在数据返回到应用程序时修改数据。您可以使用自定义代码来修改 S3 GET 请求返回的数据,以便实施转换数据格式(例如,XML 转 JSON)、动态调整图像大小、隐去机密数据等操作。您还可以使用 S3 Object Lambda 来修改 S3 LIST 请求的输出,以创建存储桶中对象的自定义视图,并使用 S3 HEAD 请求修改对象元数据(如对象名称和大小)。

本教程旨在向您展示如何开始使用 Amazon S3 对象 Lambda。许多组织将图像存储在 Amazon S3 中,可由具有独特数据格式要求的不同应用程序访问。在某些情况下,可能需要修改图像以包含水印,具体取决于访问图像的用户(例如,付费订阅用户可以查看没有水印的图像,而非付费用户则会收到带水印的图像)。

在本教程中,我们将使用 S3 对象 Lambda 为从 Amazon S3 检索的图像添加水印。可以使用 S3 对象 Lambda 在从 Amazon S3 检索数据时修改数据,而无需更改现有对象或保留数据的多个衍生副本。通过呈现相同数据的多个视图且无需存储衍生副本,可以节省存储成本。

您将学到的内容

在本教程中,您将:

  • 创建 Amazon S3 桶
  • 创建 S3 访问点
  • 创建 AWS Lambda 函数来修改图像
  • 创建 S3 对象 Lambda 访问点

先决条件

您需要一个 AWS 账户才能完成本教程。访问此支持页面,了解有关如何创建和激活新 AWS 账户的详细信息。

您可以为本教程创建 IAM 用户,也可以为现有 IAM 用户添加权限。要完成本教程,您的 IAM 用户必须包含以下权限才能访问相关 AWS 资源和执行特定操作:

  • s3:CreateBucket
  • s3:PutObject
  • s3:GetObject
  • s3:ListBucket
  • s3:CreateAccessPoint
  • s3:CreateAccessPointForObjectLambda
  • s3-object-lambda:WriteGetObjectResponse
  • lambda:CreateFunction
  • lambda:InvokeFunction
  • iam:AttachRolePolicy
  • iam:CreateRole
  • iam:PutRolePolicy

要清理在本教程中创建的资源,您需要以下 IAM 权限:

  • s3:DeleteBucket
  • s3:DeleteAccessPoint
  • s3:DeleteAccessPointForObjectLambda
  • lambda:DeleteFunction
  • iam:DeleteRole

 

 AWS 使用经验

新手

 完成时间

20 分钟

 所需费用

低于 1 美元(Amazon S3 定价页面)

 需要

AWS 账户*

*过去 24 小时内创建的账户可能尚不具有访问此教程所需资源的权限。

 使用的服务

 上次更新日期

2023 年 2 月 1 日

先决条件

您需要一个 AWS 账户才能完成本教程。访问此支持页面,了解有关如何创建和激活新 AWS 账户的详细信息。

您可以为本教程创建 IAM 用户,也可以为现有 IAM 用户添加权限。要完成本教程,您的 IAM 用户必须包含以下权限才能访问相关 AWS 资源和执行特定操作: 

实施

步骤 1:创建 Amazon S3 存储桶

1.1 — 登录 Amazon S3 控制台

1.2 — 创建 S3 存储桶

  • 从左侧导航窗格的 Amazon S3 菜单中选择存储桶,然后选择创建存储桶按钮。

1.3

  • 在存储桶名称字段中,为您的存储桶输入一个全局唯一的描述性名称。选择您希望在其中创建存储桶的 AWS 区域。在本教程的后面部分中,我们将创建另一个资源,该资源必须位于同一 AWS 区域。
  • 您可以为其余选项保留保留默认选择。导航到页面底部,然后选择创建存储桶。

步骤 2:上传对象

现在,您的存储桶已创建并配置完毕,您可以立即上传图像。

2.1 — 上传对象

  • 从可用存储桶列表中,选择您刚创建的存储桶的存储桶名称。

2.2

  • 接下来,确保已选定对象选项卡。然后在对象部分中,选择上传按钮。

2.3 — 添加文件

  • 选择添加文件按钮,然后从文件浏览器中选择要上传的图像。
  • 如果你愿意,你可以上传这张示例图像。

2.4 — 上传

  • 向下浏览页面,然后选择上传按钮。

2.5

  • 上传完成并成功后,选择 关闭按钮。

步骤 3:创建 S3 访问点

创建一个 Amazon S3 访问点,用于支持 S3 对象 Lambda 访问点(我们将在本教程的后面部分创建)。

3.1 — 创建 S3 访问点

  • 导航到 S3 控制台,然后在左侧导航窗格中选择访问点菜单选项。然后,选择 创建访问点按钮。

3.2

  • 在 属性部分中,输入所需的访问点名称,然后选择浏览 S3 按钮,选择您在步骤 1 中输入的存储桶名称。接下来,将网络源设置为互联网。

3.3

  • 所有其他默认设置保留不变。导航到页面底部,然后选择创建访问点按钮。

3.4

  • 现在,当您导航到左侧导航窗格中的访问点时,S3 访问点将出现在列表中。

步骤 4:创建 Lambda 函数

  • 接下来,创建一个 Lambda 函数,该函数将在通过 S3 对象 Lambda 访问点发出 S3 GET 请求时调用。
  • 我们将使用 AWS 管理控制台中的 AWS CloudShell 来构建和测试 S3 对象 Lambda。如果您满足以下要求,则可以使用自己的计算机或 AWS Cloud9 实例来构建解决方案:
    - 最新版本的 AWS 命令行界面(CLI)
    - 用于创建 AWS Lambda 函数/层和 IAM 角色的凭证
    - Python 3.9
    - zip 实用程序
    - jq 实用程序

4.1 — 启动 CloudShell 终端

选择 AWS 管理控制台右上角菜单中的 CloudShell 图标。

如果出现 CloudShell 简介窗口,可以随时阅读内容并选择关闭。

将使用 CloudShell 终端打开一个新的浏览器选项卡(类似于以下屏幕截图):

4.2 — 为 CloudShell 做好部署 Lambda 函数的准备

  • 在 CloudShell 中运行以下代码以准备好环境并使用 Pillow 模块部署 Lambda 层。将以下代码复制并粘贴到 CloudShell 中,以安装所需的依赖项并部署 Lambda 函数。
# Install the required libraries to build new python
sudo yum install gcc openssl-devel bzip2-devel libffi-devel -y
# Install Pyenv
curl https://pyenv.run | bash
echo 'export PYENV_ROOT="$HOME/.pyenv"' >> ~/.bash_profile
echo 'export PATH="$PYENV_ROOT/bin:$PATH"' >> ~/.bash_profile
echo 'eval "$(pyenv init -)"' >> ~/.bash_profile
source ~/.bash_profile

# Install Python version 3.9
pyenv install 3.9.13
pyenv global 3.9.13

# Build the pillow Lambda layer
mkdir python
cd python
pip install pillow -t .
cd ..
zip -r9 pillow.zip python/
aws lambda publish-layer-version \
    --layer-name Pillow \
    --description "Python Image Library" \
    --license-info "HPND" \
    --zip-file fileb://pillow.zip \
    --compatible-runtimes python3.9

注意:复制和粘贴代码时,CloudShell 将打开一个警告窗口,以确认您要粘贴多行代码。选择粘贴。

此步骤可能需要 10-15 分钟才能完成。

4.3 — 构建 Lambda 函数

  • 下载 TrueType 字体,Lambda 函数将使用该字体为图像添加水印。将以下命令复制并粘贴到 CloudShell 中。
wget https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/branding/Amazon_Typefaces_Complete_Font_Set_Mar2020.zip
  • 提取将用于在图像中写入带水印的文本的 TrueType 字体。
unzip -oj Amazon_Typefaces_Complete_Font_Set_Mar2020.zip "Amazon_Typefaces_Complete_Font_Set_Mar2020/Ember/AmazonEmber_Rg.ttf"
  • 创建用于处理 S3 对象 Lambda 请求的 Lambda 代码。
cat << EOF > lambda.py
import boto3
import json
import os
import logging
from io import BytesIO
from PIL import Image, ImageDraw, ImageFont
from urllib import request
from urllib.parse import urlparse, parse_qs, unquote
from urllib.error import HTTPError
from typing import Optional

logger = logging.getLogger('S3-img-processing')
logger.addHandler(logging.StreamHandler())
logger.setLevel(getattr(logging, os.getenv('LOG_LEVEL', 'INFO')))
FILE_EXT = {
    'JPEG': ['.jpg', '.jpeg'],
    'PNG': ['.png'],
    'TIFF': ['.tif']
}
OPACITY = 64  # 0 = transparent and 255 = full solid


def get_img_encoding(file_ext: str) -> Optional[str]:
    result = None
    for key, value in FILE_EXT.items():
        if file_ext in value:
            result = key
            break
    return result


def add_watermark(img: Image, text: str) -> Image:
    font = ImageFont.truetype("AmazonEmber_Rg.ttf", 82)
    txt = Image.new('RGBA', img.size, (255, 255, 255, 0))
    if img.mode != 'RGBA':
        image = img.convert('RGBA')
    else:
        image = img

    d = ImageDraw.Draw(txt)
    # Positioning Text
    width, height = image.size
    text_width, text_height = d.textsize(text, font)
    x = width / 2 - text_width / 2
    y = height / 2 - text_height / 2
    # Applying Text
    d.text((x, y), text, fill=(255, 255, 255, OPACITY), font=font)
    # Combining Original Image with Text and Saving
    watermarked = Image.alpha_composite(image, txt)
    return watermarked


def handler(event, context) -> dict:
    logger.debug(json.dumps(event))
    object_context = event["getObjectContext"]
    # Get the presigned URL to fetch the requested original object
    # from S3
    s3_url = object_context["inputS3Url"]
    # Extract the route and request token from the input context
    request_route = object_context["outputRoute"]
    request_token = object_context["outputToken"]
    parsed_url = urlparse(event['userRequest']['url'])
    object_key = parsed_url.path
    logger.info(f'Object to retrieve: {object_key}')
    parsed_qs = parse_qs(parsed_url.query)
    for k, v in parsed_qs.items():
        parsed_qs[k][0] = unquote(v[0])

    filename = os.path.splitext(os.path.basename(object_key))
    # Get the original S3 object using the presigned URL
    req = request.Request(s3_url)
    try:
        response = request.urlopen(req)
    except HTTPError as e:
        logger.info(f'Error downloading the object. Error code: {e.code}')
        logger.exception(e.read())
        return {'status_code': e.code}

    if encoding := get_img_encoding(filename[1].lower()):
        logger.info(f'Compatible Image format found! Processing image: {"".join(filename)}')
        img = Image.open(response)
        logger.debug(f'Image format: {img.format}')
        logger.debug(f'Image mode: {img.mode}')
        logger.debug(f'Image Width: {img.width}')
        logger.debug(f'Image Height: {img.height}')

        img_result = add_watermark(img, parsed_qs.get('X-Amz-watermark', ['Watermark'])[0])
        img_bytes = BytesIO()

        if img.mode != 'RGBA':
            # Watermark added an Alpha channel that is not compatible with JPEG. We need to convert to RGB to save
            img_result = img_result.convert('RGB')
            img_result.save(img_bytes, format='JPEG')
        else:
            # Will use the original image format (PNG, GIF, TIFF, etc.)
            img_result.save(img_bytes, encoding)
        img_bytes.seek(0)
        transformed_object = img_bytes.read()

    else:
        logger.info(f'File format not compatible. Bypass file: {"".join(filename)}')
        transformed_object = response.read()

    # Write object back to S3 Object Lambda
    s3 = boto3.client('s3')
    # The WriteGetObjectResponse API sends the transformed data
    if os.getenv('AWS_EXECUTION_ENV'):
        s3.write_get_object_response(
            Body=transformed_object,
            RequestRoute=request_route,
            RequestToken=request_token)
    else:
        # Running in a local environment. Saving the file locally
        with open(f'myImage{filename[1]}', 'wb') as f:
            logger.debug(f'Writing file: myImage{filename[1]} to the local filesystem')
            f.write(transformed_object)

    # Exit the Lambda function: return the status code
    return {'status_code': 200}
EOF
  • 创建包含 Python 代码和 TrueType 字体文件的 Lambda 压缩文件。
zip -r9 lambda.zip lambda.py AmazonEmber_Rg.ttf
  • 创建附加到 Lambda 函数的 IAM 角色。
aws iam create-role --role-name ol-lambda-images --assume-role-policy-document '{"Version": "2012-10-17","Statement": [{"Effect": "Allow", "Principal": {"Service": "lambda.amazonaws.com"}, "Action": "sts:AssumeRole"}]}'
  • 将预定义的 IAM policy 附加到之前创建的 IAM 角色。此策略包含运行 Lambda 函数所需的最低权限。
aws iam attach-role-policy --role-name ol-lambda-images --policy-arn arn:aws:iam::aws:policy/service-role/AmazonS3ObjectLambdaExecutionRolePolicy

export OL_LAMBDA_ROLE=$(aws iam get-role --role-name ol-lambda-images | jq -r .Role.Arn)

export LAMBDA_LAYER=$(aws lambda list-layers --query 'Layers[?contains(LayerName, `Pillow`) == `true`].LatestMatchingVersion.LayerVersionArn' | jq -r .[])
  • 创建并上传 Lambda 函数。
aws lambda create-function --function-name ol_image_processing \
 --zip-file fileb://lambda.zip --handler lambda.handler --runtime python3.9 \
 --role $OL_LAMBDA_ROLE \
 --layers $LAMBDA_LAYER \
 --memory-size 1024

步骤 5:创建 S3 对象 Lambda 访问点

创建 S3 对象 Lambda 访问点,用于访问存储在 S3 存储桶中的图像。

5.1 — 创建 S3 对象 Lambda 访问点

在常规部分,为对象 Lambda 访问点名称输入 ol-amazon-s3-images-guide。

确保 S3 对象 Lambda 访问点的 AWS 区域与您在步骤 1.3 中创建 S3 存储桶时指定的 AWS 区域相匹配。

对于支持访问点,使用浏览 S3 按钮指定您在步骤 3.2 中创建的 S3 访问点的 Amazon 资源名称(ARN)。

向下导航以查看转换配置。在 S3 API 列表中,选择 GetObject 选项。

在 Lambda 函数下,指定 ol_image_processing。

接下来,导航到页面底部并选择创建对象 Lambda 访问点。

步骤 6:从 S3 对象 Lambda 访问点下载图像

创建 S3 对象 Lambda 访问点后,我们将打开图像以验证在请求期间是否正确添加了水印。

6.1 — 打开 S3 对象 Lambda 访问点

  • 在 S3 控制台左侧导航窗格中选择 对象 Lambda 访问点,然后选择您在步骤 5.1 中创建的 S3 对象 Lambda 访问点,从而返回 S3 对象 Lambda 访问点列表。在此示例中,我们选择 ol-amazon-s3-images-guide 作为 S3 对象 Lambda 访问点。

选择您在步骤 2.4 中上传的图片,然后选择打开按钮。

此时将打开一个新的浏览器选项卡,其中包含您的图像和水印。
 
从 S3 对象 Lambda 访问点下载的所有兼容图像现在都将包含带水印的文本。


6.2 — 从 AWS CLI 下载转换后的图像

  • 您也可以使用 AWS CLI 下载图像。为此,您需要提供 S3 对象 Lambda 访问点的 Amazon 资源名称(ARN)。在 S3 控制台中,导航到对象 Lambda 访问点页面,选择 S3 对象 Lambda 访问点的名称,选择属性选项卡,然后选择 Amazon 资源名称(ARN)下方的复制图标。

6.3 — 从 CloudShell 运行 AWS CLI 命令

在 CloudShell 浏览器选项卡中,输入以下内容:

aws s3api get-object --bucket <paste the ARN copied above here> --key <image filename here> <filename to write here>

6.4 — 将图像下载到本地计算机

在 CloudShell 中,选择右上角的操作,然后选择下载文件。

从 S3 对象 Lambda 访问点下载图像时,输入您在在步骤 6.3 中定义的文件名,然后选择下载。

现在,您可以从本地计算机打开图像。

注意:图像查看器可能因计算机和操作系统而异。如果您不确定要使用哪个应用程序打开图像,请咨询您的管理员。

步骤 7:清理资源

接下来,您将清理在本教程中创建的资源。最好删除不再使用的资源,以免产生意外费用。

7.1 — 删除 S3 对象 Lambda 访问点

  • 导航到 S3 控制台,然后在左侧导航窗格中选择对象 Lambda 访问点。
  • 在对象 Lambda 访问点页面上,选择您在步骤 5.1 中创建的 S3 对象 Lambda 访问点左侧的单选按钮。

选择删除。

在显示的文本字段中输入 S3 对象 Lambda 访问点的名称,确认您要删除该 S3 对象 Lambda 访问点,然后选择删除。

7.2 — 删除 S3 访问点

  • 在 S3 控制台的左侧导航窗格中选择访问点。
  • 导航到您在步骤 3.1 中创建的 S3 访问点,然后选择 S3 访问点名称旁边的单选按钮。
  • 选择删除。

在显示的文本字段中输入访问点的名称,确认您要删除该访问点,然后选择删除。

7.3 — 删除测试对象

  • 导航到 S3 控制台,然后在左侧导航窗格中选择存储桶菜单选项。首先,您需要从测试存储桶中删除测试对象。选择您在本教程中使用的存储桶的名称。
  • 选中测试对象名称左侧的复选框,然后选择删除按钮。
  • 在删除对象页面上,确认您已选中要删除的正确对象,并在永久删除对象确认框中输入 delete。然后,选择删除对象按钮以继续。
接下来,您将看到一条横幅,指明删除是否成功。

7.4 — 删除 S3 存储桶

  • 接下来,从左侧导航窗格的 S3 控制台菜单中选择存储桶。选择为本教程创建的源存储桶左侧的单选按钮,然后选择删除按钮。

查看警告消息。如果要继续删除此存储桶,请在删除存储桶确认框中输入存储桶的名称,然后选择删除存储桶。

7.5 — 删除 Lambda 函数

  • 在 AWS Lambda 控制台中的左侧导航窗格中选择函数。
  • 选中您在步骤 4.3 中创建的函数名称左侧的复选框。
  • 选择操作,然后选择删除。在 删除函数对话框中,选择 “删除”。

结论

恭喜! 您学习了如何使用 Amazon S3 对象 Lambda 在检索图像时为其动态添加水印,将处理后的图像传送回请求客户端。您可以根据自己的应用场景自定义 Lambda 函数,以修改 S3 GET、HEAD 和 LIST 请求返回的数据,常见应用场景包括使用特定于调用方的详细信息自定义水印、屏蔽敏感数据、筛选某些数据行、使用来自其他数据库的信息增强数据、转换数据格式等等。

此页内容对您是否有帮助?

后续步骤