かんがるーさんの日記

最近自分が興味をもったものを調べた時の手順等を書いています。今は Spring Boot をいじっています。

S3 にアップロードされた画像ファイルから Lambda でサムネイル画像を生成してみる

概要

記事一覧はこちらです。

S3 にアップロードした画像ファイルから Lambda でサムネイル画像を生成してみます。

f:id:ksby:20200610082312p:plain

  • アップロードする画像ファイルのフォーマットは JPEG とする。
  • サムネイル画像のフォーマットも JPEG とする。サイズは幅320 x 高さ180 とする。
  • 画像ファイルをアップロードする S3 Bucket は ksbysample-upload-bucket、サムネイル画像を置く S3 Bucket は ksbysample-resize-bucket とする。
  • サムネイル画像のファイル名は <オリジナルの画像ファイル名>_thumb.jpg とする。

よく聞くパターンなので簡単だと思っていましたが、結構苦労しました。。。

参照したサイト・書籍

  1. AWS Lambdaで画像ファイル加工~環境構築から実行確認まで~ 手順紹介
    https://business.ntt-east.co.jp/content/cloudsolution/column-try-17.html

  2. Lambda trigger on existing s3 bucket
    https://forum.serverless.com/t/lambda-trigger-on-existing-s3-bucket/6056

  3. Using existing buckets
    https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-buckets

  4. How many records can be in S3 put() event lambda trigger?
    https://stackoverflow.com/questions/40765699/how-many-records-can-be-in-s3-put-event-lambda-trigger

  5. Sample Amazon S3 function code
    https://docs.aws.amazon.com/lambda/latest/dg/with-s3-example-deployment-pkg.html

  6. Pythonで文字列を置換(replace, translate, re.sub, re.subn)
    https://note.nkmk.me/python-str-replace-translate-re-sub/

  7. Pythonでパス文字列からファイル名・フォルダ名・拡張子を取得、結合
    https://note.nkmk.me/python-os-basename-dirname-split-splitext/

  8. AWS Lambdaで運用した実績から得られた、serverless frameworkのオススメ設定とプラグインの知見
    https://tech.ga-tech.co.jp/entry/2018/12/12/120000

  9. Serverless Python Requirements
    https://www.serverless.com/plugins/serverless-python-requirements/

  10. Pillow
    https://pillow.readthedocs.io/en/stable/index.html

  11. Why can't Python import Image from PIL?
    https://stackoverflow.com/questions/26505958/why-cant-python-import-image-from-pil/31728305

  12. AWS LambdaでPython Pillowを使うための手順
    https://qiita.com/ryasuna/items/9051cfdc0576134bb46c

  13. AWS Lambda で Pillow を使おうとしたらハマった
    https://michimani.net/post/aws-use-pillow-in-lambda/

  14. amazonlinux
    https://hub.docker.com/_/amazonlinux

    • Amazon Linux って Docker Image があるんですね。今回初めて知りました。
  15. lambci / docker-lambda
    https://github.com/lambci/docker-lambda

    • しかも Lambda 実行用の Docker Image もあるとは!
  16. AWS Lambda runtimes
    https://docs.aws.amazon.com/lambda/latest/dg/lambda-runtimes.html

  17. WebP変換 & 画像キャッシュサービスをサーバレスで構築する - Feed re:Architect vol.1 -
    https://buildersbox.corp-sansan.com/entry/2019/06/06/124752

  18. AWS LambdaでTensorFlow 2.0を使った画像分類
    https://tech.unifa-e.com/entry/2019/09/17/085400

    • 今回の記事とは関係ありませんが、いろいろ調べている時に見かけて良い参考になりそうだったのでメモしておきます。

目次

  1. resize-image-app-project プロジェクトを作成する
  2. Serverless Framework で resize-service サブプロジェクトを作成する
  3. S3 Bucket を作成してイベント発生時に Lambda が呼び出されるよう serverless.yml を記述する
  4. S3 Bucket にファイルをアップロードした時の event の内容を確認する
  5. サムネイル画像を生成するよう handler.py の resize 関数を実装する
  6. serverless-python-requirements プラグインをインストールする
  7. deploy する
  8. 画像をアップロードしてサムネイル画像を生成してみる
  9. 最後に

手順

resize-image-app-project プロジェクトを作成する

以下の手順で share-s3bucket-with-multi-servces プロジェクトを作成します。具体的な手順は IntelliJ IDEA+Node.js+npm+serverless framework+Python の組み合わせで開発環境を構築して AWS Lambda を作成してみる 参照。

  • resize-image-app-project の Empty Project を作成する。
  • Python の仮想環境を作成する。
  • Serverless Framework をローカルインストールする。
  • .envrc を作成する。

Serverless Framework で resize-service サブプロジェクトを作成する

プロジェクトのルートディレクトリの下で npx sls create --template aws-python3 --path resize-service を実行して resize-service サブプロジェクトを作成します。

