はじめに
この記事では「この前リリースされた機能って実際に動かすとどんな感じなんだろう」とか「もしかしたら内容次第では使えるかも??」などAWSサービスの中でも特定の機能にフォーカスして検証していく記事です。主な内容としては実践したときのメモを中心に書きます。(忘れやすいことなど)
誤りなどがあれば書き直していく予定です。
今回はAmazon S3(以下、S3)が機能として提供しているAmazon S3 Object Lambdaを検証してみます。
参考:Amazon S3 Object Lambda を使用して、取得時に画像に動的にウォーターマークを付ける
Amazon S3 Object Lambdaとは
簡単に説明するとデータを取り出す時に取り出したデータを加工できる機能です。
S3 Object Lambda を使用すると、S3 GET、HEAD および LIST リクエストに独自のコードを追加して、データがアプリケーションに返されるときにそのデータを変更および処理できます。
カスタムコードを使用して、S3 GET リクエストによって返されるデータを変更し、行のフィルタリング、画像の動的なサイズ変更、機密データの編集などを行うことができるようになりました。
また、S3 Object Lambda を使用して、バケット内のオブジェクトのカスタムビューを作成する S3 LIST リクエストの出力や、オブジェクト名やサイズなどのオブジェクトメタデータを変更する S3 HEAD リクエストの出力を変更することができます。
AWS Lambda 関数により、コードは AWS によって完全に管理されているインフラストラクチャで実行されるため、データの派生コピーを作成して保存したり、高価なプロキシを実行したりする必要はなく、アプリケーションに変更を加える必要もありません。
これができると何が嬉しいかというと上記の説明にあるとおりデータを取り出す時に画像の動的なサイズ変更、機密データの編集
ができるので複数種類のデータを保持することなく、単一のデータで複数の表現できるようになります。
たとえば、画像の動的なサイズ変更というとレスポンシブデザインの要件に応える時に必要になったりします。
実際に使ってみよう
下記の手順を参考に進めていきます。
Amazon S3 バケットを作成する
S3のマネジメントコンソールを開いてバケットを作成します。
バケット名はobject-s3-lambda-アカウントID
とします。
画像をアップロード
手順にあるAWSのロゴをダウンロードします。
作成したバケットにダウンロードしたAWSのロゴ画像をアップロードします。Uploadをクリックします。
S3 アクセスポイントを作成する
左メニューにあるAccess Points
をクリックします。
Create Access point
をクリックします。
アクセスポイント名はs3-obeject-lambda
Bucket nameは先ほど作成したバケット名を選択
Network originはインターネット
を選択します。
Create Access Pointを実行します。
Lambda 関数を作成する
画像を変更するLambda関数を作成します。このLambdaはS3へのGETリクエストを実行した際に呼び出されます。
構築のためにCloudShellを呼び出します。
ターミナルボタンをクリックします。
参考手順の4.2にある4.2 – Lambda 関数をデプロイするように CloudShell を準備する
からコードをコピーします。
以下に本記事執筆時点のコードを記載
# 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分かかるとあるのでしばらく待ちましょう。
終わりましたら下記のコマンドを実行します。
wget https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/branding/Amazon_Typefaces_Complete_Font_Set_Mar2020.zip
ウォーターマークが付いたテキストを画像に書き込むのに使用する TrueType フォントを抽出するため、下記のコマンドを実行します。unzipコマンドなので解凍作業です。
unzip -oj Amazon_Typefaces_Complete_Font_Set_Mar2020.zip "Amazon_Typefaces_Complete_Font_Set_Mar2020/Ember/AmazonEmber_Rg.ttf"
Lambdaのコードを作成
下記のcatを実行してlambda.py
を作成します。
なお、公式サイトに掲載されているコードは動作しなかったため、以下の修正したコードを利用しています。
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 = d.textlength(text, font)
text_height = d.textlength(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
コードを圧縮します。
zip -r9 lambda.zip lambda.py AmazonEmber_Rg.ttf
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ポリシーをロールにアタッチします。
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
S3 Object Lambda アクセスポイントを作成する
ここでようやく今回使う機能が登場!
S3のメニューからObject Lambda Access Pointsをクリックします。
Create Object Lambda Access Point
をクリックします。
Create Object Lambda Access Pointの設定
Object Lambda Access Point nameをol-amazon-s3-images-guide
とします。
リージョンは画像をアップロードしたS3バケットと同じリージョンを指定します。
Browse S3
(S3を参照)をクリックし、先ほど作成したアクセスポイントを指定します。
変化設定でGET Object
にチェックを入れます。
Lambda関数の選択欄ではol_image_processing
を選択します。
最後にCreate Object Lambda Access Point
をクリックします。
これで環境構築は以上です。
(テスト)画像をダウンロードする
それでは早速、画像を開いてみましょう。
画像にチェックを入れて、Open
をクリックします。
※以下、加工された画像
うっすらですが、Watermark
という文字がAWSのロゴマークに写っているのがわかるでしょうか。
ちなみに元の画像は以下
片付け
参考手順の片付け手順に従って片付けを行いましょう。
まとめ
今回はS3のダウンロードをトリガーにして画像を自動で加工し、加工した画像をダウンロードできるように環境を構築しました。
S3からファイルをダウンロードするとLambdaが実行されます。実行の様子はCloudWatch Logsのロググループから確認できます。
※ログを抜粋
Object to retrieve: /2.3%20aws%20logo.f00a88b928cdc48ba417e90c2c1eab9d961899d1.png
[INFO] 2023-11-10T14:29:37.232Z 41e2f357-2ec3-4faa-a9cc-3297cf1ce9c3 Object to retrieve: /2.3%20aws%20logo.f00a88b928cdc48ba417e90c2c1eab9d961899d1.png
[INFO] 2023-11-10T14:29:37.309Z 41e2f357-2ec3-4faa-a9cc-3297cf1ce9c3 Compatible Image format found! Processing image: 2.3%20aws%20logo.f00a88b928cdc48ba417e90c2c1eab9d961899d1.png
Compatible Image format found! Processing image: 2.3%20aws%20logo.f00a88b928cdc48ba417e90c2c1eab9d961899d1.png
END RequestId: 41e2f357-2ec3-4faa-a9cc-3297cf1ce9c3
作成された画像は商品のサンプルの画像にも使えるような見た目で少しだけ面白いと感じました。
どんな文字列でも挿入できるので使い方次第ではいろんなことができそうです。
今回の注意点
記事を執筆時点で掲載されているLambdaのソースコードは動作しなかったため、うまくいかなかった箇所を特定して修正しました。
動作しなかった箇所について説明します。
コード内にあるImageDraw
のtextsizeメソッドはno attribute
になってしまうのでtextlengthに変更します。
変更前
text_width, text_height = d.textsize(text, font)
変更後
text_width = d.textlength(text, font)
text_height = d.textlength(text, font)
最近のアップデート
今回はご紹介した機能はAmazon Athenaと統合されているようなので分析にも一役買いそうです。
Amazon S3 Object Lambda と Amazon Athena が統合