はじめに
今回は、AWS公式が提供しているハンズオンのひとつをやっていこうと思います。
やっていくのは「Amazon S3 Object Lambda を使用して、取得時に画像に動的にウォーターマークを付ける」です。
やろうと思ったきっかけ
今回、AWSのハンズオンをやりたいと思ったきっかけとしては以下のようなものがあります。
- AWSのサービスを実際に触ってみたかった
- AWSのハンズオンはどんなものなのか知りたかった
- AWSの資格勉強をしているのでその一環として
- 単純に面白そうだと思った(これが一番大きい)
それでは、早速始めていきましょう!
AWSアカウントはある前提で進めていきます。
実際にやっていく
1. IAMユーザーを作成する
元ページでは省略されている工程ですが、せっかくなのでIAMユーザーの作成からやっていきます。
AWSコンソールにログインし、IAM > アクセス管理 > ユーザーのページからユーザー作成を押下します。
![スクリーンショット 2024-12-02 12.55.03.png](/wp-content/uploads/2024/12/image-1.png)
![image.png](/wp-content/uploads/2024/12/image-2.png)
ユーザー名は s3-object-lambda-to-dynamically-watermark-images
とします。
![スクリーンショット 2024-12-02 13.03.37.png](/wp-content/uploads/2024/12/image-7.png)
許可の設定に進み、「ポリシーの作成」を押下します。
![スクリーンショット 2024-12-02 13.12.48.png](/wp-content/uploads/2024/12/image-2.png)
以下のポリシーを付与します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowS3BasicOperations",
"Effect": "Allow",
"Action": [
"s3:*",
"s3-object-lambda:*",
"lambda:*",
"cloudshell:*",
"iam:CreateRole",
"iam:GetRole",
"iam:PutRolePolicy",
"iam:PassRole",
"iam:DeleteRole",
"iam:AttachRolePolicy",
"sts:AssumeRole"
],
"Resource": "*"
}
]
}
![image.png](/wp-content/uploads/2024/12/image-2.png)
名前は s3-object-lambda-to-dynamically-watermark-images
とします。
そして、先ほどのユーザーに作成したポリシーを付与します。(名前で検索かけると楽です)
![image.png](/wp-content/uploads/2024/12/image-14.png)
確認して、ユーザーを作成します。
![](/wp-content/uploads/2024/12/https___qiita-image-store.s3.ap-northeast-1.amazonaws.com_0_2778030_21e5f9c0-a37f-c24e-7bdd-882eeae9bb70-1024x644.jpg)
また、コンソールでのログインを有効化します。
ユーザー→s3-object-lambda-to-dynamically-watermark-images
→セキュリティ認証情報→コンソールサインインで「コンソールアクセスを有効にする」を押下して、表示された「コンソールサインイン URL」、「ユーザー名」、「コンソールパスワード」をコピーして置いておきます。
![スクリーンショット 2024-12-02 13.33.19.png](/wp-content/uploads/2024/12/image-2.png)
![スクリーンショット 2024-12-02 13.33.47.png](/wp-content/uploads/2024/12/image-4.png)
以降は、このコンソールサインインURLからログインした、ユーザー(s3-object-lambda-to-dynamically-watermark-images)のコンソールで作業を行なっていきます。
![image.png](/wp-content/uploads/2024/12/image-3.png)
2. Amazon S3 バケットを作成する
コンソールからS3を選択し、「バケットを作成」ボタンを押下します。
![image.png](/wp-content/uploads/2024/12/image-5.png)
バケット名はs3-object-lambda-to-dynamically-watermark-images-202412
とします。
他は変更せず、バケットを作成します。
3: オブジェクトをアップロードする
以下の画像をダウンロードします。(AWS公式が提供している画像です)
![](/wp-content/uploads/2024/12/2.3-aws-logo.f00a88b928cdc48ba417e90c2c1eab9d961899d1.png)
S3→s3-object-lambda-to-dynamically-watermark-images-202412
から、アップロードボタンを押下しアップロードします。
![image.png](/wp-content/uploads/2024/12/image-12.png)
オブジェクトの欄に画像があればこのステップでの作業は完了です!
![image.png](/wp-content/uploads/2024/12/image-14.png)
4. S3 アクセスポイントを作成する
S3→アクセスポイントから「アクセスポイントの作成」ボタンを押下します。
![image.png](/wp-content/uploads/2024/12/image-6.png)
アクセスポイント名はaccess-point-s3
とし、バケット名は「S3の参照」から先ほど作成したバケットを選択します。
また、ネットワークオリジンはインターネット
を選択してください。
ほかは設定を変えず、作成ボタンを押します。
5. Lambda関数を作成する
このステップでは、S3へのGETリクエストがS3ObjectLambdaアクセスポイントを通じて行われたときに呼び出されるLambda関数を作成します。
すこし複雑ですが、頑張ってやっていきましょう!
5-1 CloudShellターミナルの起動
まず、CloudShell
を起動します。
コンソールの一番上にあるペインの以下画像の右端にあるボタンを押すことで起動します。
![image.png](/wp-content/uploads/2024/12/image-8.png)
以下のような画面になればOKです。
![image.png](/wp-content/uploads/2024/12/image-11.png)
5-2 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
5-3 Lambda関数を構築する
Lambda関数によって使用されるフォントをダウンロードします。
wget https://m.media-amazon.com/images/G/01/mobile-apps/dex/alexa/branding/Amazon_Typefaces_Complete_Font_Set_Mar2020.zip
ダウンロードしたフォントのzipファイルから、使用するファイルを抽出します。
unzip -oj Amazon_Typefaces_Complete_Font_Set_Mar2020.zip "Amazon_Typefaces_Complete_Font_Set_Mar2020/Ember/AmazonEmber_Rg.ttf"
S3 Object Lambdaリクエストを処理するために使用されるPythonのコードが入ったファイルを作成します。
少し長いですが、同じようにコピペして実行してください。
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のコードとフォントファイルを含むlambda.zip
ファイルを作成します。
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ポリシーを作成したIAM ロールにアタッチします。
1行ずつ実行し、エラーを吐いてないかはしっかり見ておくことを推奨します。
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
以下のような文言が表示されれば正常に作成が完了しています!
[cloudshell-user@ip-10-134-28-58 ~]$ 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
{
"FunctionName": "ol_image_processing",
"FunctionArn": "arn:aws:lambda:ap-northeast-1:*************:function:ol_image_processing",
"Runtime": "python3.9",
"Role": "arn:aws:iam::*************:role/ol-lambda-images",
"Handler": "lambda.handler",
"CodeSize": 52282,
"Description": "",
"Timeout": 3,
"MemorySize": 1024,
"LastModified": "2024-12-01T09:25:10.218+0000",
"Version": "$LATEST",
"TracingConfig": {
"Mode": "PassThrough"
},
"RevisionId": "e90a13c0-1dbd-4c73-a1cb-f59b4d644b5c",
"Layers": [],
"State": "Pending",
"StateReason": "The function is being created.",
"StateReasonCode": "Creating",
"PackageType": "Zip",
"Architectures": [
"x86_64"
],
"EphemeralStorage": {
"Size": 512
},
"SnapStart": {
"ApplyOn": "None",
"OptimizationStatus": "Off"
},
"RuntimeVersionConfig": {
"RuntimeVersionArn": "arn:aws:lambda:ap-northeast-1::runtime:**************************"
},
"LoggingConfig": {
"LogFormat": "Text",
"LogGroup": "/aws/lambda/ol_image_processing"
}
}
6. S3 Object Lambda アクセスポイントを作成する
ここからはコンソールでの操作になります。
s3→Object Lambda アクセスポイントから、「Object Lambda アクセスポイントの作成」ボタンを押下します。
![スクリーンショット 2024-12-02 14.34.39.png](/wp-content/uploads/2024/12/image-14.png)
アクセスポイント名はol-amazon-s3-images-guide
とし、
サポートするアクセスポイントは「s3の参照」から先ほど作成したアクセスポイントを選択します。
また、S3 APIはGetObject
を選択しておきます。
そして、「Lambda 関数の呼び出し」から先ほど作成したLambda関数、ol_image_processing
を選択します。
ほかは変更せず、作成ボタンを押下し戻ります。
7.S3 Object Lambdaアクセスポイントからウォーターマーク付きの画像を閲覧する
いよいよ最後のステップです。
s3 → Object Lambda アクセスポイントから先ほど作成したol-amazon-s3-images-guide
を開きます。
![スクリーンショット 2024-12-02 14.41.24.png](/wp-content/uploads/2024/12/image-13.png)
アップロードしていた画像をチェックし、「開く」を押すと…
![image.png](/wp-content/uploads/2024/12/image-14.png)
ウォーターマーク付きの画像が表示されました!
しかし、実は1回目の実行ではエラーが出ていました。
![スクリーンショット 2024-12-02 14.45.27.png](/wp-content/uploads/2024/12/image.png)
それが私だけなのか、必ず再現するかはわからないのですが、もしこのエラーが出る時は、以下の方法を試してみてください。
エラー解決: LambdaRuntimeError
Lambdaのエラーを見に行くと、以下のような記載が…
[ERROR] Runtime.ImportModuleError: Unable to import module 'lambda': No module named 'PIL' Traceback (most recent call last):
どうやら、コード内で使用しているPIL(pillow)
ライブラリがうまく使えないようです。
解決策として、LambdaのLayers機能を使用します。
コンソールでLambda → 関数 → ol_image_processingと進み、以下の画面下部にある「レイヤーの追加」をクリックします。
![スクリーンショット 2024-12-02 14.49.58.png](/wp-content/uploads/2024/12/image-10.png)
![スクリーンショット 2024-12-02 14.51.21.png](/wp-content/uploads/2024/12/image-9.png)
「ARN を指定」を選択し、arn:aws:lambda:ap-northeast-1:770693421928:layer:Klayers-p39-pillow:1
と入力して「追加」を押します。
![スクリーンショット 2024-12-02 14.52.37.png](/wp-content/uploads/2024/12/image-13.png)
これが正常に保存された後、再度S3 Object Lambdaアクセスポイントから画像を閲覧するとエラーが解消されウォーターマーク付きの画像が表示されました。
8. リソースの削除
最後に、意図せぬ料金の発生などを避けるためリソースの削除を行います。
- S3バケット
- アクセスポイント
- S3 Object Lambdaアクセスポイント
- Lambda関数
- IAMロール
- IAMポリシー
- IAMユーザー
作成したすべてのリソースを削除しておきましょう!
さいごに
最後までお読みいただきありがとうございました!
ハンズオンで実際にAWSのリソースを触ることによって、サービスへの理解が深まりました。
今後も積極的に触っていきたいと思います!
普段はQiitaにて技術記事の投稿を行なっていますので、こちらもよろしくお願いします!