아마존 AWS로 이전

VULTR와 KeyCDN을 통해 호스팅하던 것을 최근에 AWS로 옮기면서 몇 가지 기록을 남겨 놓는다. nginx 서버와 에셋을 별도의 CDN으로 배포하던 설정들을 통합해 옮겨갈 수 있는지 테스트하느라 시간을 많이 보냈다.

1. S3 (Simple Storage Service)

오브젝트 스토리지이지만 정적 웹 사이트의 형태로 서비스할 수 있다. 웹 인터페이스로는 SSL 연결을 할 수 없으므로 CloudFront에서 HTTPS 연결을 처리한다.

S3 버킷 전체가 공개적으로 접근할 수 있도록 버킷 정책에 아래와 같은 규칙을 추가해야 한다.

{
  "Version":"2012-10-17",
  "Statement":[
    {
      "Sid":"PublicRead",
      "Effect":"Allow",
      "Principal": "*",
      "Action":["s3:GetObject"],
      "Resource":["arn:aws:s3:::BUCKET-NAME/*"]
    }
  ]
}

S3 버킷을 도메인에 연결해 호스팅하려면 버킷 이름에 따라 DNS에 CNAME을 추가하면 된다. 버킷 이름이 www.example.com이라면 example.com의 DNS에 www CNAME 레코드가 S3 버킷을 가리키도록 추가한다. S3 버킷의 웹사이트 엔드포인트는 Properties(Static website hosting)에서 확인할 수 있다.

에러 페이지를 지정할 수 있지만 에러 코드별로 다른 페이지를 지정할 수 없다.

2. CloudFront

AWS에서 SSL 인증서를 무료로 발급받을 수 있으므로 SSL 연결을 지원하기 쉽다. HTTPS를 지원할 필요가 없고 전 세계 엣지(edge)로 배포되는 CDN이 필요 없다면 S3 버킷을 호스팅하는 것만으로 충분하다.

CloudFront가 가장 앞단에서 캐쉬하기 때문에 비용은 대부분 여기서 발생한다. CNAME 레코드로 쓸 주소를 지정할 수 있으므로 어떤 주소라도 연결할 수 있다.

예외적으로, 루트 도메인(예, example.com)에 연결하려면 Route 53을 사용하여 별칭(alias) 레코드를 생성해야 한다. (또는 별칭 레코드를 지원하는 DNS 서비스를 사용할 것)

오리진(원본) 서버의 설정에서 기본적으로 {BUCKET-NAME}.s3.amazonaws.com 형태의 S3 버킷이 열거되어 선택할 수 있지만, 이것은 REST API 엔드포인트이기 때문에 웹 서버와는 다르게 작동하는 부분이 있다. S3 버킷의 속성에서 확인한 웹사이트 엔드포인트 주소를 직접 입력하면 웹 서버와 같이 작동한다. 웹사이트 엔드포인트 주소는 아래와 같은 형식이다.

http://{BUCKET-NAME}.s3-website.{REGION}.amazonaws.com

