AWS中級 #3 S3 — 静的ホスティング、presigned URL

読了 9分

EC2 (#1 ~ #2) が コンピューティング の場だとすると、S3 (Simple Storage Service) は AWS の オブジェクトストレージ の場。2006 年に AWS の最初のサービスとしてリリースされた、最も古く、いまも最も広く使われているサービスの 1 つです。

S3 は実質 「無限容量のグローバルファイルシステム (ただしディレクトリは偽物)」。11 9’s (99.999999999%) の耐久性、GB あたり ~$0.023 の価格、他のすべての AWS サービスのデータハブ — この 3 点が S3 の正体。

この記事では S3 の形 → ポリシーとセキュリティ → 静的ホスティング → presigned URL → ストレージクラスを 1 本の線で通していきます。

バケットとオブジェクト #

S3 には 2 つの構成要素しかありません:

  • バケット (Bucket) — オブジェクトを格納するコンテナ。アカウント / リージョン単位で作成
  • オブジェクト (Object) — 実体ファイル。キー (key) で識別
S3 の形
my-bucket/                       ← バケット (名前はグローバル一意)
  images/
    profile/2026/avatar-001.jpg  ← オブジェクト (key = フルパス)
    profile/2026/avatar-002.jpg
  videos/
    intro.mp4
  index.html

ディレクトリは実は存在しません。 上の絵の / はキーの一部。images/profile/2026/avatar-001.jpg がオブジェクト 1 つの 完全なキー (key)。コンソールが / を区切りにフォルダのように見せているだけ。

バケット名のグローバル一意性 #

バケット名は 全世界の AWS アカウント全体で一意 である必要があります。my-bucket のような平凡な名前はとっくに誰かが取得済み。

安全なバケット名
my-company-dev-uploads-2026
acme-prod-static-ap-northeast-2

ルール:

  • 3~63 文字、小文字 / 数字 / - / .
  • ドット (.) は可だが SSL 証明書のワイルドカードで問題になる → 通常は - のみ
  • IP アドレス形式は不可、xn-- 始まり不可 (Punycode)
  • 大文字 / アンダースコア不可

名前に 環境 / 用途 / リージョン / 会社名 を埋めておくと、請求書 / 検索が楽になります。

バケットはリージョン単位 #

バケット名はグローバル一意ですが、データは 1 つのリージョンに住みますap-northeast-2 (ソウル) で作成すればオブジェクトデータはソウルのデータセンター内。コンソールに入ればどのリージョンにいるかが見えます。

リージョン間レプリケーションは S3 Replication で明示的に設定 (CRR — Cross Region Replication)。

オブジェクトの中心属性 #

オブジェクト 1 つは次を持ちます。

属性内容
Keyオブジェクトのフルパス。バケット内で一意
Body実データ (最大 5TB)
Content-Typeブラウザがどう扱うか (image/jpeg、application/json)
Metadataユーザー定義ヘッダー (x-amz-meta-*)
ACLオブジェクト単位の権限 (今はほぼ使わず、バケットポリシーに置換)
Storage Classストレージクラス (Standard、IA、Glacier 等)
Version IDバージョニング有効時のバージョン識別子
ETagコンテンツハッシュ (たいてい MD5)

コンソール / CLI / SDK でアップロード #

aws cli でアップロード / ダウンロード
# 単一ファイル
aws s3 cp ./image.jpg s3://my-bucket/images/profile/avatar.jpg

# フォルダ全体を同期
aws s3 sync ./public s3://my-bucket --delete

# Content-Type 指定
aws s3 cp ./index.html s3://my-bucket/ --content-type "text/html; charset=utf-8"

# ダウンロード
aws s3 cp s3://my-bucket/data.json ./
Python (boto3)
import boto3

s3 = boto3.client("s3")
s3.upload_file("image.jpg", "my-bucket", "images/avatar.jpg")
s3.download_file("my-bucket", "data.json", "data.json")

セキュリティの 4 つの場 #

S3 のセキュリティは 4 つの層 が重なって動きます。優先度 (上が強い):

S3 権限の評価順
1. Public Access Block      ← 最強。遮断決定がすべてに優先
2. SCP (Organizations)      ← アカウント単位ガード
3. IAM Policy               ← ユーザー / ロール単位
4. Bucket Policy            ← バケット単位
5. Object ACL               ← オブジェクト単位 (旧、ほぼ未使用)

Public Access Block — まず最初に #

Public Access Block (PAB) はバケットが誤って公開されるのを防ぐ安全装置。4 つのオプション:

オプション意味
BlockPublicAcls新しい ACL が public にならないように
IgnorePublicAcls既存の public ACL を無視
BlockPublicPolicy新しいバケットポリシーが public にならないように
RestrictPublicBuckets既に public でも IAM Principal のみアクセス可

最近のすべての新バケットは アカウントレベルで 4 つすべて有効化 がデフォルト。静的ホスティングのように意図的に public なバケットだけ明示的に解除。

アカウントレベル PAB 有効化
aws s3control put-public-access-block \
  --account-id 123456789012 \
  --public-access-block-configuration \
    BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true

Bucket Policy — JSON ポリシー #

Bucket Policy はバケットに直接付ける JSON ポリシー。誰が (Principal) 何を (Action) どこに (Resource) 行えるか定義。

ALB ログを受け取るバケットポリシー例
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "Service": "logdelivery.elasticloadbalancing.amazonaws.com"
      },
      "Action": "s3:PutObject",
      "Resource": "arn:aws:s3:::my-alb-logs/*",
      "Condition": {
        "StringEquals": {
          "s3:x-amz-acl": "bucket-owner-full-control"
        }
      }
    }
  ]
}
アプリ IAM Role のみ読取許可
{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Effect": "Allow",
      "Principal": {
        "AWS": "arn:aws:iam::123456789012:role/MyAppRole"
      },
      "Action": ["s3:GetObject", "s3:ListBucket"],
      "Resource": [
        "arn:aws:s3:::my-bucket",
        "arn:aws:s3:::my-bucket/*"
      ]
    }
  ]
}

