S3への画像アップロードをトリガーに、それを加工するようなLambdaを1コマンドでつくれるやつです!
- 通知を設定する方法
- Lambdaで外部ライブラリを使う方法
- SAMでLambdaをデプロイする方法
あたりの情報がひとつにまとまった記事が見つけられなかったので書きます。
ファイル準備
ディレクトリ作成
mkdir sam-lambda-s3
ファイルとか作成
touch template.yaml mkdir function python touch function/function.py function/requirements.txt
template.yml(CloudFormationのやつ)
概ね公式の内容通りですが、一部調整してます。
CloudFormation を使用して、既存の S3 バケットで Lambda 用の Amazon S3 通知設定を作成する方法を教えてください。 https://aws.amazon.com/jp/premiumsupport/knowledge-center/cloudformation-s3-notification-lambda/
先に内容の概要を紹介
S3NotificationLambdaFunction
- S3からの通知を受け取るLambda
- 処理自体はfunction/function.py
LibraryLayer
- S3NotificationLambdaFunctionで使うライブラリを置いておくLambdaLayer
- python/ 以下がデプロイされるように設定している
LambdaInvokePermission
- S3NotificationLambdaFunctionのPermission
LambdaIAMRole
- S3NotificationLambdaFunctionのIAMロール
CustomResourceLambdaFunction
- S3通知を設定するLambda
LambdaTrigger
- S3通知の設定
template.yaml
AWSTemplateFormatVersion: 2010-09-09 Transform: 'AWS::Serverless-2016-10-31' Description: S3から通知を受けるLambdaを作成する Parameters: NotificationBucket: Type: String Description: 通知を設定するS3バケット名 S3NotificationLambdaFunctionName: Type: String Description: S3からの通知を受ける関数名 Prefix: Type: String Description: 通知対象のファイルのPrefix(フォルダ等) Resources: S3NotificationLambdaFunction: Type: 'AWS::Serverless::Function' Properties: FunctionName: !Ref S3NotificationLambdaFunctionName CodeUri: function/ Handler: function.lambda_handler Role: !GetAtt LambdaIAMRole.Arn Runtime: python3.9 Timeout: 5 Layers: - !Ref LibraryLayer LibraryLayer: Type: "AWS::Serverless::LayerVersion" Properties: LayerName: PythonLibraryLayer ContentUri: python/ CompatibleRuntimes: - python3.9 RetentionPolicy: Delete LambdaInvokePermission: Type: 'AWS::Lambda::Permission' Properties: FunctionName: !GetAtt S3NotificationLambdaFunction.Arn Action: 'lambda:InvokeFunction' Principal: s3.amazonaws.com SourceAccount: !Ref 'AWS::AccountId' SourceArn: !Sub 'arn:aws:s3:::${NotificationBucket}' LambdaIAMRole: Type: 'AWS::IAM::Role' Properties: AssumeRolePolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Principal: Service: - lambda.amazonaws.com Action: - 'sts:AssumeRole' Path: / ManagedPolicyArns: - 'arn:aws:iam::aws:policy/AmazonS3FullAccess' - 'arn:aws:iam::aws:policy/AmazonRekognitionFullAccess' Policies: - PolicyName: root PolicyDocument: Version: 2012-10-17 Statement: - Effect: Allow Action: - 's3:GetBucketNotification' - 's3:PutBucketNotification' Resource: !Sub 'arn:aws:s3:::${NotificationBucket}' - Effect: Allow Action: - 'logs:CreateLogGroup' - 'logs:CreateLogStream' - 'logs:PutLogEvents' Resource: 'arn:aws:logs:*:*:*' CustomResourceLambdaFunction: Type: 'AWS::Lambda::Function' Properties: Handler: index.lambda_handler Role: !GetAtt LambdaIAMRole.Arn Code: ZipFile: | from __future__ import print_function import json import boto3 import cfnresponse SUCCESS = "SUCCESS" FAILED = "FAILED" print('Loading function') s3 = boto3.resource('s3') def lambda_handler(event, context): print("Received event: " + json.dumps(event, indent=2)) responseData={} try: if event['RequestType'] == 'Delete': print("Request Type:",event['RequestType']) Bucket=event['ResourceProperties']['Bucket'] delete_notification(Bucket) print("Sending response to custom resource after Delete") elif event['RequestType'] == 'Create' or event['RequestType'] == 'Update': print("Request Type:",event['RequestType']) LambdaArn=event['ResourceProperties']['LambdaArn'] Bucket=event['ResourceProperties']['Bucket'] Prefix=event['ResourceProperties']['Prefix'] add_notification(LambdaArn, Bucket, Prefix) responseData={'Bucket':Bucket} print("Sending response to custom resource") responseStatus = 'SUCCESS' except Exception as e: print('Failed to process:', e) responseStatus = 'FAILED' responseData = {'Failure': 'Something bad happened.'} cfnresponse.send(event, context, responseStatus, responseData) def add_notification(LambdaArn, Bucket, Prefix): bucket_notification = s3.BucketNotification(Bucket) response = bucket_notification.put( NotificationConfiguration={ 'LambdaFunctionConfigurations': [ { 'LambdaFunctionArn': LambdaArn, 'Events': [ 's3:ObjectCreated:*' ], 'Filter': {'Key': {'FilterRules': [ {'Name': 'Prefix', 'Value': Prefix} ]}} } ] } ) print("Put request completed....") def delete_notification(Bucket): bucket_notification = s3.BucketNotification(Bucket) response = bucket_notification.put( NotificationConfiguration={} ) print("Delete request completed....") Runtime: python3.9 Timeout: 50 LambdaTrigger: Type: 'Custom::LambdaTrigger' DependsOn: LambdaInvokePermission Properties: ServiceToken: !GetAtt CustomResourceLambdaFunction.Arn LambdaArn: !GetAtt S3NotificationLambdaFunction.Arn Bucket: !Ref NotificationBucket Prefix: !Ref Prefix
画像処理にPillowを使います
requirements.txt
Pillow
インストール
pip3 install -r function/requirements.txt -t python
function.pyを編集
import json import boto3 import os from PIL import Image WIDTH_INDEX = 0 HEIGHT_INDEX = 1 def lambda_handler(event, context): s3 = boto3.client('s3') created_images = event["Records"] for image in created_images: created_image_path = image["s3"]["object"]["key"] bucket_name = image["s3"]["bucket"]["name"] temp_file_path = u'/tmp/' + os.path.basename(created_image_path) print(f'target_image is {created_image_path}') s3.download_file(Bucket=bucket_name, Key=created_image_path, Filename=temp_file_path) target_image = Image.open(temp_file_path) image_width = target_image.size[WIDTH_INDEX] image_height = target_image.size[HEIGHT_INDEX] is_width_shorter = image_width < image_height upper_coordinate = 0 left_coordinate = 0 lower_coordinate = 0 right_coordinate = 0 if is_width_shorter: left_coordinate = 0 right_coordinate = image_width upper_coordinate = image_height / 2 - image_width / 2 lower_coordinate = image_height / 2 + image_width / 2 else: upper_coordinate = 0 lower_coordinate = image_height left_coordinate = image_width / 2 + image_height / 2 right_coordinate = image_width / 2 - image_height / 2 cropped_image = target_image.crop((left_coordinate, upper_coordinate, right_coordinate, lower_coordinate)) cropped_file_temp_path = u'/tmp/cropped-' + os.path.basename(created_image_path) cropped_image.save(cropped_file_temp_path) s3.upload_file(Filename=cropped_file_temp_path, Bucket=bucket_name, Key=f'cropped/{created_image_path}') return { 'statusCode': 200, 'body': json.dumps('Success!') }
awsコンソールからs3のバケットを作成する(すでに自由に使えるバケットがあるならスキップでOK)
ビルドとデプロイ
sam build sam deploy --guided
以下にダイアログの例を置いておきます。 NotificationBucketは作成したバケット名を指定する必要がありますが、それ以外はよしなに進めればOKです。
$ sam deploy --guided Configuring SAM deploy ====================== Looking for config file [samconfig.toml] : Found Reading default arguments : Success Setting default arguments for 'sam deploy' ========================================= Stack Name [sam-lambda-s3]: デフォルトでOK AWS Region [ap-northeast-1]: デフォルトでOK Parameter NotificationBucket []: <作成したバケット名を入力> Parameter S3NotificationLambdaFunctionName []: <Lambdaの関数名を自由に入力> Parameter Prefix []: original/ (このフォルダ以下に画像が追加されると通知が行われ、Lambdaが実行されます。特に好みがなければoriginalと入れておきましょう。) #Shows you resources changes to be deployed and require a 'Y' to initiate deploy Confirm changes before deploy [y/N]: #SAM needs permission to be able to create roles to connect to the resources in your template Allow SAM CLI IAM role creation [Y/n]: #Preserves the state of previously provisioned resources when an operation fails Disable rollback [y/N]: Save arguments to configuration file [Y/n]: SAM configuration file [samconfig.toml]: SAM configuration environment [default]:
デプロイが完了したら、S3に画像上げて→Lambdaが動き→加工された画像が/croppedに入ってるのを確認してみましょう。
以上です。