使用 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 请求返回的数据,常见应用场景包括使用特定于调用方的详细信息自定义水印、屏蔽敏感数据、筛选某些数据行、使用来自其他数据库的信息增强数据、转换数据格式等等。

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

后续步骤