はじめに
本記事では、Amazon S3に画像をアップロードしたタイミングで、自動的に画像解析を行い、その結果を別の場所に保存する仕組みについて解説します。このような構成は、画像の自動分類や不適切コンテンツの検知、検索用のタグ付けなどに活用でき、実務でも非常に有用です。AWSのマネージドサービスを組み合わせることで、サーバーの管理を意識せずに構築できる点も大きな特徴です。
AWSには画像認識サービスとしてAmazon Rekognitionが用意されており、画像に写っている物体やシーン、人物などを高精度で検出することが可能です。しかし、このサービス単体では画像を指定して解析するだけにとどまり、実運用に必要な「自動処理」には対応していません。そのため、画像のアップロードをきっかけに解析処理が自動で実行される仕組みを構築することが重要になります。
本記事で紹介する構成は、S3、SNS、Lambda、Rekognitionの4つのサービスを組み合わせたサーバーレスアーキテクチャです。各サービスの役割を分離することで、シンプルでありながら拡張性の高いシステムを実現できます。
構成概要
本構成は以下の役割分担で成り立っています。
- 画像入力:Amazon S3
- 通知(疎結合):Amazon SNS
- 実行:AWS Lambda
- 画像解析:Amazon Rekognition
- 結果保存:Amazon S3
ユーザーが入力用のS3バケットに画像をアップロードすると、そのイベントが発生します。このイベント通知はSNSトピックへ送信され、さらにそのSNSトピックから通知を受け取るLambda関数が起動します。Lambdaは受け取ったイベント情報から対象の画像を特定し、Rekognitionを利用して画像解析を実行します。そして、解析結果をJSON形式に整形し、出力用のS3バケットに保存することで、一連の処理が完結します。
このように、イベントドリブンで処理が連携される構成にすることで、手動操作を必要としない自動化が実現できます。
なぜSNSを挟むのか(疎結合のメリット)
この構成の大きなポイントは、S3とLambdaの間にSNSを挟んでいる点です。S3のイベント通知はLambdaに直接連携することも可能ですが、SNSを介在させることでシステムを疎結合に設計できます。
疎結合とは、各コンポーネント間の依存関係を弱める設計思想であり、変更や拡張に強いシステムを実現します。例えば、SNSを利用することで同一のイベントを複数の通知先に配信できるため、将来的に以下のような拡張が容易になります。
- 画像解析とは別にログ保存用のLambdaを追加する
- SQSを接続してバッチ処理に切り替える
- 外部システムへ通知する
また、S3は通知のみを担い、Lambdaは処理のみを担うという役割分担が明確になるため、障害発生時の切り分けもしやすくなります。最初は小規模な構成から始め、後から機能を追加していく場合にも、この疎結合設計は大きなメリットとなります。
全体の流れ
- 画像入力用のS3バケットに画像がアップロードされる
- S3のイベント通知がSNSトピックへ送信される
- SNSからの通知を受け取ったLambda関数が起動する
- LambdaがRekognitionで画像を分析する(例:ラベル検出)
- 解析結果(JSON)を出力用S3バケットに保存する
このように、すべての処理がイベントを起点として自動的に実行されるため、運用の手間を大幅に削減できます。
用意するもの
- 入力用S3バケット(Name:infile-bucket)
- 出力用S3バケット(Name:analysisfile-bucket)
- SNSトピック(Nema:upload-topic)
- Lambda関数(Name:rekognition-analyzer)
また、LambdaがS3やRekognitionへアクセスできるように、適切なIAMロールの設定も必要になります。ここでは、必要最小限の権限を付与することが重要です。
設定手順
1 Lambda作成(Python3.14)
作成対象のLambdaは以下の構成になります。
SNS → Messageを取り出す → JSONとして読み直す → S3バケット名とキーを取得
import json
import os
import urllib.parse
import boto3
from datetime import datetime, timezone
rekognition = boto3.client("rekognition")
s3 = boto3.client("s3")
OUTPUT_BUCKET = os.environ.get("OUTPUT_BUCKET", "analysisfile-bucket")
MIN_CONFIDENCE = float(os.environ.get("MIN_CONFIDENCE", "70"))
MAX_LABELS = int(os.environ.get("MAX_LABELS", "10"))
def lambda_handler(event, context):
if not OUTPUT_BUCKET:
raise ValueError("OUTPUT_BUCKET env var is required.")
s3_event = event
results = []
for record in s3_event.get("Records", []):
bucket = record["s3"]["bucket"]["name"]
key = urllib.parse.unquote_plus(record["s3"]["object"]["key"])
# Rekognitionでラベル検出
detect_resp = rekognition.detect_labels(
Image={"S3Object": {"Bucket": bucket, "Name": key}},
MaxLabels=MAX_LABELS,
MinConfidence=MIN_CONFIDENCE,
)
# 解析結果を必要な形に整形
now_iso = datetime.now(timezone.utc).isoformat()
payload = {
"input": {"bucket": bucket, "key": key},
"rekognition": {
"max_labels": MAX_LABELS,
"min_confidence": MIN_CONFIDENCE,
"labels": [
{
"name": lb.get("Name"),
"confidence": lb.get("Confidence"),
"parents": [p.get("Name") for p in lb.get("Parents", [])],
}
for lb in detect_resp.get("Labels", [])
],
},
"meta": {"analyzed_at": now_iso},
}
# 出力キー:results/ + 入力キー + .json
out_key = f"results/{key}.json"
s3.put_object(
Bucket=OUTPUT_BUCKET,
Key=out_key,
Body=json.dumps(payload, ensure_ascii=False).encode("utf-8"),
ContentType="application/json; charset=utf-8",
)
results.append({"input_key": key, "output_key": out_key})
return {"statusCode": 200, "results": results}
1-2 IAMポリシー作成
本ポリシーは、AWS Lambda で実行される画像解析処理に必要な最小限の権限を付与するものです。
Lambda関数が Amazon S3 上の画像を取得し、Amazon Rekognition によるラベル検出を実行し、その結果を再びS3に保存する一連の処理を実現するために設定します。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "ReadInputImages",
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::infile-bucket/*"]
},
{
"Sid": "WriteResults",
"Effect": "Allow",
"Action": ["s3:PutObject"],
"Resource": ["arn:aws:s3:::analysisfile-bucket/*"]
},
{
"Sid": "RekognitionDetectLabels",
"Effect": "Allow",
"Action": ["rekognition:DetectLabels"],
"Resource": "*"
},
{
"Sid": "WriteLogs",
"Effect": "Allow",
"Action": [
"logs:CreateLogGroup",
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "*"
}
]
}
S3は バケット単体ではなく /* まで 指定しないと GetObject/PutObject が効きません
1-3 IAMポリシーのアタッチ
本ロールは、1-2で作成したIAMポリシーを元に作成しLambdaにアタッチさせてください。
2.S3入力バケット(infile-bucket)とイベント通知用のSNSの作成
2-1.通知用SNSの作成
トピックを作成しサブスクリプションは先ほど作成したLambdaのARNを指定する。
2-2.AWSコンソール上で分析用画像アップロード用のS3バケットを作成。
※1-2 IAMポリシー作成で記入したバケット名と同じ名前にする。
作成後にObjectCreated(アップロード時)のイベント通知を設定し、送信先を作成したSNSトピックにします。
イベント通知設定の流れ
①AWSコンソールで対象のS3を選択
↓
②プロパティからイベント通知を作成を押下
↓
③一般的な設定はイベント名のみ記載、イベントタイプは「すべてのオブジェクト」を選択、作成したSNSトピックを
送信先に指定
設定のポイント
設定において重要なのは、各サービス間の連携です。SNSトピックを作成し、その通知先としてLambda関数を設定します。その後、S3バケットのイベント通知を設定し、オブジェクトがアップロードされた際にSNSへ通知が送信されるようにします。
また、LambdaではSNS経由で受け取ったイベントを正しく解釈し、対象のS3オブジェクトを特定する必要があります。この部分の理解が不十分だと、想定通りに処理が動作しない原因になります。
実行手順
1 分析対象の画像のアップロード
作成した入力用S3バケット(infile-bucket)に分析対象の画像をアップロードする。
2.対象Lambdaの処理結果をCloudwatchログで確認
Cloudwatchログからlambdaが正常に処理結果を確認
以下ログの出力結果になります。
timestampmessage
1.77722E+12"INIT_START Runtime Version: python:3.14.v39Runtime Version ARN: arn:aws:lambda:ap-northeast-1::runtime:06dae9ab05ccfc05b6ef62a6c3f8c3976f2962ec872732763a914c0041151809
1.77722E+12"START RequestId: b585ac94-4d3a-4f96-878e-fca0acd391bf Version: $LATEST
1.77722E+12'"=== EVENT ===
1777219743921,{""""Records"""": [{""""EventSource"""": """"aws:sns"""" ""EventVersion"": ""1.0" ""EventSubscriptionArn"": ""arn:aws:sns:ap-northeast-1:825157273376:upload-topic:97465ded-7f66-43a2-83e6-7b4fd3d2d483" ""Sns"": {""Type"": ""Notification" ""MessageId"": ""96fe2fe0-a684-5dd6-ad6f-4d889c5b1c98" ""TopicArn"": ""arn:aws:sns:ap-northeast-1:825157273376:upload-topic" ""Message"": ""{\""Records\"":[{\""eventVersion\"":\""2.1\"\""eventSource\"":\""aws:s3\"\""awsRegion\"":\""ap-northeast-1\"\""eventTime\"":\""2026-04-26T16:09:02.180Z\"\""eventName\"":\""ObjectCreated:Put\"\""userIdentity\"":{\""principalId\"":\""AWS:AIDA4AHZUY4QNONGMSB4M\""}\""requestParameters\"":{\""sourceIPAddress\"":\""153.246.211.254\""}\""responseElements\"":{\""x-amz-request-id\"":\""JEC4ZMVHCWGSP0NH\"\""x-amz-id-2\"":\""7EUoQKjZrhZ5Cfw62hi1F4hsmgC6SoDAYcrnztGsKazUJamhQAmIfzr4Dft3GDKyJdqhQqyQdfNBSff8xszSsFL+3uu5O142h6LIRsd6gWo=\""}\""s3\"":{\""s3SchemaVersion\"":\""1.0\"\""configurationId\"":\""analysisfile\"\""bucket\"":{\""name\"":\""infile-bucket\"\""ownerIdentity\"":{\""principalId\"":\""A2IINHQJG8W19N\""}\""arn\"":\""arn:aws:s3:::infile-bucket\""}\""object\"":{\""key\"":\""360_F_236976003_E2ScUFZwFMYLnRLE35UDb38M7IMFwmB1.jpg\"\""size\"":16397\""eTag\"":\""cdc3c899ba998d48e43fb22dbbeaa156\"\""sequencer\"":\""0069EE389E231B08D6\""}}}]}" ""Timestamp"": ""2026-04-26T16:09:03.094Z" ""SignatureVersion"": ""1" ""Signature"": ""mcEIM/9PLPsTGUpVqJRty7PrEPxL7aZQZwtEmJiy0u6+3mup5V6PQq1bMMPDNYRS1knfet1kZj/Z8IzuvTHC0I8qYFhIOgi1N9Ipyc01ahV3q37r5Q8BLe4VIby8R6jOLpAcQv8fdGdOm5K5uwqGc1H/P/qjJqxmWQss+SjMuYD8OeJ7anScOqUE9lIx6biXblrAdXXfTnxYT7diHROXs83axz+ggM74aX6i5vP33Fl/U1YZT8yOhUe0lgWmK5foN/dQczyW4l4MYNEoAEnq7U4mp4w0MqCY5wSNJ+G15yCPBH9+g9tbJ17zDJysePr/HEn7fOmAvkF26dxZTIA9aQ==" ""SigningCertUrl"": ""https://sns.ap-northeast-1.amazonaws.com/SimpleNotificationService-7506a1e35b36ef5a444dd1a8e7cc3ed8.pem" ""Subject"": ""Amazon S3 Notification" ""UnsubscribeUrl"": ""https://sns.ap-northeast-1.amazonaws.com/?Action=Unsubscribe&SubscriptionArn=arn:aws:sns:ap-northeast-1:825157273376:upload-topic:97465ded-7f66-43a2-83e6-7b4fd3d2d483" ""MessageAttributes"": {}}}]}
1777219743921,bucket=infile-bucket
1777219743921,key=360_F_236976003_E2ScUFZwFMYLnRLE35UDb38M7IMFwmB1.jpg
1777219744572,Rekognition OK
1777219744572,OUTPUT: analysisfile-bucket/results/360_F_236976003_E2ScUFZwFMYLnRLE35UDb38M7IMFwmB1.jpg.json
1777219744800,S3 PUT OK
1777219744802,END RequestId: b585ac94-4d3a-4f96-878e-fca0acd391bf
1777219744802,REPORT RequestId: b585ac94-4d3a-4f96-878e-fca0acd391bfDuration: 881.63 msBilled Duration: 1403 msMemory Size: 128 MBMax Memory Used: 98 MBInit Duration: 520.63 ms
※「Rekognition OK」と「S3 PUT OK」と出力されているため、正常に処理が行われたのを確認できます。
3.分析結果の確認
出力用S3バケット(analysisfile-bucket)に分析結果が保存されているのことを確認する。
こちらが分析結果のファイルになります。
分析結果のファイルの中身になります。
{
"input": {
"bucket": "infile-bucket",
"key": "360_F_236976003_E2ScUFZwFMYLnRLE35UDb38M7IMFwmB1.jpg"
},
"rekognition": {
"labels": [
{
"name": "Animal",
"confidence": 100.0,
"display": "Animal 100.0%"
},
{
"name": "Cat",
"confidence": 100.0,
"display": "Cat 100.0%"
},
{
"name": "Mammal",
"confidence": 100.0,
"display": "Mammal 100.0%"
},
{
"name": "Manx",
"confidence": 100.0,
"display": "Manx 100.0%"
},
{
"name": "Pet",
"confidence": 100.0,
"display": "Pet 100.0%"
},
{
"name": "Angora",
"confidence": 97.9,
"display": "Angora 97.9%"
},
{
"name": "Kitten",
"confidence": 82.9,
"display": "Kitten 82.9%"
}
]
},
"meta": {
"analyzed_at": "2026-04-26T16:09:04.572067+00:00"
}
}
Rekognitionの分析結果で、対象の画像がAnimalとCatが100%だというのが確認できました。
運用面での工夫
実運用では、コストや保守性を考慮した設計も重要になります。その一つがS3のライフサイクル機能の活用です。解析結果のJSONファイルは長期間保持する必要がない場合も多く、一定期間経過後に自動削除したり、低コストなストレージへ移行することでコストを抑えることができます。
また、CloudWatch Logsを活用することで、Lambdaの実行状況やエラーを確認できます。特に初期構築時はログをこまめに確認し、想定通りに処理が流れているかをチェックすることが重要です。
さらに、処理件数が増えてきた場合には、Lambdaの同時実行数の調整やSQSの導入を検討することで、より安定したシステム運用が可能になります。
実務での活用例
この構成は実務でも幅広く活用できます。例えば、ユーザーが投稿した画像を自動で解析し、不適切なコンテンツが含まれていないかをチェックする仕組みや、ECサイトにおける商品画像の自動タグ付けなどが代表的な例です。
また、画像の内容をメタデータとして蓄積することで、後から検索性を高めたり、分析に活用したりすることも可能になります。こうした用途では、サーバーレス構成によるスケーラビリティの高さが大きなメリットになります。
よくあるハマりどころ
構築時に注意すべきポイントもいくつかあります。
まず、SNSから受け取るメッセージは文字列形式であるため、そのままでは扱えず、JSONとしてパースする必要があります。また、IAMポリシーの設定が不十分な場合、S3へのアクセスやRekognitionの実行が失敗することがあります。
さらに、各サービスのリージョンが異なると想定外のエラーが発生することがあるため、基本的には同一リージョンで構成することが推奨されます。
まとめ
本記事では、S3への画像アップロードを起点として、Rekognitionによる画像解析を自動実行し、その結果をS3に保存する構成を紹介しました。このようなイベントドリブンなアーキテクチャは、AWSのサービス連携を理解する上で非常に良い題材です。
特にSNSを挟むことで疎結合な設計を実現でき、将来的な拡張や変更にも柔軟に対応できる点が大きなメリットです。まずは基本的なラベル検出から始め、将来的には不適切画像検知や顔認識などへ応用することで、より高度なシステムへ発展させることができます。
サーバーレスとAIサービスを組み合わせた本構成は、実務でもそのまま活用できるため、ぜひ一度構築して理解を深めてみてください。


