Amazon S3とは?
Amazon S3 は、オブジェクトストレージサービスであり、以下の特徴を持っています。
- 業界最高レベルのスケーラビリティ、可用性、セキュリティ、パフォーマンスを提供
- データレイク、Webサイト、クラウドネイティブアプリ、バックアップ、アーカイブ、機械学習、分析など、さまざまな用途で利用可能
- 99.999999999%(11ナイン)の耐久性を持ち、世界中の数百万のユーザーに利用されている
👉 簡単に言うと
**「どんなファイルでも、安全に・無限に近い容量で保存できるクラウドストレージ」**です。
S3の基本構造(ざっくり理解)
S3は以下のようなシンプルな構造です:
- Bucket(バケット):ファイルを保存する箱
- Object(オブジェクト):実際のファイル(画像・動画など)
-
Key:ファイルのパス(例:
images/avatar.png)
Use Cases(ユースケース)
S3は非常に幅広い用途で使われます。代表的なユースケースを紹介します。
1. データレイクの構築(Build a Data Lake)
ビッグデータ分析、AI、機械学習(ML)、HPC(高性能計算)などの用途で、
大量データを保存・分析する基盤として利用されます。
👉 データを一元管理し、価値あるインサイトを引き出すことが可能
2. クラウドネイティブアプリの実行(Run Cloud-Native Applications)
モバイルアプリやWebアプリの静的ファイル(画像・JS・CSSなど)をS3に配置し、
スケーラブルな構成で配信できます。
👉 高可用性かつ自動スケールするアプリ構築が可能
3. バックアップとリストア(Backup and Restore Critical Data)
重要なデータのバックアップ保存先として利用されます。
- RTO(復旧時間目標)
- RPO(復旧時点目標)
などの要件を満たしつつ、安全にデータを保管できます。
👉 災害対策(DR)としても非常に重要
4. 低コストでのアーカイブ(Archive Data at the Lowest Cost)
使用頻度の低いデータを S3 Glacier などに移動することで、
コストを大幅に削減できます。
👉 長期保存(ログ・監査データなど)に最適
バケット作成(詳細手順)
S3を使うために、まず「バケット」を作成します。
手順:
- AWSコンソール → S3を開く
- 「バケットを作成」をクリック
- 以下を設定:
-
バケット名:グローバルで一意(例:
my-app-bucket-123) -
リージョン:
ap-northeast-1(東京) -
パブリックアクセス設定:
- 開発環境 → 一部許可でもOK
- 本番 → 基本はBlock推奨
4.「作成」をクリック
バケットポリシー(公開する場合)
画像を公開したい場合は、以下のポリシーを設定:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "PublicRead",
"Effect": "Allow",
"Principal": "*",
"Action": ["s3:GetObject"],
"Resource": ["arn:aws:s3:::my-app-bucket-123/*"]
}
]
}
NestJSでS3にアップロードする
実務では、バックエンド(NestJS)からS3にアップロードすることが多いです。
1. パッケージインストール
npm install @aws-sdk/client-s3
2. S3サービスを作成
2.1 S3設定(config)
AWSの接続設定をまとめます。
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
@Injectable()
export class S3ConfigService {
constructor(private readonly configService: ConfigService) {}
private requireNonEmpty(key: string): string {
const value = this.configService.getOrThrow<string>(key);
if (!value || value.trim() === '') {
throw new Error(`Configuration "${key}" is required but was empty.`);
}
return value;
}
get region(): string {
return this.requireNonEmpty('AWS_S3_REGION');
}
get accessKeyId(): string {
return this.requireNonEmpty('AWS_S3_ACCESS_KEY_ID');
}
get secretAccessKey(): string {
return this.requireNonEmpty('AWS_S3_SECRET_ACCESS_KEY');
}
get bucketName(): string {
return this.requireNonEmpty('AWS_S3_BUCKET_NAME');
}
get endpoint(): string | undefined {
const value = this.configService.get<string>('AWS_S3_ENDPOINT');
return value && value.trim() !== '' ? value : undefined;
}
}
2.2 ヘルパー(helper)
ファイルキーやURL生成ロジックを分離します。
import {
S3Client,
PutObjectCommand,
type PutObjectCommandInput,
type S3ClientConfig,
} from '@aws-sdk/client-s3';
import { v4 as uuidv4 } from 'uuid';
export const createS3Client = (config: S3ClientConfig): S3Client =>
new S3Client(config);
export const createPutObjectCommand = (
input: PutObjectCommandInput,
): PutObjectCommand => new PutObjectCommand(input);
export const generateUuid = (): string => uuidv4();
2.3 S3サービス(service)
実際のアップロード処理を行います。
import { Injectable, BadRequestException } from '@nestjs/common';
import type { S3Client } from '@aws-sdk/client-s3';
import { S3ConfigService } from '../common/config/s3.config';
import {
createS3Client,
createPutObjectCommand,
generateUuid,
} from './s3.helpers';
export type UploadType = 'image' | 'video';
/** S3 key prefix segment: `public/uploads/...`. */
export type UploadScope = 'public';
interface UploadedFile {
buffer: Buffer;
mimetype: string;
size: number;
originalname: string;
}
@Injectable()
export class UploadService {
private readonly s3Client: S3Client;
constructor(private readonly s3Config: S3ConfigService) {
const region = this.s3Config.region;
const client = createS3Client({
region,
credentials: {
accessKeyId: this.s3Config.accessKeyId,
secretAccessKey: this.s3Config.secretAccessKey,
},
endpoint: this.s3Config.endpoint,
forcePathStyle: !!this.s3Config.endpoint,
});
this.s3Client = client;
}
async uploadFile(
file: UploadedFile | undefined,
type: UploadType,
scope: UploadScope,
): Promise<{ url: string; type: UploadType }> {
if (!file) {
throw new BadRequestException('upload.file_missing');
}
this.validateFile(file, type);
const key = this.buildObjectKey(file, type, scope);
const command = createPutObjectCommand({
Bucket: this.s3Config.bucketName,
Key: key,
Body: file.buffer,
ContentType: file.mimetype,
});
await this.s3Client.send(command);
const url = this.buildPublicUrl(key);
return { url, type };
}
private validateFile(file: UploadedFile, type: UploadType): void {
const isImage = file.mimetype.startsWith('image/');
const isVideo = file.mimetype.startsWith('video/');
if (type === 'image') {
if (!isImage) {
throw new BadRequestException('upload.unsupported_mime_type');
}
const maxBytes = 5 * 1024 * 1024;
if (file.size > maxBytes) {
throw new BadRequestException('upload.image_size_exceeded');
}
}
if (type === 'video') {
if (!isVideo) {
throw new BadRequestException('upload.unsupported_mime_type');
}
const maxBytes = 10 * 1024 * 1024;
if (file.size > maxBytes) {
throw new BadRequestException('upload.video_size_exceeded');
}
}
}
private buildObjectKey(
file: UploadedFile,
type: UploadType,
scope: UploadScope,
): string {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const extension = this.getFileExtension(file.originalname);
const uniqueId = generateUuid();
return `${scope}/uploads/${type}/${year}/${month}/${uniqueId}.${extension}`;
}
private getFileExtension(filename: string): string {
const parts = filename.split('.');
const rawExtension = parts.length > 1 ? parts[parts.length - 1] : undefined;
return rawExtension?.toLowerCase() ?? 'bin';
}
private buildPublicUrl(key: string): string {
if (this.s3Config.endpoint) {
const endpoint = this.s3Config.endpoint.replace(/\/$/, '');
return `${endpoint}/${this.s3Config.bucketName}/${key}`;
}
return `https://${this.s3Config.bucketName}.s3.${this.s3Config.region}.amazonaws.com/${key}`;
}
}
3. ControllerでアップロードAPI
import {
BadRequestException,
Body,
Controller,
Post,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import {
ApiBadRequestResponse,
ApiBody,
ApiConsumes,
ApiOkResponse,
ApiOperation,
ApiTags,
} from '@nestjs/swagger';
import { FileInterceptor } from '@nestjs/platform-express';
import { memoryStorage } from 'multer';
import { SwaggerTag } from '../common/constants/swagger-tag.constant';
import { UploadRequestDto } from './dto/upload-request.dto';
import { UploadResponseDto } from './dto/upload-response.dto';
import { UploadService } from './upload.service';
const uploadMultipartBody = {
description: 'Upload payload.',
schema: {
type: 'object' as const,
required: ['type', 'file'],
properties: {
type: { type: 'string', enum: ['image', 'video'] },
file: { type: 'string', format: 'binary' },
},
},
};
@ApiTags(SwaggerTag.UPLOADS)
@Controller('uploads')
export class UploadsController {
constructor(private readonly uploadService: UploadService) {}
@Post()
@ApiOperation({
summary: 'Upload file',
description: 'Upload an image or video file to object storage.',
})
@ApiConsumes('multipart/form-data')
@ApiBody(uploadMultipartBody)
@ApiOkResponse({
description: 'File uploaded successfully.',
type: UploadResponseDto,
})
@ApiBadRequestResponse({ description: 'Invalid upload request.' })
@UseInterceptors(FileInterceptor('file', { storage: memoryStorage() }))
async upload(
@UploadedFile() file: Express.Multer.File | undefined,
@Body() body: UploadRequestDto,
): Promise<UploadResponseDto> {
if (!body.type) {
throw new BadRequestException('upload.type_invalid');
}
const result = await this.uploadService.uploadFile(
file,
body.type,
'public',
);
const response = new UploadResponseDto();
response.url = result.url;
response.type = result.type;
return response;
}
}
4. フロントからアップロード
POST /upload
Content-Type: multipart/form-data
🔥 実務Tips(重要)
- 本番では ACL public-readは非推奨
- → CloudFront + Private S3 がベスト
- → または Presigned URL
