목차
10 장

S3 — 정적 호스팅, presigned URL

AWS의 가장 오래된 객체 스토리지 S3. 버킷의 모양과 이름의 글로벌 유일성, 정책과 Public Access Block, 정적 사이트 호스팅, presigned URL, 그리고 스토리지 클래스로 비용을 낮추는 패턴까지 정리합니다.

8장 ~ 9장의 EC2가 컴퓨팅 영역이라면, **S3 (Simple Storage Service)**는 AWS의 객체 스토리지 영역입니다. 2006년에 AWS의 첫 서비스로 출시된 가장 오래된 서비스이자, 지금도 가장 많이 쓰이는 서비스 중 하나입니다.

S3는 사실상 무한대 용량의 글로벌 파일 시스템입니다(단 디렉터리는 가짜입니다). 11 9’s (99.999999999%)의 내구성, GB 당 약 $0.023의 가격, 다른 모든 AWS 서비스의 데이터 허브 — 이 셋이 S3의 정체입니다.

본 챕터에서는 S3의 구조에서 시작해 정책과 보안, 정적 호스팅, presigned URL, 스토리지 클래스까지 한 흐름으로 정리합니다. 여기서 다루는 정적 호스팅은 14장 CloudFront에서 HTTPS와 Edge 캐시를 더해 완성되고, S3 PUT 트리거는 17장 Lambda 기초의 이벤트 처리로 이어집니다.

버킷과 객체 #

S3는 기억할 핵심이 두 가지뿐입니다.

  • 버킷 (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가 객체 하나의 전체 키(key)입니다. 콘솔이 / 기준으로 폴더처럼 보여줄 뿐입니다.

버킷 이름의 글로벌 유일성 #

버킷 이름은 전 세계 AWS 계정 모두를 가로질러 유일해야 합니다. my-bucket 같은 평범한 이름은 이미 누군가 가져갔습니다.

안전한 버킷 이름
my-company-dev-uploads-2026
acme-prod-static-ap-northeast-2

규칙은 다음과 같습니다.

  • 3~63 자, 소문자 / 숫자 / - / .를 씁니다.
  • 점(.)은 가능하지만 SSL 인증서 와일드카드에서 문제가 되므로 보통 -만 씁니다.
  • IP 주소 형태는 안 되고, xn--로 시작할 수 없습니다(Punycode).
  • 대문자와 언더스코어는 안 됩니다.

이름에 환경 / 용도 / 리전 / 회사명을 적어 두면 청구서와 검색이 편합니다.

버킷은 리전 단위 #

버킷 이름은 글로벌 유일이지만, 데이터는 한 리전에 살고 있습니다. ap-northeast-2 (서울)에 만들면 객체 데이터는 서울 데이터센터 안에 있습니다. 콘솔에 들어가면 어떤 리전에 있는지 보입니다. 이 글로벌 이름 / 리전 데이터의 구분은 1장 AWS 입문의 글로벌 서비스 vs 리전 서비스 논의와 같은 맥락입니다.

리전 간 복제는 S3 Replication (CRR, Cross Region Replication)으로 명시적으로 설정합니다.

객체의 핵심 속성 #

객체 하나는 다음을 가집니다.

속성역할
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의 보안은 여러 층이 겹쳐서 동작합니다. 평가 순서는 위에서 아래로 갈수록 약해집니다.

S3 권한의 평가 순서
1. Public Access Block      ← 가장 강력. 차단 결정이 모든 것 위에
2. SCP (Organizations)      ← 계정 단위 가드
3. IAM Policy               ← 사용자 / 역할 단위
4. Bucket Policy            ← 버킷 단위
5. Object ACL               ← 객체 단위 (옛 방식, 거의 안 씀)

Public Access Block — 가장 먼저 #

**Public Access Block (PAB)**은 버킷이 실수로 공개되는 것을 막는 안전장치입니다. 네 가지 옵션이 있습니다.

옵션의미
BlockPublicAcls새 ACL이 public 못 되게
IgnorePublicAcls기존 public ACL 무시
BlockPublicPolicy새 버킷 정책이 public 못 되게
RestrictPublicBuckets이미 public 인 버킷도 IAM Principal만 접근

요즘 모든 새 버킷은 계정 레벨에서 네 개 모두 켜는 것이 기본값입니다. 정적 호스팅처럼 일부러 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와 결합되어 효과를 만듭니다. cross-account의 경우 둘 다 통과해야 Allow가 적용됩니다.

앱 IAM Role 정책
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Action": ["s3:GetObject", "s3:PutObject"],
    "Resource": "arn:aws:s3:::my-bucket/uploads/*"
  }]
}

자세한 IAM 설정은 2장 IAM에서 다룹니다.

정적 사이트 호스팅 #

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. 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 패턴을 씁니다(14장 CloudFront에서 다룹니다). 그때는 PAB도 다시 켜고 OAC로 CloudFront만 접근을 허용하는 패턴이 됩니다.

Presigned URL — 임시 권한 #

Presigned URL은 이 객체를 N 분 동안만 누구나 다운로드 또는 업로드할 수 있다는 임시 권한을 만드는 방식입니다. 권한이 없는 사용자에게 권한을 잠시 위임하는 패턴입니다.