f:id:ksby:20200531190301p:plain

自動生成された serverless.yml に対し、以下の設定を追加します。

  • リソースが東京リージョン(ap-northeast-1)に生成されるようにする。

S3 Bucket を作成してイベント発生時に Lambda が呼び出されるよう serverless.yml を記述する

2つの S3 Bucket ksbysample-upload-bucketksbysample-resize-bucket を作成して、ksbysample-upload-bucket にファイルがアップロードされた時に Lambda が呼び出されるよう serverless.yml に以下の記述を追加します。handler.py の関数名も resize に変更します。

service: resize-service

provider:
  name: aws
  runtime: python3.8

  stage: dev
  region: ap-northeast-1

  iamRoleStatements:
    - Effect: "Allow"
      Action:
        - "s3:GetObject"
      Resource:
        - "arn:aws:s3:::ksbysample-upload-bucket/*"
    - Effect: "Allow"
      Action:
        - "s3:PutObject"
      Resource:
        - "arn:aws:s3:::ksbysample-resize-bucket/*"

functions:
  resize:
    handler: handler.resize
    events:
      # Using existing buckets
      # https://www.serverless.com/framework/docs/providers/aws/events/s3#using-existing-buckets
      - s3: ksbysample-upload-bucket

resources:
  Resources:
    KsbysampleResizeBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ksbysample-resize-bucket
  • events の下に - s3: ksbysample-upload-bucket を記述すると S3 Bucket が生成されます。resources に S3 Bucket の記述をする必要はありません。(新規作成するリソースは resources に記述するものと思い resources に S3 Bucket の記述をしてから - s3: ksbysample-upload-bucket も書いて試しに deploy してみたら CREATE_FAILED が出てしばらく悩みました。。。)
  • もしイベント発生元の S3 Bucket を terraform 等で別に作成する場合には Using existing buckets を参考に設定すれば良さそうです。
  • ksbysample-resize-bucket はイベント発生元ではないので resources の下に記述します。
  • この2つを書いただけでは S3 Bucket の GetObject、PutObject が出来ないので iamRoleStatements に必要な権限を記述します。

S3 Bucket にファイルをアップロードした時の event の内容を確認する

event がログに出力されるよう handler.py を以下のように変更してから、

import logging

logger = logging.getLogger()
logger.setLevel(logging.INFO)


def resize(event, context):
    logger.info(event)

deploy して S3 Bucket に sample.jpg をアップロードしてみると以下の json がログに出力されました(名前に principal が含まれているものと IP アドレスはマスクしています)。

{
  'Records': [
    {
      'eventVersion': '2.1',
      'eventSource': 'aws:s3',
      'awsRegion': 'ap-northeast-1',
      'eventTime': '2020-06-10T00:45:25.995Z',
      'eventName': 'ObjectCreated:Put',
      'userIdentity': {
        'principalId': 'AWS:XXXXXXXXXXXXXXXXXXXXX'
      },
      'requestParameters': {
        'sourceIPAddress': 'xxx.xxx.xxx.xxx'
      },
      'responseElements': {
        'x-amz-request-id': '11F4BEB2A6E4D924',
        'x-amz-id-2': 'k2RJ6yeU8sk5tRp7ntfyM8QDmR58rF1TIdacJimdExhLOOSQ3N6pOtIAWXyk6pGjTeBGOBx995ELqaK53zhdAo6Ky/PZb08e'
      },
      's3': {
        's3SchemaVersion': '1.0',
        'configurationId': '66e09001-2a37-4900-9a98-06da126cc896',
        'bucket': {
          'name': 'ksbysample-upload-bucket',
          'ownerIdentity': {
            'principalId': 'XXXXXXXXXXXXXX'
          },
          'arn': 'arn:aws:s3:::ksbysample-upload-bucket'
        },
        'object': {
          'key': 'sample.jpg',
          'size': 669286,
          'eTag': 'febe0aaf4d817457776b6be293973442',
          'sequencer': '005EE02D28714EE2DC'
        }
      }
    }
  ]
}

Records が配列になっているので複数ファイルをアップロードしたら複数ここに入ってくるのかな?と思い、今度は sample.jpg、sample2.jpg の2つのファイルをアップロードしてみると、1 ファイルずつ event が発生しています。

f:id:ksby:20200610095224p:plain

stackoverflow に How many records can be in S3 put() event lambda trigger? があり、これを読むと 1 ファイル 1 event のようです。

サムネイル画像を生成するよう handler.py の resize 関数を実装する

Sample Amazon S3 function codePython 3 のサンプルを参考にして実装します。

import logging
import os
import re
import uuid
from urllib.parse import unquote_plus

import boto3
from PIL import Image

thumbnail_size = 320, 180

logger = logging.getLogger()
logger.setLevel(logging.INFO)
s3_client = boto3.client('s3')


