叡智に富んだ読者の皆様、ご機嫌よう。
健康診断と確定申告を無事に済ませ、肩の荷が降りたすぎちゃんです。
お陰で長時間ランニングをしても疲れにくくなりました。
今日はS3のイベント通知機能のアウトプットも兼ねて、エロ画像のアップロードを試みた人間を密告する機能を実装していきます。
本記事の想定読了時間
①AWSコンソール上でのみ完結したい人:0.5~1時間
②エロ画像検出APIを立ち上げた上で試したい人:1.5~2時間
📕前回までのあらすじ
本題に入る前に今までのあらすじを簡単にご説明します。
昨年は、EugenCepoiさんの作ったnsfw_apiをAWSのECRに移行し、AWS環境で画像の不適切度を0~1の範囲でスコア化するAPIの実装に成功しました。
後半ではLaravelからS3バケットに画像をアップロードできるようにしました。
アップロードされた画像のオブジェクトURLを入力値とし、不適切度をバックエンドでも取得できるようAPIの組み込みを行いました。
バックエンドでは以下のように不適切度による処理の場合分けがなされます。
-
0.8未満の場合:特に何もしない
-
0.8以上の場合:S3バケットからオブジェクトを抹消する
この機能によりあからさまなエロ画像による汚染を未然に防ぐことができ、
健全なコミュニティづくりに大きく貢献することが期待できます。
しかし成果物をAWSを業務で使っている友人に見せたところ・・・
すぎちゃんは雷を落とされてしまいました・・・
問題点
AWSが超できる友人に色々理由を聞いたところ、以下の問題点を指摘されました。
- 「誰がどの画像をいつアップロードしたか」の証跡が取れていない
- エロ画像のアップロードを検知した時、管理者側に通知を届けるべき
友人「ただ動くものを作ればいいってもんじゃない。
例えばAWSには運用上の優秀性ってのがあるでしょ?」
すぎちゃん「参考書では見たことあるけど、
それが一体何を意味しているかがわからないよ… 」
友人「例えば、誰がどのタイミングでそのAPIを実行したかのログを取る追跡可能性、EventBridgeとLambdaを組み合わせて処理を自動化することを含むんだ。」
すぎちゃん「なんか超イカしてる!
確かに今までのやつだとエロ画像がアップロードされそうになっても
アップロードを試みた人間を特定することはできなかった・・・!
だから特定できるようにすればいいってことか!」
友人「そういうこと!これをマスターすれば一人前のAWSエンジニアに近づけるよ!」
そんな経緯でS3のイベント通知を用いて「エロ画像アップロードを試みる不届きものを密告する」機能を作ることになりました。
前準備
以前は不適切だと判断された画像をS3バケットから抹消していましたが、悪質なユーザーを特定するためにもやはり行動履歴は残しておくべきであることを学びました。
今回は以下の流れでアップロードされた画像を処理します。
- S3バケットに画像をアップロードする
(この時誰がアップロードしたかの証跡を残すため、メタデータも付与する) - S3からオブジェクトURLを取得する
- 2で取得したオブジェクトURLをエロ画像検出APIの入力値とし不適切度を算出する
- 不適切度をアプリケーションに渡す
- 4で算出された不適切度に基づきアプリケーション側で以下の処理を実行する
5-a. 0.8未満の場合:何もしない
5-b. 0.8以上の場合:is_erotic
タグをS3オブジェクトに付与する
さらに不適切と判断されたS3オブジェクトにタグを付与された場合、
AWS側でS3イベント通知を送る機能も実装します。
それでは実際のAWSのコンソール画面で設定していきましょう
AWS側の設定
SNS経由でS3のイベント通知を受け取りたい場合、
以下の手順で設定を行います。
- SNSの作成
- S3イベント通知の設定
- S3イベント通知とSNSトピックとの紐付け
1. SNSの設定
まずはSNSの設定からです。
まずはトピックの作成に遷移します。
タイプはスタンダード、名前はinform_erotic_art_uploaded
にそれぞれ設定します。
画面最下部の「トピックの作成」をタップし、成功すればトピックの詳細画面に遷移します。
次にトピックに紐づくサブスクリプションの設定を行います。
「サブスクリプション」タブをタップし、赤枠で囲っているボタンをタップすると以下のようなサブスクリプション作成画面が表示されます。
次にトピックに紐づくサブスクリプションの設定を行います。
「サブスクリプション」タブをタップし、テーブルの右上の「サブスクリプションの作成」ボタンをタップすると以下のようなサブスクリプション作成画面が表示されます。
今回はメールで通知を受け取りたいので、プロトコルのドロップダウンメニューからEメールを選択します。
またエンドポイントは自分が通知を受け取りたい任意のメールアドレスを入力してください。
設定が完了すると、入力したメールアドレスのメールボックスに「Subscription Confirmation」という題名のメールが届きます。
メールのリンク「Confirm subscription」をタップすると、以下のような画面が表示されます。
この画面が表示されていれば、SNSからの通知を受け取ることができるようになります。
2. S3イベント通知の設定
S3のイベント通知の設定を行います。
任意のS3バケットの詳細画面に遷移し、
画面上部の「プロパティ」タブをタップします。
プロパティをタップした後、下にスクロールしていくと「イベント通知」のブロックが表示されます。
赤枠で囲まれている「イベント通知を作成」をタップすると、
以下のイベント通知作成画面が表示されます。
イベント名にはdetect_erotic_image_uploaded
を設定し、
イベントタイプの設定では「追加されたオブジェクトタグ」を選択してください。
3. S3イベント通知とSNSトピックとの紐付け
最後にS3イベント通知とSNSトピックの紐付けについて説明します。
任意のバケットのイベント通知作成画面の最下部までスクロールしてください。
そうすると送信先の設定画面が表示されます。
送信先にはSNSトピック、SNSトピックのドロップダウンメニューでは1-1. SNSの設定で作成したSNSトピック(inform_erotic_art_uploaded
)を選択します。
そして画面最下部の「変更の保存」をタップすると、以下のようなエラーが表示されます。
Unable to validate the following destination configurations
ググってみたところ、↑の記事が良さそうなので書いている通りにSNSのアクセスポリシーを設定しました。
再度SNSトピックの詳細画面に遷移し、画面上部の「編集」ボタンをタップします。
SNSトピックの編集画面をスクロールしていくと「アクセスポリシー - オプション」が表示されます。
アクセスポリシーを覗いてみるとどうやら、S3からのアクセスを許可していないがために先ほどのエラーが発生しました。
なのでアクセス許可を付与するべく、アクセスポリシーのStatementに以下のポリシーを追加します。
{
"Sid": "S3-policy",
"Effect": "Allow",
"Principal": {
"AWS": "*"
},
"Action": "SNS:Publish",
"Resource": "arn:aws:sns:us-east-1:<アカウントID>:inform_erotic_art_uploaded",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:s3:::pictures-local"
}
}
}
アクセスポリシーの変更を保存した後、イベント通知作成画面の「変更を保存」ボタンをタップするとイベント通知一覧にdetect_erotic_image_uploaded
が追加されていることを確認できます!
バックエンドのコードを書く
急ぎの方はこのセクションを読み飛ばしてもらっても構いません。あくまでも参考程度です
お急ぎで無い方はこちらの記事でエロ画像APIを立ち上げておいてください。慣れている方は15分、慣れていない人は1時間ほどかかりますがその分勉強になると思います
1. ログインユーザーの情報を取得
public function update(Request $request, UpdateProfileImage $updateProfileImage)
{
if(!array_key_exists("image", $request->all())){
return null;
}
$user = Auth::user(); # ①ログインユーザーの情報を取得
$uploadedFile = $request->file('image');
$nsfwResponse = $updateProfileImage->execute($uploadedFile, $user);
// 省略
}
$user = Auth::user();
でログインユーザーの情報を取得します。
それをビジネスロジック($updateProfileImage->execute()
)の引数として引き渡します。
2. ログインユーザーの情報をS3オブジェクトに付与する
①で取得したログインユーザー情報をstoreImage()
メソッドの引数として引き渡します。
public function execute(UploadedFile $uploadedFile, User $user)
{
$storeImageOutput = $this->storeImage($uploadedFile, $user); #②ログインユーザーの情報をstoreImageに引き渡す
// 省略
}
ImageUploaderTraitクラスのstoreImage
メソッドはWebサーバー上の画像をS3バケットに保管するためのクラスです。
public function storeImage(UploadedFile $uploadedFile, User $user):array
{
$path = $uploadedFile->store('public/img');
$fileContents = Storage::get($path);
$randomStr = base_convert(md5(uniqid()), 16,36);
$ext = $uploadedFile->guessExtension();
$fileName = "$randomStr.$ext";
// ②ログインユーザーの情報をS3オブジェクトに付与
$params = [
'Metadata' => [
'user_id' => $user->id,
'user_name' => $user->name
]
];
Storage::disk('s3')->put($fileName, $fileContents, $params);
return [
'url' => Storage::disk('s3')->url($fileName),
'key' => $fileName,
'local_path' => $path,
'file_contents' => $fileContents
];
}
Storage::disk('s3')->put()
メソッドの第3引数にメタデータの情報をセットします。
メタデータには①で取得したログインユーザーのID, ユーザー名を指定します。
3. エロ画像検出APIで画像の不適切度を算出する
ステップ3ではエロ画像検出APIを呼び出し、S3にアップロードされた画像の不適切度を測ります。
public function execute(UploadedFile $uploadedFile, User $user)
{
// 省略
# ③nsfw_apiを呼び出す
$nsfwApiResponse = $this->nsfwApiClient->singlePrediction($storeImageOutput['url']);
// 省略
}
class NsfwApiClient
{
public function __construct(HttpClient $httpClient)
{
$this->httpClient = $httpClient;
$this->prefix = 'http://<エロ画像検出APIのパブリックIPアドレス>:5000';
}
/**
* @param string $s3Path
* @return mixed
*/
public function singlePrediction(string $s3Path)
{
$endPoint = $this->prefix
.'/?url='
.urlencode($s3Path);
$jsonString = $this->httpClient->get($endPoint);
return json_decode($jsonString, true);
}
// 省略
}
S3オブジェクトのURLをsinglePrediction()
に引き渡し、下記のようにクエリパラメータとして設定します。
class HttpClient
{
/**
* @param string $url
* @param array $headers
* @return false|string
*/
public static function get(string $url)
{
$options = [
'http' => [
'method'=> 'GET',
'header'=> 'Content-type: application/json; charset=UTF-8'
]
];
$context = stream_context_create($options);
return file_get_contents($url, false, $context);
}
// 省略
}
http://<エロ画像検出APIのパブリックIPアドレス>:5000/?url=<S3オブジェクトのURL>
<エロ画像検出APIのパブリックIPアドレス>
の部分にはエロ画像検出APIのパブリックアドレスを指定してください。
4. 不適切と判断されたS3オブジェクトにタグ付け
最後に③で算出した不適切度が0.8以上であれば、S3オブジェクトにis_erotic
タグを付与する処理を追加します。
public function execute(UploadedFile $uploadedFile, User $user)
{
// 省略
# ④NSFWスコアが0.8以上の場合はS3オブジェクトにタグを付与する
if( $nsfwApiResponse['score'] >= 0.8 ){
# is_eroticタグを不適切画像に付与する
Storage::disk('s3')->put(
$storeImageOutput['key'],
$storeImageOutput['file_contents'],
['Tagging' => ['is_erotic'=>'1']]
);
return new NsfwOutputResponseDomain(
$nsfwApiResponse['score'],
$nsfwApiResponse['url']
);
}
// 省略
}
テスト(AWSコンソールのみで完結)
それでは動作確認のフェーズに移ります!
まず試しに、S3バケットにすけべ絵をアップロードします。
(どんなものをアップロードしたかはご想像にお任せします)
また証跡のため、メタデータの設定も行います。
今回はLaravelからの繋ぎ込みを行わないので、コンソールから手動で設定します。
将来的に誰がアップロードしたかを追跡したいので、ユーザーID・ユーザー名を記録しようと思います。
タイプはどちらも「ユーザー定義」を選択肢、
キーと値はそれぞれ以下のように設定してください。
属性名 | キー | 値 |
---|---|---|
ユーザーID | x-amz-meta-user_id |
114514 |
ユーザー名 | x-amz-meta-user_name |
tadokoro |
アップロードが完了したらすけべ絵の詳細画面に遷移し下にスクロールしてください。
タグを編集ブロックの右上に位置する「編集」ボタンをタップすると、タグの編集画面に遷移します。
以下のようにタグを編集します。
キー | 値 |
---|---|
is_erotic |
1 |
このタグを設定することで、アップロードされた画像が不適切かどうかを一目で判断できるようになります。また、特定のタグを持つオブジェクトに対して、一定期間が過ぎた後に自動削除するライフサイクルポリシーを設定することも可能です。
タグの設定が終われば画面右下の「変更の保存」をタップしましょう。
そしてメールボックスを覗いてみると・・・
なんということでしょう!!!
なんともよみづいらいJSON形式の文面が出てきたではありませんか・・・!!
タグ付与時にSNS経由でメールが送信されたことを確認することはできましたが、
付与対象のS3オブジェクトのメタデータまで詳細には記されていませんでした。
おまけにsourceIPAddressなどのセンシティブな情報が平文で送られてくるので、
セキュリティ上あまりよろしくありません
S3イベント通知からSNS経由でメールを受信すると、読みづらい文章で送られてくる。
まとめ
今回は、S3のイベント通知を活用し、S3オブジェクトのメタデータを取得して、SNSを通じて不届き者の情報を密告する機能を実装してみました。
その結果、S3のイベント通知から直接SNSを経由すると、可読性の低いJSON形式のメッセージが送信されることが分かりました
また、SNSではS3オブジェクトのメタデータを直接参照できないという課題も判明し、結果こそ思うようにいかなかったものの、大きな学びが得られました
次回は、Lambdaを経由することで柔軟な機能を実装する方法について解説します!