학습 목표

시나리오

제품 소개 페이지를 정적 파일로 배포합니다. 요구는 이렇습니다.

S3의 정적 웹사이트 호스팅 기능만 쓰면 버킷을 퍼블릭으로 열어야 하고, HTTPS도 직접 제공되지 않습니다. 그래서 비공개 버킷 + CloudFront + OAC가 현재의 표준 조합입니다.

아키텍처 개요

구성 요소와 역할
구성 요소 역할 설정 요점
S3 버킷 정적 파일의 오리진 퍼블릭 액세스 차단 유지, 정적 웹사이트 호스팅은 켜지 않는다
오리진 액세스 제어(OAC) CloudFront가 서명된 요청으로 오리진에 접근 서명 프로토콜 sigv4, 서명 동작 always
버킷 정책 CloudFront 서비스 주체에게만 읽기 허용 조건으로 특정 배포 ARN만 허용
CloudFront 배포 엣지 캐시와 HTTPS 종료 뷰어 프로토콜 정책 redirect-to-https, 기본 루트 객체 지정

구성 단계

1 · 비공개 버킷 만들기

create-bucket.sh — 버킷 생성과 퍼블릭 액세스 차단 확인 (버킷 이름과 리전은 예시입니다)
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 · 오리진 액세스 제어 만들기

create-oac.sh — S3 오리진용 OAC 생성
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 · 배포 만들기

distribution.json — 배포 구성 (버킷 이름과 OAC ID는 앞 단계의 값으로 바꿉니다)
{
  "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
  }
}
create-distribution.sh — 배포 생성과 도메인 이름 확인
aws cloudfront create-distribution \
  --distribution-config file://distribution.json \
  --query 'Distribution.[Id,DomainName,Status]' \
  --output table

4 · 버킷 정책으로 배포만 허용

OAC를 만들었다고 접근이 되는 것은 아닙니다. 버킷 정책이 CloudFront 서비스 주체를 허용해야 완성됩니다. 조건으로 배포 ARN을 못 박아 이 배포만 읽을 수 있게 합니다.

bucket-policy.json — CloudFront 서비스 주체에게만 읽기 허용 (계정 ID와 배포 ID는 예시입니다)
{
  "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"
        }
      }
    }
  ]
}
apply-policy.sh — 정책 적용
aws s3api put-bucket-policy \
  --bucket "$BUCKET" \
  --policy file://bucket-policy.json

aws s3api get-bucket-policy --bucket "$BUCKET" --query 'Policy' --output text

5 · 배포 후 캐시 무효화

deploy-and-invalidate.sh — 파일 갱신과 무효화 (배포 ID는 예시입니다)
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이 반환된다
캐시 적중 같은 경로를 두 번 요청하고 응답 헤더 확인 두 번째 요청이 엣지 캐시에서 처리된다
verify.sh — 배포 상태와 접근 차단 확인
# 배포가 전 엣지에 전파되었는지 상태를 확인합니다.
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에 있습니다.

프로덕션 고려사항

정리 — 리소스 삭제

cleanup.sh — 배포 비활성화 후 삭제, 버킷 정리
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에서 발급한다

이어서 볼 곳

공식 문서 출처