IAM Policy #

IAM ユーザー / ロール に付けるポリシー。Bucket Policy と組み合わさって効果を生みます — 両方を通過してこそ Allow が適用 (cross-account の場合)。

アプリ IAM Role ポリシー
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject"],
    "Resource": "arn:aws:s3:::my-bucket/uploads/*"
  }]
}

詳しい IAM の場は 基礎 #2

静的サイトホスティング #

S3 は静的 HTML / CSS / JS を そのままホスティング できます。最もシンプルな静的サイトのホスティング手段の一つです。

バケットの静的ホスティング有効化
aws s3 website s3://my-static-site/ \
  --index-document index.html \
  --error-document 404.html

このコマンド後、バケットは次の URL で応答:

S3 website endpoint
http://my-static-site.s3-website-ap-northeast-2.amazonaws.com

公開アクセスを許可 #

デフォルトでは PAB が遮断します。静的ホスティングは意図された public なので次を実行:

  1. バケット PAB で BlockPublicPolicy の 2 項目を解除
  2. Bucket Policy で全員に GetObject 許可:
静的ホスティング用 public read ポリシー
{
  "Version": "2012-10-17",
  "Statement": [{
    "Sid": "PublicReadGetObject",
    "Effect": "Allow",
    "Principal": "*",
    "Action": "s3:GetObject",
    "Resource": "arn:aws:s3:::my-static-site/*"
  }]
}

S3 静的ホスティングの限界 #

S3 だけでは次ができません:

  • HTTPS (S3 website endpoint は HTTP)
  • カスタムドメイン + SSL 証明書 が直接できない
  • Edge キャッシュ (世界中で高速応答)

そのため運用ではほぼ常に S3 + CloudFront パターン — #7 CloudFront で扱います。そのときは PAB も再び有効化し、OAC で CloudFront のみアクセス許可するパターン。

