かんがるーさんの日記

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

外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置する(前編)

概要

記事一覧はこちらです。

前々々回、前々回、前回の記事で作成した resize-image-app-project プロジェクト をベースに別プロジェクトを作成して、外部パッケージの Pillow と独自モジュール(.py ファイル)を Lambda Layer に配置するサンプルを作成します。

長いので2回に分けます。

尚、動作させてみて分かりましたが、Lambda Layer を更新した場合、利用している Lambda 関数の方も deploy し直さないと新しいバージョンの Lambda Layer を利用してくれません。利用する Lambda Layer は latest ではなくバージョン固定で指定されていて(マネジメントコンソールで AWS Lambda の関数を見るとどのバージョンを利用しているのか表示されます)、Lambda Layer を更新しても Lambda 関数が参照する Layer のバージョンは変わらないからです。

Pillow のようなパッケージなら delploy 時間を短縮するために Lambda Layer に配置するのはありだと思いますが、ちょっとした独自の共有モジュール程度なら serverless-package-external でいいんじゃないかなという気がします。

参照したサイト・書籍

  1. AWS - Layers
    https://www.serverless.com/framework/docs/providers/aws/guide/layers/

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

    • 「Lambda Layer」の記述を参照しました。
  3. Getting started with AWS Lambda Layers for Python
    https://medium.com/@adhorn/getting-started-with-aws-lambda-layers-for-python-6e10b1f9a5d

  4. How to publish and use AWS Lambda Layers with the Serverless Framework
    https://www.serverless.com/blog/publish-aws-lambda-layers-serverless-framework/

  5. Reference CloudFormation Outputs
    https://www.serverless.com/framework/docs/providers/aws/guide/variables#reference-cloudformation-outputs

目次

  1. python-lambda-layer-project プロジェクトを作成する
  2. resize-image-app-project プロジェクトから resize_service、tests ディレクトリをコピーし、boto3、Pillow、moto をインストールする
  3. 外部パッケージの Pillow を配置する Lambda Layer 用の shared_package_layer サブプロジェクトを作成する
  4. 独自モジュール(.py ファイル)を配置する Lambda Layer 用の my_module_layer サブプロジェクトを作成する
  5. resize_service ディレクトリ内のファイルを変更する
  6. 後編に続く

手順

python-lambda-layer-project プロジェクトを作成する

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

  • python-lambda-layer-project の Empty Project を作成する。
  • Python の仮想環境を作成する。
  • Serverless Framework をローカルインストールする。
  • .envrc を作成する。
  • npm install --save-dev serverless-python-requirements

resize-image-app-project プロジェクトから resize_service、tests ディレクトリをコピーし、boto3、Pillow、moto をインストールする

resize-image-app-project プロジェクトから resize_service、tests ディレクトリをコピーします。resize_service の下の .serverless、pycache ディレクトリは削除します。

f:id:ksby:20200617012409p:plain:w300

IDEA の Terminal を起動して boto3、Pillow、moto をインストールします。

  • pip install boto3
  • pip install Pillow
  • pip install moto

コマンドラインIntelliJ IDEA からユニットテストが成功することを確認します。

f:id:ksby:20200617012533p:plain f:id:ksby:20200617012638p:plain

deploy が成功することも確認します(キャプチャは省略)。

外部パッケージの Pillow を配置する Lambda Layer 用の shared_package_layer サブプロジェクトを作成する

まずは外部パッケージの Pillow を Lambda Layer に配置します。ポイントは、Serverless Framework のドキュメントの AWS - Layers の設定は一切使わず、serverless-python-requirements プラグインのドキュメント の「Lambda Layer」に記載されている設定を使う点です。これ、分からなくて結構悩みました。。。

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

f:id:ksby:20200617015045p:plain

serverless.yml を以下の内容に変更します。

service: shared-package-layer

plugins:
  - serverless-python-requirements

custom:
  pythonRequirements:
    dockerizePip: true
    # Lambda Layer の定義は pythonRequirements の下に記述する
    layer:
      name: shared-package-layer
      description: 共通パッケージ用 Lambda Layer

provider:
  name: aws
  runtime: python3.8
  stage: dev
  region: ap-northeast-1

resources:
  Outputs:
    # 他の Stack から Lambda Layer を参照できるようにする
    # Value に記載している "PythonRequirementsLambdaLayer" はこの文字列固定である
    SharedPackageLayer:
      Value:
        Ref: PythonRequirementsLambdaLayer

以下のファイルを削除・移動します。

  • handler.py は不要なので削除します。
  • resize_service/requirements.txt を shared_package_layer ディレクトリの下に移動します。

