叡智に富んだ読者の皆様、ご機嫌よう。
ダイエットのために自炊生活を始めたものの、ついつい食べ過ぎてしまい、気づけばプチ増量してしまったすぎちゃんです
前回に引き続き、S3のイベント通知機能を活用した「密告機能」について解説していきます。
今回は、上級者の皆様(?)向けに、「Lambdaを一旦経由し、SNSからメッセージを送る」といった、少し複雑な機能を実装していきます。
📕前回までのあらすじ
前回はS3のイベント通知機能を活用し、「アップロードされた画像に問題があれば即座に密告できるシステム」の構築を試みました。しかし、実装を進めるうちに思わぬ落とし穴にハマってしまうのでした
直面した問題
-
S3 → SNS に直接通知を飛ばすと、情報が貧弱すぎる!
→ どのファイルが問題なのか、誰がアップロードしたのか分からない… -
コンソールではS3オブジェクトのタグやメタデータが見えるのに、SNSの通知には含まれない!
→ 期待していた「不届き者の情報を即座にキャッチする機能」がまるで機能しない…
「このままではメールを読む私が発狂する…!」と感じた私は、
Lambdaを経由して通知内容をカスタマイズする作戦に打って出ることを決意しました!
S3イベント→Lambda→SNSでメールの出しわけをしてみる
次はLambdaを経由して読みやすいメッセージに整形した上でSNSからメッセージを送ってみましょう。
手順としては以下の通りになります。
- Lambda実行ロールの作成
- Lambda関数の設定
- Lambda関数のロジック作成
1. Lambda実行ロールの作成
1-1. IAMポリシーの作成
Lambda関数の中身を作成する前に実行ロールの設定を行う必要がございます。
今回はAWS管理ポリシーを使用せず、最小権限の原則に従い必要なポリシーのみを付与していきます。
ポリシー作成画面に遷移します。
ポリシーエディタのJSONをタップすると以下のような画面が表示されます。
以下のJSONをコピペして、ポリシーエディタにペーストしましょう。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObjectTagging"
],
"Resource": [
"arn:aws:s3:::pictures-local/*"
]
},
{
"Effect": "Allow",
"Action": "sns:Publish",
"Resource": "arn:aws:sns:us-east-1:<アカウントID>:inform_erotic_art_uploaded"
}
]
}
ポリシーの説明
-
s3:getObject
Resourceで指定されたS3バケット内にあるオブジェクトの情報を取得できるようにする -
s3:PutObjectTagging
指定したオブジェクトに任意のタグを付与できるようにする -
sns:Publish
Resourceで指定されたSNSトピックに向けてメッセージを発信できるようにする
ポリシーの設定が終われば次のステップに進みます。
ポリシー名にはEroticArtUploadNotifier
を設定しましょう。
ページ最下部にある「ポリシーの作成」をタップすると新しくカスタマー管理のロールが作成されます。
1-2. IAMロールの作成
次に実行ロールの本体を作成していきます。
ステップ1では信頼されたエンティティタイプ、ユースケースの設定を行います。
それぞれ以下のように設定しましょう。
信頼されたエンティティタイプ:AWSのサービス
ユースケース:Lambda
上記のように設定が済んだら「次へ」をタップし、ステップ2に遷移します。
ステップ2ではロールに紐付ける許可ポリシーを選択します。
許可ポリシーの下に許可の境界を設定も表示されています、他のユーザーに委任する予定がないので今回は許可の境界なしでロールを作成のままで設定します。
「次へ」をタップし、ステップ3に遷移します。
ステップ3ではロール名および説明を設定します。
ロール名の入力欄にEroticArtUploadNotificationLambdaRole
と入力します。
説明は任意項目なので特になくても大丈夫です
ロールの作成に成功すると、ロール一覧に新しく表示されていることが確認できます!
2. Lambda関数の作成
いよいよお待ちかねのLambda関数の作成です!
SNSでメッセージを送る前にLambdaを経由させるメリットは以下の通りです。
- 読みやすいように整形したメッセージをSNSに伝達できる
- SNSを直接紐づけるよりも、はるかに柔軟な処理が実現できる
2-1. 基本的な情報の設定
関数の作成画面に遷移してください。
一番上に3つの選択肢が表示されていると思いますが、
今回は設計図の使用を選択しましょう!
そうすると以下のコードが最初に設定されるので、Lambda関数の作成がかなり楽になります!
次に設計図名をドロップダウンメニューから選択します。
S3オブジェクトの情報を取得しSNSに伝達したいので、
Get S3 objectを選択します。
実行ロールは「既存のロールを使用する」を選択肢、
既存のロールにはEroticArtUploadNotificationLambdaRole
を設定します。
2-2. S3トリガーの設定
最後にS3トリガーの設定に移ります。
下にスクロールすると以下の画面が表示されます。
イベントタイプは以下のようにドロップダウンメニューで表示されます。
デフォルトで設定されている「全てのオブジェクト作成イベント」は❌ボタンで解除し、「PUT」のみを選択しましょう。
上記が全て完了したら画面最下部の「関数の作成」ボタンをタップしてください!
以下の画面が表示されるとLambda関数の作成に成功しています
3. Lambda Layerの設定
Lambda関数からAPIリクエストを送信するにあたりrequestsライブラリが必要になります。
「import requests
で呼び出せばいいじゃん?」と思う方もおられますが、残念ながらAWSではrequestsデフォルトでサポートしていないため、Lambda Layerで新しく設定する必要があります。
Lambda Layerの設定手順は以下の通りです。
- ローカルでrequestsをダウンロードする
- ダウンロードしたファイルをzip形式で圧縮する
- zipファイルをアップロード
- LambdaレイヤーのLambda関数への紐付け
予めローカル環境のPythonランタイムをAWSのランタイムと合わせておきましょう。
でないと上記の手順を完璧にこなしてもエラーが発生します。
こちらのREADMEが一番信憑性高いです。
3-1. ローカルでrequestsをダウンロードする
まずコマンドプロンプトで、requestsライブラリをローカルにダウンロードしてください。
pip install requests -t python/
3-2. ダウンロードしたファイルをzip形式で圧縮する
次に1でダウンロードした複数のファイルをzipで圧縮します。
zip -r layer.zip python/
3-3. zipファイルのアップロード
Lambdaレイヤー作成画面に遷移します。
名前はrequest_layer
と設定し、ラジオボタンではzipファイルをアップロードを選択してください。「ファイルを選択」をタップし2で作成した.zipファイルを選択すると、AWSでrequestライブラリを使えるようになります
画面最下部の「作成」ボタンをタップすると、Layerの作成に成功すると以下のような詳細画面に遷移します。
真ん中に表示されているバージョンARNはLambda関数との紐付けに使用するためコピーしておきましょう!
arn:aws:lambda:us-east-1:<アカウントID>:layer:requests_layer:<バージョン番号>
3-4. LambdaレイヤーのLambda関数への紐付け
最後にLambdaレイヤーをLambda関数に紐づける手順について説明します。
Lambda関数の詳細画面に表示されている「コード」タブを選択し、
画面最下部までスクロールすると「レイヤー」のブロックが表示されます。
ブロック右上に位置する「レイヤーの追加」ボタンをタップすると、以下のような画面が表示されます。
右下の「追加」ボタンをタップするとLambdaの詳細画面に遷移します。
成功していれば赤枠で表示されたLayerの数が0から1に増えていることがわかります。
4. Lambda関数のロジック作成
4-1. エロ画像検出APIの起動
ロジックを作成する前に↓の記事でエロ画像判別APIを起動しておいてださい。
慣れている人だと15分、初心者だと1時間ほどかかる見込みです。
4-2. Lambda関数の中身を書いていく
import json
import urllib.parse
import requests
import boto3
s3 = boto3.client('s3')
client = boto3.client('sns')
def lambda_handler(event, context):
# ① eventデータよりS3バケット名、S3オブジェクトキーを取得する
bucket = event['Records'][0]['s3']['bucket']['name']
key = urllib.parse.unquote_plus(event['Records'][0]['s3']['object']['key'], encoding='utf-8')
try:
# ② S3オブジェクトの詳細情報を取得する
response = s3.get_object(Bucket=bucket, Key=key)
# ③ オブジェクトURLを入力値とし、不適切度をNSFW_APIで算出する
object_url = "https://"+ bucket +".s3.us-east-1.amazonaws.com/" + key
nsfw_output = requests.get("http://<ECSタスクのパブリックIP>:5000/?url=" + object_url)
if('score' in nsfw_output.json()): # エロ画像検出APIが正常に動作した時
nsfw_score = nsfw_output.json()['score']
# ④ 不適切度に基づくメッセージの出しわけ
if(nsfw_score >= 0.8) :
# ④-a: エロ画像判定喰らった時
# S3オブジェクトにタグを付与する
s3.put_object_tagging(
Bucket = bucket,
Key = key,
Tagging = {
'TagSet': [
{
'Key': 'is_erotic',
'Value': '1'
}
]
})
# 不適切画像をアップロードした人の情報を通知する
params = {
'TopicArn': 'arn:aws:sns:us-east-1:<アカウントID>:inform_erotic_art_uploaded',
'Subject': '不適切画像がアップロードされました',
'Message': 'どこぞの不届きものがエロ画像をアップロードしたようです\n\n'+
'不届きも者のID: ' + response['Metadata']['user_id'] + '\n\n' +
'不届き者の名前: ' + response['Metadata']['user_name'] + '\n\n' +
'画像のURL: ' + object_url + '\n\n' +
'不適切度: ' + str(nsfw_score)
}
else:
# ④-b: 健全だと判断された場合
params = {
'TopicArn': 'arn:aws:sns:us-east-1:<アカウントID>:inform_erotic_art_uploaded',
'Subject': '健全な画像がアップロードされました',
'Message': '画像が正常にアップロードされました\n\n'+
'画像のURL: ' + object_url
}
# ⑤SNSでメッセージを送信する
client.publish(**params)
return {
'statusCode': 200,
'body': json.dumps('Message has been successfully sent! ')
}
else: # エロ画像検出APIで何かしくじった時
return {
'statusCode': 500,
'body': json.dumps('Oops!Something went wrong!')
}
except Exception as e:
print(e)
print('Error getting object {} from bucket {}. Make sure they exist and your bucket is in the same region as this function.'.format(key, bucket))
raise e
お急ぎの方は上記のコードを説明を読み飛ばしてコピペするだけでも構いません
上記のコードをざっくりと説明すると次の通りになります。
①イベントデータより、アップロード先のS3バケット名、アップロードされたオブジェクト名を取得する
②s3.get_object
を用いてS3オブジェクトの詳細情報を取得する
③ オブジェクトURLを入力値とし、不適切度をエロ画像検出APIで算出する
④ 不適切度に基づき処理の出しわけを行う(※1)
(※1: 処理の出しわけ)
- 不適切度が0.8以上の場合:
S3オブジェクトにis_erotic
タグを付与する
さらにアップロードした本人のIDとユーザー名、画像の不適切度をメールで通知する。- 不適切度が0.8未満の場合:
健全な画像がアップロードされた旨を通知する。
⑤EOF
それではちゃんと機能するか動作確認に移りましょう!
単体テスト(結合?)
最後に作成したLambda関数が正常に動作するかの確認です。
S3バケットには健全な画像、明らかなエロ画像をそれぞれ一枚以上アップロードしておきましょう
イベントJSONは以下のように設定してください。
{
"Records": [
{
"eventVersion": "2.0",
"eventSource": "aws:s3",
"awsRegion": "us-east-1",
"eventTime": "1970-01-01T00:00:00.000Z",
"eventName": "ObjectCreated:Put",
"userIdentity": {
"principalId": "EXAMPLE"
},
"requestParameters": {
"sourceIPAddress": "XX.XX.XX.XX"
},
"responseElements": {
"x-amz-request-id": "EXAMPLE123456789",
"x-amz-id-2": "EXAMPLE123/5678abcdefghijklambdaisawesome/mnopqrstuvwxyzABCDEFGH"
},
"s3": {
"s3SchemaVersion": "1.0",
"configurationId": "testConfigRule",
"bucket": {
"name": "pictures-local",
"ownerIdentity": {
"principalId": "EXAMPLE"
},
"arn": "arn:aws:s3:::pictures-local"
},
"object": {
"key": "boob_job.jpg",
"size": 1024,
"eTag": "0123456789abcdef0123456789abcdef",
"sequencer": "0A1B2C3D4E5F678901"
}
}
}
]
}
上記は、S3イベントが発火した際実際にLambdaに伝達されるデータのmockです。
実際のデータと形式を揃えることでLambdaの単体テストを行うことができます。
テストの時はobject
の中にあるkey
を随時変更するようにしてください!
健全な画像をアップロードした時
不健全な画像をアップロードした時
不適切だと判定されたS3オブジェクトのメタデータを通知内容に含めることで、リアルタイムで不届き者を密告することができます
Lambdaを経由することで、人間にとって理解しやすい文面に直した上でメールを送ってもらえる。
Laravelアプリケーションに繋ぎ込んだ上での結果
健全な画像をアップロードした場合
(「健全な画像がアップロードされました」というお題のメールが届くことを確認)
エビデンス
不健全な画像をアップロードした場合
(「不適切画像がアップロードされました」というお題のメールが届くことを確認)
エビデンス
まとめ
今回は S3のイベント通知 → Lambda → SNS 連携 を活用し、アップロードされた画像の情報を カスタマイズしたメール通知 として送信する仕組みを構築しました!
Lambdaを経由することで、SNSの通知に オブジェクトのタグやメタデータを含める ことが可能になり、 誰が・どんな画像をアップロードしたのかを明確に通知できるようになりました
参考記事
Lambdaからsnsを呼び出す
Lambdaからs3オブジェクトにタグを付与する