使用 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 资源和执行特定操作:
-
所需权限
- 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
实施
步骤 1:创建 Amazon S3 存储桶
1.1 — 登录 Amazon S3 控制台
- 登录 AWS 管理控制台并打开 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.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 访问点
5.1 — 创建 S3 对象 Lambda 访问点
- 导航到 S3 控制台,然后在左侧导航窗格中选择对象 Lambda 访问点。选择创建对象 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 访问点下载图像
6.1 — 打开 S3 对象 Lambda 访问点
- 在 S3 控制台左侧导航窗格中选择 对象 Lambda 访问点,然后选择您在步骤 5.1 中创建的 S3 对象 Lambda 访问点,从而返回 S3 对象 Lambda 访问点列表。在此示例中,我们选择 ol-amazon-s3-images-guide 作为 S3 对象 Lambda 访问点。
选择您在步骤 2.4 中上传的图片,然后选择打开按钮。
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 请求返回的数据,常见应用场景包括使用特定于调用方的详细信息自定义水印、屏蔽敏感数据、筛选某些数据行、使用来自其他数据库的信息增强数据、转换数据格式等等。