실습 예제 · 3
S3 정적 사이트 + CloudFront + OAC
이 실습의 결론은 하나입니다 — 버킷을 퍼블릭으로 열지 않고도 정적 사이트를 배포할 수 있습니다. 오리진 액세스 제어(Origin Access Control, OAC)를 쓰면 CloudFront 배포만 버킷을 읽을 수 있고 직접 접근은 차단됩니다.
학습 목표
- 퍼블릭 액세스가 차단된 버킷을 CloudFront 오리진으로 쓸 수 있습니다.
- 오리진 액세스 제어와 버킷 정책이 어떻게 짝을 이루는지 설명할 수 있습니다.
- 기본 루트 객체와 사용자 지정 오류 응답을 설정할 수 있습니다.
- 배포 후 캐시 무효화로 변경을 반영할 수 있습니다.
- 버킷에 직접 접근했을 때 차단되는지 검증할 수 있습니다.
시나리오
제품 소개 페이지를 정적 파일로 배포합니다. 요구는 이렇습니다.
- 전 세계 사용자에게 낮은 지연으로 제공해야 합니다.
- 오리진 버킷은 인터넷에 직접 노출되지 않아야 합니다.
- HTTPS로만 접근되어야 합니다.
- 배포 후 변경 사항이 빠르게 반영되어야 합니다.
S3의 정적 웹사이트 호스팅 기능만 쓰면 버킷을 퍼블릭으로 열어야 하고, HTTPS도 직접 제공되지 않습니다. 그래서 비공개 버킷 + CloudFront + OAC가 현재의 표준 조합입니다.
아키텍처 개요
| 구성 요소 | 역할 | 설정 요점 |
|---|---|---|
| S3 버킷 | 정적 파일의 오리진 | 퍼블릭 액세스 차단 유지, 정적 웹사이트 호스팅은 켜지 않는다 |
| 오리진 액세스 제어(OAC) | CloudFront가 서명된 요청으로 오리진에 접근 | 서명 프로토콜 sigv4, 서명 동작 always |
| 버킷 정책 | CloudFront 서비스 주체에게만 읽기 허용 | 조건으로 특정 배포 ARN만 허용 |
| CloudFront 배포 | 엣지 캐시와 HTTPS 종료 | 뷰어 프로토콜 정책 redirect-to-https, 기본 루트 객체 지정 |
구성 단계
1 · 비공개 버킷 만들기
BUCKET=example-lab03-site
REGION=ap-northeast-2
aws s3api create-bucket \
--bucket "$BUCKET" \
--region "$REGION" \
--create-bucket-configuration LocationConstraint="$REGION"
# 신규 버킷은 기본으로 차단되어 있지만, 명시적으로 확인하고 고정합니다.
aws s3api put-public-access-block \
--bucket "$BUCKET" \
--public-access-block-configuration \
BlockPublicAcls=true,IgnorePublicAcls=true,BlockPublicPolicy=true,RestrictPublicBuckets=true
aws s3api get-public-access-block --bucket "$BUCKET"
# 정적 파일을 올립니다.
aws s3 cp ./site/ "s3://$BUCKET/" --recursive
2 · 오리진 액세스 제어 만들기
aws cloudfront create-origin-access-control \
--origin-access-control-config '{
"Name": "lab03-site-oac",
"Description": "OAC for lab03 static site",
"OriginAccessControlOriginType": "s3",
"SigningProtocol": "sigv4",
"SigningBehavior": "always"
}' \
--query 'OriginAccessControl.Id' --output text
OriginAccessControlOriginType은
s3 · mediastore · mediapackagev2 · lambda 중
하나이고, 이 실습에서는 s3입니다.
서명 프로토콜은 sigv4 하나뿐입니다.
3 · 배포 만들기
{
"CallerReference": "lab03-site-2026-07-29",
"Comment": "lab03 static site",
"Enabled": true,
"DefaultRootObject": "index.html",
"Origins": {
"Quantity": 1,
"Items": [
{
"Id": "s3-origin",
"DomainName": "example-lab03-site.s3.ap-northeast-2.amazonaws.com",
"OriginAccessControlId": "E1EXAMPLEOACID",
"S3OriginConfig": { "OriginAccessIdentity": "" }
}
]
},
"DefaultCacheBehavior": {
"TargetOriginId": "s3-origin",
"ViewerProtocolPolicy": "redirect-to-https",
"AllowedMethods": {
"Quantity": 2,
"Items": ["GET", "HEAD"],
"CachedMethods": { "Quantity": 2, "Items": ["GET", "HEAD"] }
},
"CachePolicyId": "658327ea-f89d-4fab-a63d-7e88639e58f6",
"Compress": true
}
}
aws cloudfront create-distribution \
--distribution-config file://distribution.json \
--query 'Distribution.[Id,DomainName,Status]' \
--output table
4 · 버킷 정책으로 배포만 허용
OAC를 만들었다고 접근이 되는 것은 아닙니다. 버킷 정책이 CloudFront 서비스 주체를 허용해야 완성됩니다. 조건으로 배포 ARN을 못 박아 이 배포만 읽을 수 있게 합니다.
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": { "Service": "cloudfront.amazonaws.com" },
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::example-lab03-site/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E1EXAMPLEDIST"
}
}
}
]
}
aws s3api put-bucket-policy \
--bucket "$BUCKET" \
--policy file://bucket-policy.json
aws s3api get-bucket-policy --bucket "$BUCKET" --query 'Policy' --output text
5 · 배포 후 캐시 무효화
aws s3 sync ./site/ "s3://$BUCKET/" --delete
aws cloudfront create-invalidation \
--distribution-id E1EXAMPLEDIST \
--paths "/*" \
--query 'Invalidation.[Id,Status]' --output table
무효화는 편리하지만 남용하면 요금 항목이 됩니다.
프로덕션에서는 파일 이름에 내용 해시를 넣어 새 경로로 배포하고,
index.html처럼 이름이 고정된 소수의 파일만 무효화하는 방식이 일반적입니다.
검증 방법
| 확인 | 방법 | 기대 결과 |
|---|---|---|
| CloudFront 경유 접근 | 배포 도메인 이름으로 HTTPS 요청 | 정상 응답 (200) |
| 버킷 직접 접근 | S3 객체 URL로 직접 요청 | 거부됨 — 접근이 차단된다 |
| HTTP 접근 | 배포 도메인에 HTTP로 요청 | HTTPS로 리디렉션 |
| 기본 루트 객체 | 경로 없이 배포 도메인만 요청 | index.html이 반환된다 |
| 캐시 적중 | 같은 경로를 두 번 요청하고 응답 헤더 확인 | 두 번째 요청이 엣지 캐시에서 처리된다 |
# 배포가 전 엣지에 전파되었는지 상태를 확인합니다.
aws cloudfront get-distribution \
--id E1EXAMPLEDIST \
--query 'Distribution.[Status,DomainName]' --output table
# 버킷 정책과 퍼블릭 액세스 차단이 의도대로인지 확인합니다.
aws s3api get-public-access-block --bucket "$BUCKET"
aws s3api get-bucket-policy-status --bucket "$BUCKET"
get-bucket-policy-status가 버킷이 퍼블릭이 아니라고 보고하고,
동시에 CloudFront 도메인으로는 페이지가 열려야 이 실습이 성공한 것입니다.
비용 고려
| 항목 | 성격 |
|---|---|
| S3 저장 | 저장 용량과 요청 수 |
| CloudFront 전송 | 엣지에서 뷰어로 나가는 데이터와 요청 수 |
| 오리진 요청 | 캐시 미스로 오리진에 도달한 요청. 적중률이 오르면 줄어든다 |
| 무효화 | 일정 수를 넘는 경로 무효화가 항목이 된다. 남용하지 않는다 |
| 가격 등급 | 배포를 어느 지역 엣지까지 쓸지에 따라 달라진다 |
정적 사이트에서 비용을 좌우하는 것은 캐시 적중률입니다. 캐시 키에 불필요한 쿼리 문자열이나 헤더를 넣으면 같은 콘텐츠가 여러 항목으로 나뉘어 적중률이 떨어지고 오리진 요청이 늘어납니다. 구조적 배경은 케이스 6에 있습니다.
프로덕션 고려사항
- 사용자 지정 도메인과 인증서.
CloudFront에 붙일 ACM 인증서는
us-east-1에서 발급해야 합니다. 다른 리전에서 만든 인증서는 붙지 않습니다. - 단일 페이지 애플리케이션의 라우팅.
존재하지 않는 경로를
index.html로 돌려주려면 사용자 지정 오류 응답을 설정합니다. - 보안 헤더. 응답 헤더 정책으로 콘텐츠 보안 정책 등을 붙일 수 있습니다.
- 접근 로그. 기본이 꺼짐이므로 필요한 경우 켜고 저장 위치를 정합니다.
- 지리적 제한. 특정 국가만 허용하거나 차단해야 한다면 배포 수준에서 설정합니다.
- WAF 연동. 애플리케이션 계층 공격 방어가 필요하면 배포에 웹 ACL을 붙입니다.
- 버전 관리. 실수로 덮어쓴 파일을 되돌리려면 버킷 버전 관리를 켭니다. 대신 이전 버전 만료 규칙을 함께 둡니다(케이스 3).
정리 — 리소스 삭제
DIST_ID=E1EXAMPLEDIST
# 현재 구성과 ETag 를 받아 Enabled 를 false 로 바꿔 다시 올립니다.
aws cloudfront get-distribution-config --id "$DIST_ID" > dist.json
ETAG=$(python3 -c "import json;print(json.load(open('dist.json'))['ETag'])")
python3 - <<'PY'
import json
d = json.load(open('dist.json'))['DistributionConfig']
d['Enabled'] = False
json.dump(d, open('dist-disabled.json','w'))
PY
aws cloudfront update-distribution \
--id "$DIST_ID" \
--distribution-config file://dist-disabled.json \
--if-match "$ETAG"
# 상태가 Deployed 가 될 때까지 기다린 뒤 삭제합니다.
aws cloudfront wait distribution-deployed --id "$DIST_ID"
NEW_ETAG=$(aws cloudfront get-distribution-config --id "$DIST_ID" --query 'ETag' --output text)
aws cloudfront delete-distribution --id "$DIST_ID" --if-match "$NEW_ETAG"
# 오리진 액세스 제어와 버킷을 정리합니다.
aws s3 rm "s3://$BUCKET" --recursive
aws s3api delete-bucket --bucket "$BUCKET"
버킷 버전 관리를 켰다면 rm --recursive만으로는 비워지지 않습니다.
이전 버전과 삭제 마커까지 지워야 버킷을 삭제할 수 있습니다.
자주 하는 실수
| 실수 | 증상 | 고치는 법 |
|---|---|---|
| OAC만 만들고 버킷 정책을 안 붙임 | CloudFront 경유 요청이 접근 거부로 실패한다 | 서비스 주체 허용 정책을 적용한다 |
버킷 정책에 AWS:SourceArn 조건이 없음 |
다른 계정의 배포도 이 버킷을 오리진으로 쓸 수 있다 | 배포 ARN 조건을 추가한다 |
| 문제를 풀려고 퍼블릭 액세스 차단을 해제 | 실습의 목적이 사라지고 버킷이 노출된다 | 차단을 유지한 채 정책과 OAC를 점검한다 |
| 정적 웹사이트 호스팅 엔드포인트를 오리진으로 지정 | OAC 서명이 적용되지 않아 버킷을 퍼블릭으로 열어야 한다 | 버킷의 REST 엔드포인트를 오리진으로 쓴다 |
| 기본 루트 객체 미설정 | 도메인만 입력했을 때 오류가 난다 | DefaultRootObject를 지정한다 |
| 변경 후 무효화하지 않음 | 옛 파일이 계속 보인다 | 무효화하거나 파일 이름에 해시를 넣는 배포 방식으로 바꾼다 |
| 인증서를 배포 리전에서 발급 | CloudFront에 붙지 않는다 | us-east-1에서 발급한다 |
이어서 볼 곳
- 14장 · 엣지와 DNS CloudFront 캐시 동작과 오리진 액세스 제어, Route 53 라우팅 정책을 다룹니다.
- 8장 · S3와 오브젝트 스토리지 버킷 정책과 퍼블릭 액세스 차단, 신규 버킷의 보안 기본값을 다룹니다.
- 케이스 6 · 데이터 전송 요금이 예산을 넘겼다 캐시 적중률이 오리진 송신량과 비용을 어떻게 바꾸는지 다룹니다.
- 케이스 3 · S3 비용이 매달 늘어난다 버전 관리를 켰을 때 함께 넣어야 하는 수명 주기 규칙을 다룹니다.
- 실습 11 · KMS 고객 관리형 키로 암호화 오리진 객체를 고객 관리형 키로 암호화할 때의 권한 구성을 다룹니다.
공식 문서 출처
- Amazon CloudFront API 레퍼런스 — CreateOriginAccessControl
— 오리진 유형(
s3등), 서명 프로토콜sigv4, 서명 동작 값 - Amazon CloudFront API 레퍼런스 — CreateDistribution
— 배포 구성의 필수 요소와
DefaultRootObject, 뷰어 프로토콜 정책 - Amazon CloudFront API 레퍼런스 — CreateInvalidation — 무효화 경로 지정 방식
- Amazon S3 API 레퍼런스 — PutPublicAccessBlock — 네 가지 차단 설정의 의미
- Amazon S3 API 레퍼런스 — PutBucketPolicy — 버킷 정책 적용 방식
- Amazon S3 API 레퍼런스 — CreateBucket
— 퍼블릭 액세스 차단 기본 활성화와 ACL 비활성화(
BucketOwnerEnforced) 기본값 - Amazon CloudFront 개발자 안내서 — CloudFront란 — 엣지 캐시의 동작과 배포 상태의 의미