Amazon S3 Object Lambda を使用して、取得時に画像に動的にウォーターマークを付ける

チュートリアル

概要

Amazon S3 Object Lambda を使用すると、S3 GET、HEAD および LIST リクエストに独自のコードを追加して、データがアプリケーションに返されるときにそのデータを変更できます。カスタムコードを使用して、S3 GET リクエストによって返されるデータを変更し、データ形式の変換 (例えば、XML から JSON)、画像の動的なサイズ変更、機密データの編集などを行うことができるようになりました。さらに、S3 Object Lambda を使用することで、S3 LIST リクエストの出力を変更してバケット内にあるオブジェクトのカスタムビューを作成したり、S3 HEAD リクエストの出力を変更してオブジェクト名やサイズといったオブジェクトのメタデータを変更したりできます。

このチュートリアルの目的は、Amazon S3 Object Lambda の使用を開始する方法を示すことです。多くの組織は、それぞれ独自のデータ形式要件を持つさまざまなアプリケーションによってアクセスされる Amazon S3 に画像を保存しています。画像にアクセスするユーザーによっては、画像にウォーターマークを含めるように画像を変更する必要がある場合があります (例えば、有料サブスクライバーはウォーターマークのない画像を表示できますが、無料ユーザーはウォーターマークが付いた画像を受け取ります)。

このチュートリアルでは、S3 Object Lambda を使用して、Amazon S3 から取得された画像にウォーターマークを追加します。S3 Object Lambda を使用すると、既存のオブジェクトを変更したり、データの派生コピーを複数保持したりすることなく、Amazon S3 から取得したデータを変更できます。同じデータの複数のビューを表示し、派生コピーを保存する必要がなくなるため、ストレージコストを節約できます。

実行するアクション

このチュートリアルでは、次のことを行います。

  • Amazon S3 バケットを作成する
  • S3 アクセスポイントを作成する
  • AWS Lambda 関数を作成して画像を変更する
  • S3 Object 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 experience

初心者

 所要時間

20 分

 完了までのコスト