Presigned URL — 一時的な権限 #

Presigned URL は「このオブジェクトを N 分間だけ誰でもダウンロード / アップロードできる」URL を生成する仕組みです。権限のないユーザーに 権限を一時委任 するパターン。

最も一般的な用途:

  • ユーザープロフィール画像のアップロード — クライアントが直接 S3 に PUT
  • 決済領収書のダウンロード — 5 分の URL
  • 非公開動画ストリーミング — 1 時間トークン
presigned PUT URL 生成 (boto3)
import boto3

s3 = boto3.client("s3")
url = s3.generate_presigned_url(
    "put_object",
    Params={
        "Bucket": "my-bucket",
        "Key": f"uploads/user-123/{filename}",
        "ContentType": "image/jpeg",
    },
    ExpiresIn=600,  # 10 分
)
# クライアントはこの URL に PUT リクエスト
curl で presigned URL を使用
curl -X PUT --upload-file ./photo.jpg "<presigned-url>"

presigned URL のセキュリティ #

  • URL 自体に 一時認証情報 が埋め込まれています。その URL があれば誰でも使える
  • 有効期限を過ぎると自動無効
  • HTTPS のみ 使う。HTTP で漏れたら露出
  • ContentType / Content-Length のような条件も埋め込み可能

POST フォーム vs PUT URL #

アップロードには 2 モード:

  • PUT URL — 単純。ヘッダーでメタデータ伝達、ContentType 1 つに固定
  • POST フォーム (presigned post) — 複雑。複数条件 (content-length-rangestarts-with 等) でより安全

大きい / 重要なアップロードは POST フォーム推奨。単純なものは PUT URL。

バージョニングとライフサイクル #

Versioning — オブジェクトの履歴 #

バケットに Versioning を有効化すると、同じキーに複数回 PUT したとき以前のバージョンが自動保存。

Versioning 有効化
aws s3api put-bucket-versioning \
  --bucket my-bucket \
  --versioning-configuration Status=Enabled

有効化後:

  • Delete も実際には消えない — Delete Marker が追加されるだけ
  • 以前のバージョンは --version-id で復元可能
  • 保存費用は全バージョンの合算 ← 罠

Lifecycle — 自動整理 / 移行 #

古いオブジェクトを 自動的に安いクラスへ移動 または削除するルール。

Lifecycle ルール例
{
  "Rules": [{
    "ID": "ArchiveOldLogs",
    "Status": "Enabled",
    "Filter": { "Prefix": "logs/" },
    "Transitions": [
      { "Days": 30,  "StorageClass": "STANDARD_IA" },
      { "Days": 90,  "StorageClass": "GLACIER" }
    ],
    "Expiration": { "Days": 365 }
  }]
}

運用ではライフサイクルがほぼ必須 — ないと 6 か月後の請求書が怖くなります

ストレージクラス — 費用の場 #

同じデータでも どれだけ頻繁に / 早く取り出すか によって異なるクラスに置けば費用が大きく節約できます。

クラスGB/月頻繁にアクセス取得時間用途
Standard$0.023毎日即時デフォルト。ホットデータ
Standard-IA$0.0125たまに即時バックアップ、分析データ
One Zone-IA$0.01たまに、再生成可能即時1 AZ のみ — 重要度低
Intelligent-Tiering自動パターン不明即時アクセス頻度が変動
Glacier Instant Retrieval$0.004四半期 1 回即時アーカイブ + たまに即時必要
Glacier Flexible Retrieval$0.0036年 1~2 回分~時間一般アーカイブ
Glacier Deep Archive$0.00099ほぼなし12 時間長期コンプライアンス

数値は ap-northeast-2 基準のおおよそ。詳細は 公式価格 を参照。

クラス決定ガイド #

決定木
このデータ、毎日 / 毎週見る ?
├── YES → Standard
└── NO →
    たまに (月 1 回以下) ?
    ├── YES → Standard-IA  (再生成可能なら One Zone-IA)
    └── NO →
        パターンが予測可能 ?
        ├── YES → Glacier 系
        └── NO  → Intelligent-Tiering