가장 흔한 사용처는 다음과 같습니다.

  • 사용자 프로필 이미지 업로드 — 클라이언트가 직접 S3에 PUT 합니다.
  • 결제 영수증 다운로드 — 5분짜리 링크입니다.
  • 비공개 비디오 스트리밍 — 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 #

업로드 방식은 두 가지입니다.

  • PUT URL — 단순합니다. 헤더로 메타데이터를 전달하고, ContentType 한 개를 고정합니다.
  • POST 폼 (presigned post) — 복잡합니다. 다중 조건(content-length-range, starts-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 }
  }]
}

운영에 라이프사이클은 거의 필수입니다. 없으면 몇 달 뒤 청구서가 무서워집니다.

스토리지 클래스 — 비용 항목 #

같은 데이터를 얼마나 자주, 얼마나 빨리 꺼내는지에 따라 다른 클래스로 두면 비용이 크게 절약됩니다.

클래스GB/월자주 접근검색 시간역할
Standard$0.023매일즉시기본값. 핫 데이터
Standard-IA$0.0125가끔즉시백업, 분석 데이터
One Zone-IA$0.01가끔, 재생성 가능즉시한 AZ만 — 중요도 낮음
Intelligent-Tiering자동패턴 모름즉시접근 빈도가 들쭉날쭉
Glacier Instant Retrieval$0.004분기 1회즉시아카이브 + 가끔 즉시 필요
Glacier Flexible Retrieval$0.00361년 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 캐시 + 사용자 도메인 (14장)
LambdaS3 PUT 트리거로 이미지 변환 / 인덱싱 (17장)
AthenaS3의 CSV / Parquet / JSON을 SQL로
GlueS3의 데이터 카탈로그 / ETL
CloudTrail / VPC Flow Logs / ALB Logs모두 S3에 저장

자주 만나는 함정 #

  • 버킷이 의도치 않게 public — 뉴스에 나오는 데이터 유출의 절반은 S3입니다. 새 버킷은 PAB 네 개를 모두 켠 상태로 시작하고, 정적 호스팅처럼 의도된 경우만 명시 해제합니다.
  • 비용 폭탄 — GB 당 저장 + 요청 수 + 데이터 전송의 3중 과금입니다. 특히 인터넷으로 나가는 트래픽(Egress)이 GB 당 약 $0.09 라서 인기 정적 사이트는 이것이 큽니다. CloudFront와 묶어 Egress를 절감하고 Edge 캐시로 가속합니다(14장).
  • 작은 파일 수백만 개 — 객체 하나하나마다 GET / PUT 비용이 있어 작은 파일 수백만 개 패턴은 비용이 의외로 큽니다. 묶어서(tar.gz, Parquet) 저장하거나 다른 저장소로 옮기는 것이 답입니다.
  • Lifecycle 없이 1년 — 로그나 임시 파일이 Standard에 그대로 남으면 몇 달 뒤 청구서가 폭증합니다. 라이프사이클은 버킷을 만든 첫날 같이 설정합니다.
  • Versioning 켜고 잊기 — Versioning을 켜고 lifecycle이 없으면 저장 비용이 무한히 증가합니다. 켰다면 라이프사이클로 오래된 비현재 버전을 정리합니다.
  • presigned URL의 만료가 너무 김 — 24시간짜리 presigned URL은 사실상 영구 접근입니다. 보통 5~15분, 길어야 1시간으로 둡니다.
  • s3:* 와일드카드 IAMAction: "s3:*" 정책은 위험합니다. 적어도 GetObject / PutObject / ListBucket 같이 명시합니다.

연습문제 #

  1. §“S3 권한의 평가 순서"의 다섯 층 중, 정적 호스팅 버킷을 일부러 공개하려면 어느 층을 어떻게 해제해야 하는지 적어 보세요. 그리고 14장 CloudFront의 OAC 패턴에서는 같은 버킷의 PAB를 왜 다시 켜는지 한 줄로 대비해 설명해 보세요.
  2. presigned PUT URL의 ExpiresIn을 600초로 둔 코드를 보고, 만료가 너무 길 때와 너무 짧을 때 각각 어떤 문제가 생기는지 §“presigned URL의 보안"을 근거로 적어 보세요.
  3. 로그 버킷 하나를 가정하고, 30일 후 Standard-IA, 90일 후 Glacier, 365일 후 삭제하는 Lifecycle 규칙을 직접 작성해 보세요. 이 규칙이 §“비용 폭탄"의 어떤 항목을 줄여 주는지 27장 비용 최적화와 연결해 메모해 두세요.

한 줄 요약: S3는 버킷(글로벌 유일 이름)과 객체(key)의 두 개념만 있는 무한 객체 스토리지이고 디렉터리는 가짜다. 보안은 PAB가 최상위이며 새 버킷은 PAB 네 개를 모두 켠다. 정적 호스팅은 HTTPS와 Edge가 없어 CloudFront와 묶고, presigned URL은 5~15분 임시 권한 위임이며, Versioning은 반드시 Lifecycle과 한 쌍으로 둔다.

다음 챕터 #

객체 영역은 잡았습니다. 다음 11장 RDS에서는 관계형 DB 차례로 넘어갑니다. RDS의 매니지드 모델, 자동 백업과 PITR, Multi-AZ, 파라미터 / 옵션 그룹, 그리고 마이너 vs 메이저 업그레이드를 어떻게 다룰지 정리합니다.

X