ここまででディレクトリ構成は以下のようになります。

f:id:ksby:20200617063023p:plain

deploy します。

f:id:ksby:20200617064144p:plain f:id:ksby:20200617064309p:plain

マネジメントコンソールで確認すると Lambda Layer が作成されており(バージョンが 1 でないのは何度も試しているからです)、

f:id:ksby:20200617064544p:plain

「ダウンロード」ボタンをクリックして zip ファイルの中身を確認すると、

f:id:ksby:20200617064716p:plain

python ディレクトリの下に Pillow のパッケージが入っていました。

f:id:ksby:20200617065301p:plain

独自モジュール(.py ファイル)を配置する Lambda Layer 用の my_module_layer サブプロジェクトを作成する

外部パッケージだけでなく自分で作成したモジュール(.py ファイル)も Lambda Layer に配置してみます。ポイントは以下の2点です。

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

f:id:ksby:20200617070223p:plain

serverless.yml を以下の内容に変更します。

service: my-module-layer

provider:
  name: aws
  runtime: python3.8
  stage: dev
  region: ap-northeast-1

package:
  include:
    - ./python/**

layers:
  # こちらの名称には "Layer" は付けない
  # "my-module-layer" の前半の "my-module" だけ取り出して MyModule にする
  MyModule:
    path: .
    name: my-module-layer
    description: 独自モジュール用 Lambda Layer
    compatibleRuntimes:
      - python3.8

resources:
  Outputs:
    # How to publish and use AWS Lambda Layers with the Serverless Framework
    # https://www.serverless.com/blog/publish-aws-lambda-layers-serverless-framework/
    #
    # こちらの名称には "Layer" を付ける
    # "my-module-layer" から MyModuleLayer にする
    # 他の Stack から参照する時のキーになる
    MyModuleLayer:
      Value:
        # layers に書いた "MyModule" + "LambdaLayer" の文字列で参照する
        Ref: MyModuleLambdaLayer

handler.py は不要なので削除し、my_module_layer ディレクトリの下に python ディレクトリを作成します。

独自モジュール(.py ファイル)は my_module_layer/python の下に image_lib ディレクトリを作成し、その下に resize_utils.py を作成して、resize_service/handler.py の中に定義していた resize_image 関数を持ってくることにします。

my_module_layer/python/image_lib/resize_utils.py に以下の内容を記述します。

from PIL import Image

thumbnail_size = 320, 180


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

ここまででディレクトリ構成は以下のようになります。

f:id:ksby:20200617073328p:plain

deploy します。

f:id:ksby:20200617211754p:plain f:id:ksby:20200617211901p:plain

マネジメントコンソールで確認すると Lambda Layer が作成されており(バージョンが 1 でないのは何度も試しているからです)、

f:id:ksby:20200617212038p:plain

「ダウンロード」ボタンをクリックして zip ファイルの中身を確認すると、python ディレクトリの下に image_lib/resize_utils.py が入っていました。

f:id:ksby:20200617212232p:plain

resize_service ディレクトリ内のファイルを変更する

serverless.yml の以下の点を変更します。

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
    layers:
      # Stack名はマネジメントコンソールの CloudFormation > スタック で確認する
      - ${cf:shared-package-layer-dev.SharedPackageLayer}
      - ${cf:my-module-layer-dev.MyModuleLayer}

resources:
  Resources:
    KsbysampleResizeBucket:
      Type: AWS::S3::Bucket
      Properties:
        BucketName: ksbysample-resize-bucket
  • serverless-python-requirements プラグインを使用しないので plugins、custom の定義を削除します。
  • functions - resize の下に layers を追加し、以下の2行を追加します。他の Stack の Outputs を参照する方法は Reference CloudFormation Outputs に記載があります。
    • - ${cf:shared-package-layer.SharedPackageLayer}
    • - ${cf:my-module-layer.MyModuleLayer}

handler.py の以下の点を変更します。

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

import boto3

from image_lib import resize_utils

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


def resize(event, context):
    s3_client = boto3.client('s3')

    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_utils.resize_image(download_path, resized_path)
        s3_client.upload_file(resized_path, "ksbysample-resize-bucket", resized_key)
        logger.info('サムネイルを生成しました({})'.format(resized_key))
  • my_module_layer/python/image_lib/resize_utils.py に移動した resize_image 関数を削除します。
  • 以下の記述も削除します。
    • from PIL import Image
    • thumbnail_size = 320, 180
  • 以下の記述を追加します。
    • from image_lib import resize_utils
  • resize_image(download_path, resized_path)resize_utils.resize_image(download_path, resized_path) に変更します。

後編に続く

履歴

2020/06/20
初版発行。