Image Delivery S3 + CloudFront + Lambda Proxy
Tip: このセットアップの役割: S3に画像をプライベートに保存し、Lambdaをスマートなプロキシレイヤーとして使用し、CloudFront CDNを通じてグローバルに配信します — 高速、低コスト、セキュアです。
📐 アーキテクチャ
User → CloudFront CDN → Lambda (proxy) → S3 Bucket (private)
| Layer | Role |
| S3 | すべての画像のプライベートオリジンストレージ |
| Lambda | プロキシ — 認証、リサイズ、ルーティング、フォーマット変換を処理 |
| CloudFront | グローバルCDN — エッジロケーションからキャッシュして配信 |
Warning: S3をCloudFrontに直接公開しない理由は?
Lambdaをプロキシとして使用することで、認証、オンザフライの画像変換、署名付きURL生成、ルーティングロジック など、S3単独では実現できない制御が可能になります。
🔧 ステップバイステップのセットアップ
1 — プライベートS3バケットの作成
aws s3api create-bucket \
--bucket my-images-bucket \
--region us-east-1
aws s3api put-public-access-block \
--bucket my-images-bucket \
--public-access-block-configuration \
"BlockPublicAcls=true,IgnorePublicAcls=true,\
BlockPublicPolicy=true,RestrictPublicBuckets=true"
🔒 バケットを完全にプライベートに保ちます。CloudFront + Lambdaが画像にアクセスする唯一の方法になります。
2 — Lambda プロキシ関数の作成
この関数はCloudFrontからリクエストを受け取り、S3から画像をフェッチして返します。
// index.mjs (Node.js 20.x, ES Module)
import { S3Client, GetObjectCommand } from "@aws-sdk/client-s3";
const s3 = new S3Client({ region: "us-east-1" });
const BUCKET = "my-images-bucket";
export const handler = async (event) => {
// パスから画像キーを抽出 例: /images/photo.jpg → images/photo.jpg
const key = decodeURIComponent(event.rawPath.slice(1));
try {
const command = new GetObjectCommand({ Bucket: BUCKET, Key: key });
const s3Response = await s3.send(command);
// ボディをバッファにストリーム
const chunks = [];
for await (const chunk of s3Response.Body) chunks.push(chunk);
const imageBuffer = Buffer.concat(chunks);
return {
statusCode: 200,
headers: {
"Content-Type": s3Response.ContentType || "image/jpeg",
"Cache-Control": "public, max-age=31536000", // 1年キャッシュ
},
body: imageBuffer.toString("base64"),
isBase64Encoded: true,
};
} catch (err) {
return { statusCode: 404, body: "Image not found" };
}
};
関数をデプロイします:
zip -r function.zip index.mjs node_modules/
aws lambda create-function \
--function-name image-proxy \
--runtime nodejs20.x \
--handler index.handler \
--role arn:aws:iam::YOUR_ACCOUNT:role/lambda-s3-role \
--zip-file fileb://function.zip
# Function URLを追加 (CloudFrontがオリジンとして使用できるように)
aws lambda create-function-url-config \
--function-name image-proxy \
--auth-type NONE
3 — LambdaにS3アクセス権限を付与
Lambda実行ロールにこのIAMポリシーをアタッチします:
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:GetObject"],
"Resource": "arn:aws:s3:::my-images-bucket/*"
}
]
}
4 — CloudFront ディストリビューションの作成
aws cloudfront create-distribution \
--origin-domain-name YOUR_LAMBDA_FUNCTION_URL \
--default-cache-behavior '{
"ViewerProtocolPolicy": "redirect-to-https",
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"]
},
"Compress": true
}'
Note: CloudFrontオリジンとしてLambda Function URL(生のS3 URLではなく)を使用します。これによってすべてのリクエストがLambdaプロキシを通じてルーティングされます。
CloudFrontの主要な設定:
| 設定 | 推奨値 |
| Viewer Protocol | HTTP を HTTPS にリダイレクト |
| Cache Policy | CachingOptimized (ビルトイン) |
| Compress Objects | Yes (gzip/brotli) |
| Price Class | すべてのエッジロケーションを使用 (またはコスト削減のため制限) |
| Custom Domain | オプション — ACM証明書で独自ドメインを追加 |
5 — S3に画像をアップロード
# 単一の画像
aws s3 cp photo.jpg s3://my-images-bucket/images/photo.jpg
# フォルダを一括アップロード
aws s3 sync ./photos/ s3://my-images-bucket/images/
# CloudFrontを経由でアクセス
# https://d1234abcd.cloudfront.net/images/photo.jpg
🧩 オプションの強化機能
オンザフライ画像リサイズ
sharpをLambdaに追加してクエリパラメータに基づいてリサイズします:
import sharp from "sharp";
// ハンドラー内で、S3からフェッチした後:
const width = parseInt(event.queryStringParameters?.w || "800");
const resized = await sharp(imageBuffer).resize(width).webp().toBuffer();
return {
statusCode: 200,
headers: { "Content-Type": "image/webp", "Cache-Control": "public, max-age=31536000" },
body: resized.toString("base64"),
isBase64Encoded: true,
};
// 使用例: https://cdn.example.com/images/photo.jpg?w=400
署名付きURL認証 (プライベート画像)
import { getSignedUrl } from "@aws-sdk/s3-request-presigner";
// まずリクエストヘッダーからトークンを検証
const token = event.headers?.authorization;
if (!isValidToken(token)) {
return { statusCode: 403, body: "Forbidden" };
}
// その後、通常通り画像を提供
💰 コスト内訳 — 月10,000枚の画像
仮定: 平均画像サイズ 500 KB、月50,000リクエスト(画像あたり5リクエスト)、ユーザーが世界中に分散、エッジでの画像キャッシュが良好。
S3 ストレージ
| Item | 計算 | 月額費用 |
| Storage (5 GB) | 5 GB × $0.023/GB | $0.12 |
| PUT requests (アップロード) | 10,000 × $0.005/1,000 | $0.05 |
| GET requests (Lambdaが取得) | ~5,000 キャッシュミス × $0.0004/1,000 | $0.00 |
| S3 合計 | ~$0.17 |
Lambda
| Item | 計算 | 月額費用 |
| Invocations | 5,000 キャッシュミス (残りはCFで提供) | 無料枠 |
| Duration (128 MB, 平均200ms) | 5,000 × 0.2s × $0.0000000021/ms | ~$0.00 |
| Lambda 合計 | ~$0.00 |
完了: Lambdaは月100万リクエストの無料枠があります — このスケールでは簡単にカバーできます。
CloudFront CDN
| Item | 計算 | 月額費用 |
| Data transfer out (25 GB) | 25 GB × $0.0085/GB (US/EU) | $0.21 |
| HTTP requests (50,000) | 50,000 × $0.0000010/request | $0.05 |
| CloudFront 合計 | ~$0.26 |
完了: CloudFront無料枠には1 TB データ転送 + 1000万リクエスト/月が最初の12ヶ月間含まれています。
📊 月額総コスト概要
| サービス | 月額費用 |
| S3 Storage + Requests | $0.17 |
| Lambda | ~$0.00 |
| CloudFront | $0.26 |
| 合計 | ~$0.43 / 月 |
🎉 10,000枚の画像と月50,000回の配信の場合、月額1ドル未満 — そして初年度は無料枠内に十分に収まります。
スケーリング参考値
| スケール | Images | Requests/月 | 推定 Cost/月 |
| Small | 10,000 | 50,000 | ~$0.43 |
| Medium | 100,000 | 500,000 | ~$4.00 |
| Large | 1,000,000 | 5,000,000 | ~$38.00 |
| Enterprise | 10,000,000 | 50,000,000 | ~$350.00 |
✅ クイックチェックリスト
-
S3バケットを作成してプライベートに設定
-
Lambda関数をS3読み取りIAMポリシーでデプロイ
-
Lambda Function URLを有効化
-
CloudFrontディストリビューションをLambda URLをオリジンとして設定
-
Lambdaレスポンスに Cache-Control ヘッダーを設定
-
カスタムドメイン + SSL証明書を設定 (オプション)
-
画像アップロードパイプラインを配置 (CLI / SDK / CI)
-
モニタリングを有効化 (Lambda用CloudWatch、CloudFrontメトリクス)