def resize_image(image_path, resized_path):
    with Image.open(image_path) as image:
        image.thumbnail(thumbnail_size)
        image.save(resized_path)


def resize(event, context):
    for record in event['Records']:
        bucket = record['s3']['bucket']['name']
        key = unquote_plus(record['s3']['object']['key'])

        # ディレクトリの場合には何もしない
        if key.endswith('/'):
            return

        tmpkey = key.replace('/', '')
        download_path = '/tmp/{}{}'.format(uuid.uuid4(), tmpkey)
        resized_path = '/tmp/resized-{}'.format(tmpkey)

        filename = key.split('/')[-1]
        dirname = re.sub(filename + '$', '', key)
        basename, ext = os.path.splitext(filename)
        resized_key = '{}{}_thumb{}'.format(dirname, basename, ext)

        s3_client.download_file(bucket, key, download_path)
        resize_image(download_path, resized_path)
        s3_client.upload_file(resized_path, "ksbysample-resize-bucket", resized_key)
        logger.info('サムネイルを生成しました({})'.format(resized_key))

IDEA の Terminal を起動して boto3、Pillow をインストールします。IDEA の赤波下線を消したり補完を効かせるために入れているもので、deploy する時のパッケージは serverless-python-requirements プラグインを利用して収集します。

  • pip install boto3
  • pip install Pillow

f:id:ksby:20200604071741p:plain f:id:ksby:20200610113304p:plain

serverless-python-requirements プラグインをインストールする

外部ライブラリを利用しているので serverless-python-requirements プラグインをインストールして自動でパッケージしてくれるようにします。

特に今回使用している Pillow は OS 依存のバイナリがあるそうなので、作業している Windows にインストールしたものではなく Docker を利用して Amazon Linux 2 上でライブラリのファイルをインストールする必要があります。AWS Lambda が動作している OS は AWS Lambda runtimes で確認できます。

serverless-python-requirements プラグインのサイトではインストールコマンドは sls plugin install -n serverless-python-requirements と書いてありますが、今回作成しているプロジェクトでは package.json と serverless.xml の位置が異なるので npm install --save-dev serverless-python-requirements でインストールして serverless.yml には自分で設定を追加することにします。

npm install --save-dev serverless-python-requirements でインストールしてから、

f:id:ksby:20200610111025p:plain

serverless.yml を以下のように変更します。

service: resize-service

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true

provider:
  name: aws
  runtime: python3.8

  ..........
  • plugins を追加して serverless-python-requirements を記述します。
  • custom を追加して pythonRequirements を記述します。Docker を利用して deploy する外部ライブラリを収集するので dockerizePip: true を設定します。

IDEA の Terminal で pip freeze > requirements.txt コマンドを実行して resize-service ディレクトリの下に requirements.txt を作成します。

f:id:ksby:20200610111730p:plain

requirements.txt には以下のように出力されましたが、

boto3==1.13.26
botocore==1.16.26
docutils==0.15.2
jmespath==0.10.0
Pillow==7.1.2
python-dateutil==2.8.1
s3transfer==0.3.3
six==1.15.0
urllib3==1.25.9

Pillow 以外は不要なので削除します。

Pillow==7.1.2

deploy する

最初に serverless-python-requirements プラグインが利用する Docker コンテナが C:\Users\<ユーザ名>\AppData\Local\UnitedIncome\ の下のファイルを参照するので、アクセスできるよう Docker の設定に追加します。追加しなくても delploy 時に追加するか聞かれるのですが、ランダム文字列を含むパスが追加されるので上位のディレクトリを追加しておくことにします。

f:id:ksby:20200610191306p:plain

npx sls deploy -v コマンドを実行して deploy します。

f:id:ksby:20200610190944p:plain f:id:ksby:20200610191049p:plain

最初に lambci/lambda:build-python3.8 の Docker Image をダウンロードして requirements.txt を参照して外部ライブラリをインストールしているのが分かります。

deploy が完了すると AWS マネジメントコンソールから ksbysample-upload-bucketksbysample-resize-bucket の2つの S3 Bucket が作成されていることが確認できました。

f:id:ksby:20200610191945p:plain

画像をアップロードしてサムネイル画像を生成してみる

ksbysample-upload-bucket に sample.jpg をアップロードすると、

f:id:ksby:20200610192308p:plain f:id:ksby:20200610192450p:plain

ksbysample-resize-bucket に sample_thumb.jpg が生成されていました!

f:id:ksby:20200610192554p:plain f:id:ksby:20200610192741p:plain

S3 Bucket 内の画像ファイルを削除してから npx sls remove -v で delploy したリソース一式を削除します。

最後に

Serverless Framework と serverless-python-requirements プラグインがあったから何とか動かすことが出来た感が強いです。serverless-python-requirements プラグインなしで Python で Lambda 書くなんて考えられないんじゃないかな、と正直思いました。

履歴

2020/06/10
初版発行。