9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【AWS】Amazon S3 Object Lambdaを検証!

Last updated at Posted at 2023-11-10

はじめに

この記事では「この前リリースされた機能って実際に動かすとどんな感じなんだろう」とか「もしかしたら内容次第では使えるかも??」など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をクリックします。

Screenshot 2023-11-10 at 22.09.20.png

画像をアップロードしたらUploadをクリックします。
Screenshot 2023-11-10 at 22.12.21.png

S3 アクセスポイントを作成する

左メニューにあるAccess Pointsをクリックします。

Screenshot 2023-11-10 at 22.14.05.png

Create Access pointをクリックします。

Screenshot 2023-11-10 at 22.15.28.png

アクセスポイント名はs3-obeject-lambda
Bucket nameは先ほど作成したバケット名を選択
Network originはインターネットを選択します。

Create Access Pointを実行します。

Lambda 関数を作成する

画像を変更するLambda関数を作成します。このLambdaはS3へのGETリクエストを実行した際に呼び出されます。

構築のためにCloudShellを呼び出します。
ターミナルボタンをクリックします。

Screenshot 2023-11-10 at 22.26.05.png

参考手順の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をクリックします。

Screenshot 2023-11-10 at 22.44.34.png

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にチェックを入れます。

Screenshot 2023-11-10 at 22.50.40.png

Lambda関数の選択欄ではol_image_processingを選択します。

Screenshot 2023-11-10 at 22.51.51.png

最後にCreate Object Lambda Access Pointをクリックします。
これで環境構築は以上です。

(テスト)画像をダウンロードする

それでは早速、画像を開いてみましょう。

画像にチェックを入れて、Openをクリックします。

Screenshot 2023-11-10 at 23.30.15.png

※以下、加工された画像

image.png

うっすらですが、Watermarkという文字がAWSのロゴマークに写っているのがわかるでしょうか。
ちなみに元の画像は以下

image.png

片付け

参考手順の片付け手順に従って片付けを行いましょう。

まとめ

今回は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に変更します。

image.png

変更前

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 が統合

おわり

9
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?