罠 — クラス遷移費用 #

Standard → IA のような遷移ごとに オブジェクトあたり ~$0.01 の小さな費用が発生。1 億オブジェクトなら? ライフサイクルで頻繁に動かしてはいけません。オブジェクトサイズ / 頻度 を見て決定。

S3 の整合性 #

昔は read-after-write の 整合性が弱かった。2020 年 12 月から全リージョンで strong consistency:

  • PUT 直後の GET が即時可能
  • DELETE 直後の LIST に即時反映

ただし バージョンオブジェクト / メタデータ変更 は依然として若干の時差がある場合があります。

S3 と他サービスの場 #

よく一緒に使う場パターン
CloudFrontS3 + Edge キャッシュ + 自前ドメイン (#7)
LambdaS3 PUT トリガーで画像変換 / インデックス (上級 #3)
AthenaS3 の CSV / Parquet / JSON を SQL で
GlueS3 のデータカタログ / ETL
CloudTrail / VPC Flow Logs / ALB Logsすべて S3 に保存

よくハマる罠 #

1) バケットが意図せず public #

ニュースに出るデータ漏洩の半分は S3。新バケットは PAB 4 つすべて有効 で開始、静的ホスティングのように意図された場のみ明示解除。

2) 費用爆弾 #

  • GB あたり保存 + リクエスト数 + データ転送 の 3 重課金
  • 特に インターネット向け転送 (Egress) が GB あたり ~$0.09 — 人気の静的サイトはここが大きい
  • CloudFront とセット にして Egress 削減 + Edge キャッシュで高速化 (#7)

3) 小さなファイルが数百万個 #

オブジェクトごとに GET / PUT 費用がかかるため、小さなファイル数百万個 パターンは費用が意外に高い。まとめて (tar.gz、Parquet) 保存するか DynamoDB などへ移すのが正解。

4) Lifecycle なしで 1 年 #

ログ / 一時ファイルが Standard のまま — 6 か月後に請求が暴騰。ライフサイクルは作成初日に同時に 設定。

5) Versioning を有効化して放置 #

Versioning + lifecycle なし = 保存費用が無限に増加。有効化したらライフサイクルで古い非カレントバージョンを整理。

6) presigned URL の有効期限が長すぎる #

24 時間の presigned URL は実質的に永続アクセス。通常は 5~15 分、長くて 1 時間。

7) s3:* ワイルドカード IAM #

Action: "s3:*" ポリシーは 危険。少なくとも GetObject / PutObject / ListBucket のように明示。

まとめ #

今回押さえたこと:

  • S3 = 無限オブジェクトストレージ。バケット (グローバル一意の名前) + オブジェクト (key) の 2 つの構成要素のみ
  • ディレクトリは偽物 — key の一部にすぎない
  • PAB → IAM Policy → Bucket Policy → ACL の順でセキュリティ評価
  • 新バケットは PAB 4 つすべて有効 がデフォルト
  • 静的ホスティング = バケット + website endpoint + public read ポリシー。HTTPS / Edge は #7 CloudFront へ
  • Presigned URL = 一時的な権限委任。5~15 分、HTTPS のみ、ContentType を埋め込む
  • Versioning + Lifecycle は対。ライフサイクルなしの versioning だけだと請求暴騰
  • ストレージクラス — Standard / IA / One Zone-IA / Intelligent-Tiering / Glacier 3 種
  • 罠 — public 漏れ、Egress 費用、小ファイル、ライフサイクル抜け、versioning 費用、presigned 期限、ワイルドカード IAM

次回 — RDS #

オブジェクトの場は掴みました。次は リレーショナル DB の場へ。

#4 RDS — マネージド DB、バックアップ、パラメータグループ では、RDS のマネージドモデル、自動バックアップと PITR、Multi-AZ、パラメータ / オプショングループ、そして運用時のマイナー vs メジャーアップグレードをどう扱うかを整理します。

X