CloudFront Free 플랜으로 비공개 S3 블로그 공개하기
S3 비공개 버킷과 OAC, CloudFront Function으로 Jekyll의 디렉터리 주소를 처리했다. 403·404와 Free 플랜 비용 범위까지 정리한다.
지난 글에서 글은 집에서 만들고 공개는 AWS에 맡긴다고 적었다. 이번에는 Jekyll 결과물을 비공개 S3 버킷에서 제공하기까지의 설정과, 실제로 막혔던 /about/ 문제를 정리한다.
방문자 → CloudFront (blog.ecojk.dev, HTTPS) → S3 버킷 (비공개)
1. S3 버킷을 비공개로 두기
S3 웹사이트 엔드포인트를 공개하는 대신 일반 S3 원본과 Origin Access Control(OAC)을 사용했다. 버킷의 퍼블릭 액세스 차단 네 항목을 켜고, 버킷 정책에서는 내 CloudFront 배포에만 GetObject를 허용한다. 버킷 주소로 직접 파일을 받아 가는 경로를 닫기 위해서다.
사이트 버킷에는 버전 관리를 켜고 이전 버전을 30일 뒤 정리한다. 저장 시 암호화는 SSE-S3다. 버전 관리는 배포 실수를 되돌리는 데 도움이 되지만, 장기 백업의 대체재는 아니다.
2. CloudFront에서 확인한 설정
| 항목 | 설정 |
|---|---|
| 요금제 | 정액제 Free 플랜 |
| 원본 접근 | 비공개 S3 + OAC |
| 공개 주소 | blog.ecojk.dev |
| 인증서 | ACM us-east-1에서 발급한 도메인 인증서 |
| HTTP | HTTPS로 리다이렉트 |
| 캐시 | CachingOptimized, 배포 시 무효화 |
| 응답 헤더 | 관리형 SecurityHeadersPolicy |
| 기본 루트 객체 | index.html |
CloudFront에 연결할 ACM 인증서는 미국 동부 버지니아(us-east-1) 리전에 있어야 한다. Free 플랜은 배포를 만들거나 기존 배포를 변경할 때 명시적으로 선택한다. 기본값을 짐작해 넘어가지 말고 배포의 요금제 표시를 확인하는 편이 안전하다.
3. /about/에서 403이 나온 이유
Jekyll은 /about/ 페이지를 보통 about/index.html로 만든다. 하지만 CloudFront의 기본 루트 객체는 사이트 맨 앞 /에만 적용된다. 비공개 S3 원본은 /about/을 자동으로 about/index.html로 바꾸지 않는다. 요청한 객체를 찾지 못했고, 원본의 권한 조건에 따라 403이 돌아왔다.
뷰어 요청 단계에 다음 CloudFront Function을 연결해 주소를 정규화했다. 아래는 운영 코드의 핵심 부분이다.
function handler(event) {
var request = event.request;
var uri = request.uri;
if (uri.endsWith('/')) {
request.uri = uri + 'index.html';
return request;
}
var last = uri.split('/').pop();
if (last.indexOf('.') === -1) {
return {
statusCode: 301,
statusDescription: 'Moved Permanently',
headers: {
location: { value: 'https://blog.ecojk.dev' + uri + '/' }
}
};
}
return request;
}
/about은 /about/으로 옮기고, /about/은 원본에 보낼 때 /about/index.html로 바꾼다. 운영 코드에는 글 주소가 바뀌었을 때 쓸 개별 301 목록도 있다. 이 단순한 규칙은 확장자 없는 경로를 모두 디렉터리 주소로 간주하므로, 그런 이름의 실제 파일이 있다면 별도 예외가 필요하다.
4. 없는 페이지는 404로 보여 주기
비공개 S3에서 없는 객체는 권한 구성에 따라 403으로 보일 수 있다. CloudFront의 사용자 지정 오류 응답에서 원본의 403·404를 /404.html과 HTTP 404로 바꾸고, 오류 캐시는 60초로 설정했다.
이 방법은 단순하지만 권한 설정 실수로 생긴 403도 방문자에게 404로 보일 수 있다. 그래서 배포 뒤 홈·글·없는 주소를 각각 점검하고, 문제가 생기면 CloudFront와 S3의 원본 권한도 함께 확인해야 한다.
5. 주소와 비용의 경계
공개 주소는 blog.ecojk.dev 하나만 사용한다. 루트 도메인과 www를 영구 301로 연결하지 않은 이유는 나중에 다른 용도로 쓸 여지를 남기기 위해서다.
2026년 9월 기준 Free 플랜의 기본 사용량은 월 100만 요청, 100GB 전송이다. 한도를 넘는다고 CloudFront 초과 요금이 붙지는 않지만, 지속적으로 크게 넘으면 전달 성능이 조정될 수 있다. Free 플랜에는 상세 접속 로그가 포함되지 않는다. Route 53 호스팅 영역 비용은 해당 영역을 플랜에 연결했을 때 포함 범위에 들어가며, S3 요청과 별도 기능 등은 청구 내역을 확인해야 한다. 따라서 ‘CloudFront 플랜 $0’과 ‘AWS 전체 비용 $0’은 같은 말이 아니다.
배포 후에는 주요 URL의 200·301·404, HSTS와 nosniff 헤더를 확인한다. x-amz-cf-pop이 ICN으로 시작하면 그 요청은 서울 엣지에서 처리됐다는 뜻이다.