AWS S3 + CloudFront로 CDN 구축 시 CORS 강제 설정
데이터를 제공하는 파일 서버를 구축해야 하는 경우 AWS의 S3와 CloudFront 서비스를 이용하면 손쉽게 구축할 수 있다. 특히 이미지의 경우 이 조합이 자주 사용된다.
이 조합으로 Webapp을 만들 경우 CORS관련 문제를 겪을 수 있다. 일반적인 경우는 브라우저의 기본 기능을 통해 파일을 직접 다운로드를 하거나 DOM을 이용해 (<img> 테그나 CSS) 이미지를 표시하기 때문에 이 조합이 문제를 일으키지 않는다. 하지만 XHR을 통해 데이터를 받아서 사용하는 특수한 경우에는 Webapp이 제공되는 도메인과 CloudFront 서비스의 도메인이 다르기 때문에 CORS 관련 문제를 겪을 수 있다. 물론 AWS S3에서 CORS 관련 설정을 제공하나, 브라우저와 CloudFront의 캐쉬기능과 엮이면서 재대로 동작하지 않는 경우가 빈번했다.
Unity3d를 이용해서 Facebook Canvas 웹게임을 만들 때 이 문제가 발생했다. Unity3d의 기본 HTTP Client인 UnityWebRequest를 이용해서 데이터를 받아오도록 짜면 Facebook Canvas 빌드의 경우 XHR을 사용하도록 컴파일이 된다. 이렇게 만들어진 게임은 Facebook을 통해 서비스가 되는데, 받아와야 하는 데이터는 AWS S3에 있기 때문에 CORS 문제를 피할 수 없게된다. Unity3d쪽은 가능하면 수정하고 싶지 않아 서버쪽에서 CORS 지원하도록 수정하였다
크롬 브라우저 캐시로 인한 CORS 에러
첫번째 경우는 크롬 브라우저의 캐시 정책으로 발생한 문제였다. 광고 이미지를 Facebook Page에 게시물로 올리고, Canvas 게임 안에서도 배너로 표시하였다. 유저가 Facebook Page에 접근해서 이미지를 본 후 링크를 눌러 게임에 진입했을 경우에, Unity 게임 안에서 동일한 이미지를 XHR로 받으려고 하면 CORS 에러가 발생했다. 에러 발생과정은 아래와 같다.:
- 유저가 Facebook Page에서 이미지를 본다. 이미지는 img 태그로 표시되기 때문에 HTTP 통신을 보면 이미지 요청과 응답에는 CORS 관련 헤더가 없다. 크롬 브라우저는 이 요청과 응답을 캐시함.
- Unity가 XHR로 같은 이미지를 요청함. 이 때 요청에는 CORS 관련 헤더를 포함시켰음.
- 크롬 브라우저의 네트워크 담당 부분에서 1에서 캐시한 응답결과를 리턴함. 자바스크립트쪽에서는 응답된 결과에 CORS 관련 헤더가 없기 때문에 에러가 발생함.
2의 요청과 1의 요청은 헤더가 다르고 결과도 다른 경우인데, 크롬 브라우저가 같은 결과가 나올 것을 기대하고 캐시를 써버린게 원인이었다. 매우 마이너한 경우이고, 브라우저의 동작을 명확히 규정하는 표준도 없기 때문에 발생한 경우였다.
해결책은 크롬 브라우저가 2의 요청에 1에서 캐시한 결과를 사용하지 않도록 Vary header로 힌트를 주는 것이다. 이를 위해 CloudFront에 Lambda Edge를 추가해서 응답 결과에 아래와 같이 Vary header를 추가했다. (관련 Stackoverflow 답변과 해결책 원문)
'use strict';
// If the response lacks a Vary: header, fix it in a CloudFront Origin Response trigger.
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
if (!headers['vary'])
{
headers['vary'] = [
{ key: 'Vary', value: 'Access-Control-Request-Headers' },
{ key: 'Vary', value: 'Access-Control-Request-Method' },
{ key: 'Vary', value: 'Origin' },
];
}
callback(null, response);
};
CloudFront Origin별 캐시 오동작
두번째 경우는 CloudFront가 CORS 요청에 비 CORS 응답을 캐시해서 발생했다. S3는 당연하게도 CORS 관련 헤더가 요청에 있을 경우에만 응답에 CORS 관련 헤더를 추가해준다. 따라서 CloudFront도 단순 URL 뿐만 아니라 HTTP 헤더에 따라 다른 결과를 캐시해야하고, 이를 위해 Origin 헤더에 따라 다른 결과를 캐시하는 기능을 제공한다. 아쉽게도 이 기능은 처음에는 잘 동작하는 듯 했는데, 한 시간 정도 지나면 CORS 요청에 비 CORS 응답이 캐시되는 경우가 빈번히 발생했다. 설정 전의 캐시결과가 살아 남았을까도 의심해보았지만, 설정 시간 후에 생성된 데이터에 대해서도 이 경우가 발생하는 것으로 보아, AWS 내부 문제라고 결론 내렸다.
프로필 이미지나 정적 데이터를 제공할 뿐인 단순한 경우라서 CloudFront의 모든 응답에 CORS관련 헤더를 강제로 추가하는 것으로 해결했다.
'use strict';
// If the response lacks a Vary: header, fix it in a CloudFront Origin Response trigger.
// If the response lacks CORS header, ...
exports.handler = (event, context, callback) => {
const response = event.Records[0].cf.response;
const headers = response.headers;
if (!headers['vary'])
{
headers['vary'] = [
{ key: 'Vary', value: 'Access-Control-Request-Headers' },
{ key: 'Vary', value: 'Access-Control-Request-Method' },
{ key: 'Vary', value: 'Origin' },
];
}
if (!headers['access-control-allow-origin'])
{
headers['access-control-allow-origin'] = [
{ key: 'Access-Control-Allow-Origin', value: '*' }
];
}
if (!headers['access-control-allow-methods'])
{
headers['access-control-allow-methods'] = [
{ key: 'Access-Control-Allow-Methods', value: 'GET' },
{ key: 'Access-Control-Allow-Methods', value: 'HEAD' }
];
}
if (!headers['access-control-expose-headers'])
{
headers['access-control-expose-headers'] = [
{ key: 'Access-Control-Expose-Headers', value: 'ETag' },
{ key: 'Access-Control-Expose-Headers', value: 'Last-Modified' }
];
}
callback(null, response);
};
Edit 2021.11.16.
이런 목적에 더 맞는 CloudFront Function 이 추가되었다. Lambda@Edge 대신에 CloudFront Function 을 쓰면된다. 공식 홈페이지에도 관련 문서가 있다.