1 USD 未満 (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 アクセスポイントを作成する

S3 Object Lambda アクセスポイントをサポートするために使用する Amazon S3 アクセスポイントを作成します。このアクセスポイントは、チュートリアルの後半で作成します。

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

  • S3 コンソールに移動し、左側のナビゲーションペインで [アクセスポイント] メニューオプションを選択します。次に、[アクセスポイントの作成] ボタンを選択します。

3.2

  • [プロパティ] セクションで、目的のアクセスポイント名を入力し、[S3 の閲覧] ボタンを選択して、ステップ 1 で入力した [バケット名] を選択します。次に、[ネットワークオリジン] を [インターネット] に設定します。

3.3

  • 他のデフォルトはすべてそのままにしておきます。ページの下部に移動し、[アクセスポイントの作成] ボタンを選択します。

3.4

  • 左側のナビゲーションペインで [アクセスポイント] に移動すると、S3 アクセスポイントがリストに表示されるようになります。

ステップ 4: Lambda 関数を作成する

  • 次に、S3 GET リクエストが S3 Object Lambda アクセスポイントを通じて行われたときに呼び出される Lambda 関数を作成します。
  • AWS マネジメントコンソールの AWS CloudShell を使用して S3 Object Lambda を構築およびテストします。次の要件を満たしている場合は、ユーザー自身のコンピュータまたは AWS Cloud9 インスタンスを使用してソリューションを構築できます。
    - AWS コマンドラインインターフェイス (CLI) の最新バージョン
    - AWS Lambda 関数/レイヤーと IAM ロールを作成するための認証情報
    - Python 3.9
    - zip ユーティリティ
    - jq ユーティリティ

4.1 – CloudShell ターミナルを開始する

AWS マネジメントコンソールの右上のメニューで CloudShell アイコンを選択します。

CloudShell の紹介ウィンドウが表示されたら、内容を読んで [閉じる] を選択します。

CloudShell ターミナルで新しいブラウザタブが開きます (以下のスクリーンショットと類似)。

4.2 – Lambda 関数をデプロイするように CloudShell を準備する

  • 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 関数を構築する

  • 画像にウォーターマークを追加するために Lambda 関数によって使用される TrueType フォントをダウンロードします。以下のコマンドをコピーして 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 Object 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 ファイルを作成します。
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 ロールにアタッチします。このポリシーには、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 Object Lambda アクセスポイントを作成する

S3 バケットに保存されている画像にアクセスするために使用される S3 Object Lambda アクセスポイントを作成します。

5.1 – S3 Object Lambda アクセスポイントを作成する

[一般] セクションの [Object Lambda アクセスポイント名] に ol-amazon-s3-images-guide と入力します。

S3 Object Lambda アクセスポイントの AWS リージョンが、ステップ 1.3 で S3 バケットを作成したときに指定した AWS リージョンと一致していることを確認してください。

サポートアクセスポイントでは、[S3 の閲覧] ボタンを使用して、ステップ 3.2 で作成した S3 アクセスポイントの Amazon リソースネーム (ARN) を指定します。

下に移動して、[変換設定] を表示します。[S3 API ] リストで、[GetObject] オプションを選択します。

[Lambda 関数] で、ol_image_processing を指定します。

次に、ページの下部に移動し、[Object Lambda アクセスポイントの作成] を選択します。

ステップ 6: S3 Object Lambda アクセスポイントから画像をダウンロードする

S3 Object Lambda アクセスポイントを作成したら、画像を開いて、リクエスト中にウォーターマークが適切に追加されていることを確認します。

6.1 – S3 Object Lambda アクセスポイントを開く

  • S3 コンソールの左側のナビゲーションペインで [Object Lambda アクセスポイント] を選択して S3 Object Lambda アクセスポイントのリストに戻り、ステップ 5.1 で作成した S3 Object Lambda アクセスポイントを選択します。この例では、S3 Object Lambda アクセスポイントを ol-amazon-s3-images-guide として選択しました。

ステップ 2.4 でアップロードした画像を選択し、[開く] ボタンを選択します。

新しいブラウザタブが開き、画像とウォーターマークが表示されます。
 
S3 Object Lambda Access Point からダウンロードされた互換性のあるすべての画像には、ウォーターマークの付いたテキストが含まれるようになります。


6.2 – 変換後の画像を AWS CLI からダウンロードする

  • AWS CLI を使用して画像をダウンロードすることもできます。それを行うには、S3 Object Lambda アクセスポイントの Amazon リソースネーム (ARN) が必要です。S3 コンソールで、[Object Lambda アクセスポイント] ページに移動し、S3 Object 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 Object Lambda アクセスポイントから画像をダウンロードするときにステップ 6.3 で定義したファイル名を入力し、[ダウンロード] を選択します。

これで、ローカルコンピュータから画像を開くことができます。

注意: 画像ビューワーは、コンピュータとオペレーティングシステムによって異なる場合があります。画像を開くためにどのアプリケーションを使用すればよいかわからない場合は、管理者に確認してください。

ステップ 7: リソースをクリーンアップする

以下では、このチュートリアルで作成したリソースをクリーンアップします。意図しない料金が発生しないように、使用しなくなったリソースを削除するのがベストプラクティスです。

7.1 – S3 Object Lambda アクセスポイントを削除する

  • S3 コンソールに移動し、左側のナビゲーションペインで [Object Lambda アクセスポイント] を選択します。
  • [Object Lambda アクセスポイント] ページで、ステップ 5.1 で作成した S3 Object Lambda アクセスポイントの左側にあるラジオボタンを選択します。

[削除] を選択します。

表示されるテキストフィールドに名前を入力して S3 Object Lambda アクセスポイントを削除することを確認し、[削除] を選択します。

7.2 – S3 アクセスポイントを削除する

  • S3 コンソールの左側のナビゲーションペインで、[アクセスポイント] を選択します。
  • ステップ 3.1 で作成した S3 アクセスポイントに移動し、S3 アクセスポイントの名前の横にあるラジオボタンを選択します。
  • [削除] を選択します。

表示されたテキストフィールドにアクセスポイントの名前を入力してそれを削除することを確認し、[削除] を選択します。

7.3 – テストオブジェクトを削除する

  • S3 コンソールに移動し、左側のナビゲーションペインで [バケット] メニューオプションを選択します。最初に、テストバケットからテストオブジェクトを削除する必要があります。このチュートリアルのために作業しているバケットの名前を選択します。
  • テストオブジェクト名の左側にあるチェックボックスを選択し、[削除] ボタンを選択します。
  • [オブジェクトの削除] ページで、削除する適切なオブジェクトを選択していることを確認し、[オブジェクトを完全に削除] 確認ボックスに削除と入力します。その後、[オブジェクトの削除] ボタンを選択して続行します。
次に、削除が成功したかどうかを示すバナーが表示されます。

7.4 – S3 バケットを削除する

  • 次に、左側のナビゲーションペインの S3 コンソールメニューから [バケット] を選択します。このチュートリアル用に作成したソースバケットの左側にあるラジオボタンを選択し、[削除] ボタンを選択します。

警告メッセージを確認します。このバケットの削除を続行する場合は、[バケットの削除] 確認ボックスにバケット名を入力し、[バケットの削除] を選択します。

7.5 – Lambda 関数を削除する

  • AWS Lambda コンソールで、左側のナビゲーションペインの [関数] を選択します。
  • ステップ 4.3 で作成した関数の名前の左側のチェックボックスを選択します。
  • 次に、[アクション] を選択してから、[削除] を選択します。[関数の削除] ダイアログボックスで、[削除] を選択します。

まとめ

おめでとうございます。 Amazon S3 Object Lambda を使用して、取得時に画像にウォーターマークを動的に追加し、処理された画像をリクエスト元のクライアントに配信する方法を学びました。ユースケースに合わせて Lambda 関数をカスタマイズして、S3 GET、HEAD、LIST リクエストによって返されるデータを変更できます。一般的なユースケースには、呼び出し元固有の詳細を使用したウォーターマークのカスタマイズ、機密データのマスキング、特定のデータ行のフィルタリング、他のデータベースの情報によるデータの拡張、データ形式の変換などがあります。

このページはお役に立ちましたか?

次のステップ