S3 Cloudfront로 Fe와 Be를 하나의 도메인에 연결하기
S3에 올린 프런트엔드와 별도의 백엔드를 CloudFront의 origin과 cache behavior로 묶어 하나의 도메인에서 서비스해보자.
개요
정적 프런트엔드를 S3에 올리고 CloudFront로 배포하는 구성은 흔하다. 그런데 API 서버는 ALB, API Gateway 또는 별도 웹 서버에서 동작한다면 주소가 둘로 나뉘기 쉽다.
1
2
프런트엔드: https://www.example.com
백엔드: https://api.example.com
이 구조도 문제는 없지만, 프런트엔드가 호출하는 API를 같은 도메인 아래에 두고 싶을 때가 있다.
1
2
프런트엔드: https://www.example.com/
백엔드: https://www.example.com/api/*
CloudFront distribution 하나에 origin을 두 개 등록하고 path별 cache behavior를 설정하면 이를 구현할 수 있다.
flowchart LR
U[Browser] -->|www.example.com| CF[CloudFront]
CF -->|기본 동작: *| S3[(Private S3 bucket)]
CF -->|/api/*| BE[ALB 또는 API server]
브라우저에서는 하나의 도메인만 보지만 CloudFront가 요청 path를 보고 실제 요청을 받을 origin을 고른다.
핵심은 origin과 cache behavior
CloudFront의 origin은 원본 콘텐츠가 있는 위치다. 이 구성에는 두 origin이 필요하다.
| Origin | 역할 | 예시 |
|---|---|---|
| S3 origin | HTML, JavaScript, CSS, 이미지 | my-frontend.s3.ap-northeast-2.amazonaws.com |
| Backend origin | 동적 API 요청 처리 | my-alb.ap-northeast-2.elb.amazonaws.com |
Cache behavior는 URL path와 origin을 연결하는 routing rule이다.
| 우선순위 | Path pattern | Origin | 용도 |
|---|---|---|---|
| 0 | /api/* | Backend | API 요청 |
| 마지막 | * | S3 | 그 외 모든 프런트엔드 요청 |
CloudFront는 cache behavior를 위에서부터 확인하고 처음 일치한 규칙을 사용한다. 어떤 추가 규칙에도 일치하지 않으면 항상 마지막에 있는 기본 behavior *가 적용된다.
따라서 /api/users는 Backend origin으로, /assets/app.js와 /는 S3 origin으로 전달된다.
나의 경우에는 별도의 이미지, 음성 등의 저장을 위해 /media 라는 추가 동작을 두었다. 
- api는 ALB로 라우팅되고 나머지는 S3의 디렉토리에 맞게 동작이 라우팅된다.
1. 프런트엔드를 S3에 업로드하기
React, Vue 등의 프로젝트를 build하면 정적 파일이 만들어진다.
1
2
npm run build
aws s3 sync dist/ s3://my-frontend-bucket --delete
프레임워크에 따라 build 결과 디렉터리가 build/, dist/ 등으로 다를 수 있다.
S3 bucket은 public으로 열지 않고 일반 S3 origin과 OAC(Origin Access Control)를 사용하는 것을 권장한다. OAC를 사용하면 CloudFront가 서명한 요청만 bucket에 접근하도록 제한할 수 있다.
여기서 S3의 Static website hosting endpoint와 일반 S3 bucket endpoint를 구분해야 한다.
- OAC를 쓸 때: 일반 S3 bucket origin 사용
- S3 website endpoint를 쓸 때: CloudFront에서는 custom origin으로 취급되며 OAC를 사용할 수 없음
이 글에서는 bucket을 private으로 유지하기 위해 일반 S3 origin과 OAC를 사용한다.
CloudFront distribution과 OAC를 만든 뒤 bucket policy에는 해당 distribution만 읽을 수 있도록 권한을 준다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "AllowCloudFrontServicePrincipalReadOnly",
"Effect": "Allow",
"Principal": {
"Service": "cloudfront.amazonaws.com"
},
"Action": "s3:GetObject",
"Resource": "arn:aws:s3:::my-frontend-bucket/*",
"Condition": {
"StringEquals": {
"AWS:SourceArn": "arn:aws:cloudfront::123456789012:distribution/E123EXAMPLE"
}
}
}
]
}
계정 ID, distribution ID, bucket 이름은 자신의 환경에 맞게 바꿔야 한다. S3의 Block Public Access는 켜둔다.
2. Backend origin 추가하기
CloudFront distribution의 Origins에서 백엔드 주소를 custom origin으로 추가한다.
예를 들어 ECS service가 ALB 뒤에 있다면 ALB의 DNS 이름을 origin domain으로 지정할 수 있다.
1
my-api-alb-123456.ap-northeast-2.elb.amazonaws.com
Origin protocol policy는 가능하면 HTTPS only로 설정한다. 그러면 브라우저와 CloudFront뿐 아니라 CloudFront와 백엔드 사이도 암호화된다.
백엔드가 외부에 공개된 ALB라면 사용자가 CloudFront를 거치지 않고 ALB 주소로 직접 접근할 수도 있다. 이것을 막아야 한다면 다음 방법을 검토할 수 있다.
- CloudFront가 origin 요청에 secret custom header를 추가하고 백엔드에서 확인
- ALB listener rule에서 해당 header가 없는 요청 거부
- 지원되는 구성이라면 CloudFront VPC origin 사용
- security group, WAF 등으로 접근 범위 제한
custom header 하나만으로 모든 보안 문제가 해결되는 것은 아니지만, origin 우회를 줄이는 한 가지 수단이 된다. secret 값은 프런트엔드 코드나 응답에 노출하면 안 된다.
3. /api/* behavior 만들기
이 부분이 실제 routing을 담당한다.
1
2
3
4
5
Path pattern: /api/*
Origin: Backend origin
Viewer protocol policy: Redirect HTTP to HTTPS
Allowed HTTP methods: GET, HEAD, OPTIONS, PUT, POST, PATCH, DELETE
Cache policy: CachingDisabled
API가 읽기 전용이 아니라면 POST, PUT, PATCH, DELETE도 전달하도록 Allowed HTTP methods를 열어야 한다. 기본값인 GET, HEAD만 허용하면 프런트엔드의 수정 요청이 CloudFront에서 막힌다.
로그인한 사용자마다 응답이 달라지는 일반적인 API는 먼저 캐시를 끄고 시작하는 편이 안전하다. 인증된 응답을 잘못 캐시하면 다른 사용자에게 같은 결과가 전달되는 심각한 문제가 생길 수 있다. 정말 캐시가 필요한 공개 GET API만 응답 특성과 cache key를 검토한 뒤 별도 behavior로 분리한다.
header, cookie, query string 전달
CloudFront는 viewer 요청의 모든 값을 자동으로 origin에 전달하지 않는다. 무엇을 백엔드까지 전달할지는 origin request policy로 정해야 한다.
예를 들어 다음 값이 필요할 수 있다.
Authorizationheader- session cookie
?page=2,?keyword=cloudfront같은 query string- CORS 또는 추적에 필요한 header
Cache policy와 origin request policy는 역할이 다르다.
- Cache policy: 어떤 값으로 cache key를 구분할지 결정
- Origin request policy: cache key와 별개로 origin에 어떤 값을 보낼지 결정
인증 API에서 CachingDisabled를 선택했더라도 백엔드가 필요한 header와 cookie, query string이 전달되는지는 따로 확인해야 한다. 무조건 모든 header를 전달하기보다는 애플리케이션이 실제로 쓰는 값을 allowlist로 관리하는 편이 이해하기 쉽다.
4. 기본 behavior는 S3로 보내기
기본 behavior *의 origin을 S3로 지정한다.
1
2
3
4
Path pattern: *
Origin: S3 origin
Viewer protocol policy: Redirect HTTP to HTTPS
Allowed HTTP methods: GET, HEAD, OPTIONS
정적 파일 이름에 content hash가 들어가는 경우 긴 cache 시간을 사용할 수 있다.
1
2
assets/app.a1b2c3.js
assets/style.d4e5f6.css
반면 index.html은 새 배포를 빠르게 반영할 수 있도록 짧게 캐시하거나 Cache-Control: no-cache를 적용하는 것이 좋다. 배포할 때 모든 파일을 매번 invalidation하기보다 파일 이름 versioning과 적절한 Cache-Control을 함께 쓰는 편이 효율적이다.
6. 도메인과 HTTPS 연결하기
CloudFront distribution은 기본적으로 다음과 같은 주소를 준다.
1
d111111abcdef8.cloudfront.net
내 도메인을 사용하려면 CloudFront에 alternate domain name을 추가하고 해당 이름을 포함하는 TLS certificate를 연결해야 한다.
1
2
Alternate domain name: www.example.com
Certificate SAN: www.example.com
CloudFront에서 사용할 ACM certificate는 us-east-1 region에 있어야 한다. Seoul region에서 서비스하더라도 CloudFront용 인증서는 Virginia 북부에서 발급한다는 점을 자주 놓친다.
가비아처럼 Route 53이 아닌 DNS 사업자를 사용한다면 보통 다음 CNAME record를 등록한다.
1
2
3
Type: CNAME
Host: www
Value: d111111abcdef8.cloudfront.net
인증서 DNS 검증을 위한 CNAME record도 별도로 등록해야 한다. CloudFront console 또는 ACM에 표시되는 host와 value를 그대로 DNS에 추가한다.
example.com처럼 host가 없는 apex(root) domain에는 DNS 표준상 CNAME을 둘 수 없다. 사용하는 DNS 사업자가 ALIAS/ANAME 또는 CNAME flattening을 지원하는지 확인하고, 지원하지 않는다면 www.example.com을 CloudFront에 연결한 뒤 root domain에서 www로 redirect하는 방법을 사용할 수 있다.
요청이 실제로 어떻게 흐르는가
설정을 마치면 같은 도메인이라도 path에 따라 origin이 달라진다.
sequenceDiagram
participant B as Browser
participant C as CloudFront
participant S as S3
participant A as API Server
B->>C: GET /assets/app.js
C->>S: GET /assets/app.js
S-->>C: JavaScript
C-->>B: JavaScript
B->>C: GET /api/users?page=1
C->>A: GET /api/users?page=1
A-->>C: JSON
C-->>B: JSON
프런트엔드는 상대 경로로 API를 호출할 수 있다.
1
2
3
4
5
const response = await fetch('/api/users?page=1', {
headers: {
Authorization: `Bearer ${token}`,
},
});
브라우저 기준으로 프런트엔드와 API의 scheme, host, port가 같기 때문에 별도 subdomain을 사용할 때보다 CORS 구성이 단순해진다.
설정 확인하기
distribution 배포와 DNS 반영이 끝나면 정적 파일과 API를 각각 확인한다.
1
2
3
curl -I https://www.example.com/
curl -I https://www.example.com/assets/app.js
curl -i https://www.example.com/api/health
응답 header의 x-cache와 via를 보면 CloudFront를 거쳤는지 판단하는 데 도움이 된다.
API method와 값 전달도 확인한다.
1
2
3
4
5
6
curl -i -X POST https://www.example.com/api/items \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer test-token' \
-d '{"name":"sample"}'
curl -i 'https://www.example.com/api/items?page=2'
최소한 다음 항목은 배포 전후로 확인하는 것이 좋다.
/api/*가 S3가 아니라 백엔드로 전달되는가?POST,PATCH,DELETE,OPTIONS가 필요한 경우 정상 동작하는가?Authorization, cookie, query string이 백엔드에 도착하는가?- API 응답이 의도하지 않게 cache되지 않는가?
- 존재하지 않는 API가 HTML
index.html이 아니라 API 404를 반환하는가? - S3 object URL로 직접 접근했을 때 public으로 노출되지 않는가?
- HTTP 요청이 HTTPS로 redirect되는가?
주의할 점
/api와 /api/*는 따로 생각하기
/api/*는 /api/users 같은 하위 path를 위한 규칙이다. /api 자체 요청도 사용한다면 실제 matching 결과를 확인하고 필요하면 /api behavior를 별도로 추가하거나 애플리케이션에서 trailing slash 규칙을 통일한다.
CloudFront는 /api prefix를 자동으로 제거하지 않는다
/api/users가 Backend origin으로 routing되면 기본적으로 백엔드도 /api/users path를 받는다. 백엔드가 /users만 기대한다면 CloudFront Function이나 Lambda@Edge 등으로 URI를 rewrite하거나, 백엔드 route 자체를 /api prefix에 맞춰야 한다.
behavior 순서는 보안 설정이기도 하다
여러 path pattern이 겹치면 먼저 일치한 behavior가 선택된다. 더 구체적인 규칙을 위에 두고, 각 behavior의 인증·signed URL·허용 method 정책이 의도한 대로 적용되는지 확인해야 한다.
장애 지점을 origin별로 구분하기
같은 도메인을 사용하더라도 프런트엔드와 API의 origin은 다르다. 장애가 났을 때는 CloudFront 전체 문제로만 보지 말고 어느 behavior와 origin에서 실패했는지 나눠 확인한다. CloudFront access log, ALB access log, S3 access log와 응답의 x-cache header가 도움이 된다.
정리
CloudFront는 단순히 S3 파일을 빠르게 내려주는 CDN으로만 사용할 필요가 없다. 여러 origin과 cache behavior를 조합하면 하나의 도메인에서 정적 프런트엔드와 동적 백엔드를 함께 제공하는 routing layer가 된다.
핵심 설정은 다음과 같다.
- 기본
*behavior는 private S3 origin으로 보낸다. /api/*behavior는 Backend origin으로 보낸다.- S3는 OAC로 CloudFront에서만 접근하게 한다.
- 동적·인증 API는 우선 cache를 끄고 필요한 header, cookie, query string을 명시적으로 전달한다.
- SPA fallback이 API 오류 응답까지 바꾸지 않게 분리한다.
- CloudFront용 certificate와 DNS record로 사용자 도메인을 연결한다.
이 구조를 사용하면 프런트엔드는 /api/... 상대 경로로 백엔드를 호출할 수 있고, 브라우저에 노출되는 주소도 하나로 단순해진다. 대신 CloudFront가 routing, cache, header 전달에 관여하게 되므로 behavior별 설정을 애플리케이션의 일부로 보고 관리해야 한다.