REST API 엔드포인트는 오브젝트 스토리지의 역활로 사용되기를 기대하기 때문에, 디렉터리로 끝나는 주소(https://www.euler.kr/search/) 에 접근할 때 인덱스 문서(index.html)을 찾지 않고, (Default Root Object를 지정하면 최상위 디렉터리에 대해서는 작용한다.) 오류가 발생하면 HTML 오류 페이지가 아닌 오류의 내용을 담은 XML을 받게 된다.

Cache Behavior에서 HTTP 연결을 HTTPS로 리다이렉트 시키게 설정할 수 있고, 알려진 주요 리소스 타입에 대해서 압축을 지원한다. (Compress Objects Automatically 옵션) HTML, CSS, JavaScript 등의 파일들이 요청한 웹 브라우저에서 지원하는 포맷(gzip)으로 압축하여 제공하기 때문에 트래픽 비용을 크게 줄일 수 있다.

HTTP 에러 코드에 따라서 대응 페이지를 각각 지정할 수 있다.

3. Route 53

도메인 등록기관에서 DNS를 제공하지만, DNS를 Route 53으로 옮기면 AWS 콘솔에서 설정을 관리할 수 있어서 편리하다.

하나의 도메인에 호스트 존을 만들면 월 0.5달러의 비용이 발생한다(DNS를 사용하기 위해 하나 이상의 호스트 존이 필요하다). Free Tier에서 무료로 제공하는 사용량은 없다.

아마존은 도메인 등록기관이기 때문에 도메인을 Route 53으로 옮기는 것도 좋은 방법이다. 대체로 프로모션이 작용하지 않을 때 GoDaddy보다 저렴하다. 호스트 존 하나를 만들면 연 6달러의 비용이 발생하지만, 연락처 보호 기능이 무료로 제공되므로 얻는 이득이 더 크다. (현재 .kr 도메인은 이전 불가) 일반 최상위 도메인(gTLD)의 .com, .net, .org는 Amazon Registrar로 등록되고 그 외의 도메인은 아마존의 프랑스 협력사 Gandi를 통해 등록된다.

DNS를 옮길 때 (특히 이메일 서비스와 연동된 도메인이라면) MX 레코드 등을 정확하게 옮겨야 한다.

4. 리다이렉션

AWS로 옮기면서 기본 도메인을 www.euler.kr로 바꾸었고 기존의 euler.kr에서는 리다이렉션하도록 하였다. HTTP, HTTPS 모두 www가 없는 도메인(apex domain)으로 연결하면 https://www.euler.kr로 리다이렉션한다.

euler.kr에서 리다이렉션시키기 위하여 S3 버킷과 그것을 배포하는 CloudFront 배포(distribution)를 한 벌 더 만들어야 한다. CloudFront 배포를 하지 않으면 S3 버킷만으로 HTTPS 연결을 지원할 수 없다. HTTPS 연결을 리다이렉션하기 위해 euler.kr의 SSL 인증서도 필요하다.

S3 버킷은 비어있지만 요청을 https://www.euler.kr 로 리다이렉션시키도록 설정하였다. (이 버킷은 버킷 정책을 설정하지 않아도 된다.) S3의 설정을 바꾸면서 테스트할 때는 curl 툴로 응답 헤더에서 캐쉬 상태를 확인하는 것이 중요하다. 필요하면 캐쉬 무효화(invalidation)를 하거나 캐쉬 히트가 생기지 않도록 URL을 바꾸면서 테스트해야 한다. (CloudFront의 기본 캐쉬 TTL은 24시간이다.)

Route 53 호스트 존에서 www.euler.kr과 euler.kr이 각각의 CloudFront 배포로 연결되도록 A와 AAAA(IPv6) 별칭 레코드를 설정하였다.

5. Lambda@Edge를 통해 응답 헤더 추가

CloudFront는 응답 헤더에 필드 추가를 지원하지 않기 때문에 몇 가지 보안 설정 필드 추가하기 위해 Lambda@Edge를 사용하였다. Lambda@Edge는 AWS Lambda 함수를 엣지 로케이션 서버에서 실행하기 때문에 활용해볼 여지가 많이 있고 그 중의 하나가 응답 헤더에 필드를 추가하거나 수정하는 것이다. 아래와 같은 함수를 만들어 CloudFront의 origin-response 이벤트에 트리거되도록 설정하였다.

'use strict';

exports.handler = (event, context, callback) => {
    const response = event.Records[0].cf.response;
    const headers = response.headers;

    headers['strict-transport-security'] = [
        {
            key: 'Strict-Transport-Security',
            value: 'max-age=63072000'
        }
    ];

    callback(null, response);
};

캐시된 객체의 응답에도 트리거되는 viewer-response보다 오리진 서버로부터 받은 응답과 함께 캐시되는 origin-response 이벤트에서 트리거되도록 해 람다 함수의 호출을 크게 줄이고, 결과적으로 반응 시간도 향상하였다.