AWS S3 + CloudFront + Route 53을 이용한 정적 웹 사이트 배포

시소·2023년 10월 17일
5

Next.js Static Exports

목록 보기
2/3
post-thumbnail

들어가며

이전 포스트 - Next.js로 정적 웹 사이트 개발하고 빌드하기에서 Next.js 를 통해 정적 웹 사이트를 개발하고 빌드하는 방법에 대해 소개하였다.
이후, 빌드를 수행하여 만들어 낸 번들을 AWS의 클라우드 서비스를 이용해 실제 사용자에게 제공하려면 어떻게 해야 하는지에 대한 내용을 담아볼 예정이다. 이를 위해 AWS의 S3, CloudFront, Route53 서비스를 활용할 것이다.

S3에서 정적 웹 사이트를 서비스 하기 위한 몇 가지 방법이 존재하는데, 세부 내용은 AWS re:Post 지식 센터 - CloudFront를 사용하여 Amazon S3에 호스팅되는 정적 웹 사이트를 서비스하려면 어떻게 해야 합니까?에 안내되어 있다.
배포를 진행하면서 각 방식마다 상이한 세부 설정 방법으로 인해 원활하게 진행되지 않았던 부분이 있어, 해당 내용에 관한 내용을 정리해보고 공유하기 위한 목적으로 글을 작성하였다.

⛓️ 구성도

참고용으로 간략한 구성도를 그려 보았다. 간단하게 설명을 덧붙이자면 다음과 같다.

  1. 사용자가 브라우저에게 https://example.com 접속을 요청한다.
  2. 요청은 AWS의 Route 53(DNS 서비스)을 통해 CloudFront(CDN 서비스) 배포로 라우팅된다.
  3. CloudFront에 의해 사용자는 S3 버킷(오리진 서버)에서 캐싱된 콘텐츠를 빠른 속도로 제공받을 수 있게 된다.
    3-1. CloudFront 오리진이 S3 REST API Endpoint로 구성된 경우, 별도 CloudFront Function 구성 필요 X
    3-2. CloudFront 오리진이 S3 Website Endpoint로 구성된 경우, CloudFront Function 추가 구성 필요(파일명이 포함되지 않은 요청에 index.html 추가하는 함수)

🚧 중요: S3 버킷의 퍼블릭 액세스 제한하기

참고로 S3 버킷에는 퍼블릭 액세스를 차단할 수 있는 권한 옵션이 존재한다.
만약 버킷의 퍼블릭 액세스가 허용된 상태에서 S3 버킷에 배포하고자 하는 사이트 번들을 업로드한다면, 모든 사용자가 S3의 엔드포인트를 통해 직접 접근이 가능해지게 된다.
우리는 사용자들에게 엣지 로케이션의 이점을 활용하여 더 빠른 속도로 사이트를 제공하고자 하므로, 사용자가 S3가 아닌 CloudFront 배포를 통해서만 접근할 수 있도록 해야 한다.
따라서, 사용자가 S3 버킷의 엔드포인트에 직접 접근이 불가하도록 버킷의 퍼블릭 액세스를 제한하는 것이 필요하다.

✌️ 2가지 방식 소개

사이트를 CloudFront 배포와 연결하면서 S3 버킷의 퍼블릭 액세스를 제한하기 위해선, 아래와 같은 두 방식을 취할 수 있다.

  1. REST API 엔드포인트를 오리진으로 사용: 오리진 액세스 제어 (OAC) 또는 오리진 액세스 ID (OAI) 로 액세스를 제한
  2. 웹 사이트 엔드포인트를 오리진으로 사용: Referer 헤더를 사용하여 액세스를 제한

지금부터 두 방식을 모두 이용한 예제를 소개한다.


방식 1️⃣: S3 REST API 엔드포인트 방식 + CloudFront + CloudFront Function 이용

  • 우선 S3 버킷의 퍼블릭 액세스를 차단하고, CloudFront 배포 구성 시 OAC(Origin Access Control) 정책을 추가하여 CloudFront를 통해서만 S3 오리진에 접근할 수 있도록 하는 방식
  • 다만 배포 후에 페이지에서 브라우저 새로고침 동작 시, 4xx 에러가 발생하는 문제가 있어 이를 해결하기 위해 CloudFront Function 구성 필요

1) S3 버킷 구성

버킷 생성

[ Amazon S3 > 버킷 > 버킷 만들기 ]
가장 먼저 버킷을 생성하는 것부터 시작한다. 이름과 리전만 설정해 주고 다른 옵션은 기본 값으로 두었다.
추가로, "이 버킷의 퍼블릭 액세스 차단 설정" 옵션에서 모든 퍼블릭 액세스 차단 체크박스가 체크 상태로 되어 있는지 확인해준다.

웹 사이트 파일 업로드

[ Amazon S3 > 버킷 > 버킷 선택 > 객체 업로드 ]
빌드 결과로 생성되었던 워크스페이스의 out/ 디렉토리 내의 모든 파일을 빠짐 없이 업로드 해준다.
(수동으로 빌드 번들을 업로드 하는 대신에 이 과정을 자동화하여 업로드할 수도 있는데, 해당 방법에 대해선 다음 시리즈에서 다룰 예정이다.)

정적 웹 사이트 호스팅 옵션

[ Amazon S3 > 버킷 > 버킷 선택 > 속성 ]
해당 방식에서는 정적 웹 사이트 호스팅 옵션을 활성화 할 필요가 없다. 따라서 옵션이 비활성화됨 으로 표시되는지 확인한다.

2) CloudFront 배포 구성

[ CloudFront > 배포 생성 ]

해당 화면에서 구성 가능한 주요 설정은 다음과 같다.

  • 원본 도메인: 드롭다운을 눌러 앞서 생성한 S3 버킷 선택
  • 원본 액세스: 원본 액세스 제어 설정(권장) -> 제어 설정 생성 버튼 클릭 후 팝업에서 생성
  • 뷰어 프로토콜 정책: Redirect HTTP to HTTPS
  • 웹 애플리케이션 방화벽(WAF): 보안 보호 비활성화 (상황에 따라, 필요한 경우 활성화)
  • 사용자 정의 SSL 인증서: 기존에 발급했던 ACM 인증서 선택
  • 기본값 루트 객체: index.html 입력


3) S3 버킷 권한 수정

[ CloudFront > 배포 > 배포 선택 ]
배포를 생성하고 나면, 화면 상단에 아래 이미지와 같은 메시지가 노출된다.
메시지 우측에 정책 복사 버튼을 누른 뒤, [ 버킷 선택 > 권한 > 정책 ] 영역으로 이동하여 편집 버튼을 눌러 방금 복사한 정책을 붙여 넣고 저장해 준다.

// 예시
{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "AllowCloudFrontServicePrincipal",
            "Effect": "Allow",
            "Principal": {
                "Service": "cloudfront.amazonaws.com"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::<S3 bucket name>/*",
            "Condition": {
                "StringEquals": {
                  "AWS:SourceArn": "arn:aws:cloudfront::<AWS account ID>:distribution/<CloudFront distribution ID>"
                }
            }
        }
    ]
}

4) 배포된 내용 확인 (💥문제 발생)

[ CloudFront > 배포 > 배포 선택 > 일반 ]
세부 정보 영역을 보면 URL 형태로 된 배포 도메인 이름이 생성된 것을 볼 수 있다. 브라우저를 열어 해당 도메인 경로로 이동하면, 우리의 웹 사이트가 제대로 보여지는 것을 확인할 수 있다. 라우트 이동도 정상적으로 동작하고 있(는줄 알았)다.

그런데 아래와 같이 새로고침을 하였더니 문제가 발생한다.

확인해 보니, 문제는 다음과 같은 경로에서 일어나는 걸로 정리되었다.

  • domain.cloudfront.net/ 에서 새로고침: 정상 작동
  • domain.cloudfront.net/some-path/ 에서 새로고침하거나 해당 경로 직접 접근: 문제 발생, 페이지 불러올 수 X
  • domain.cloudfront.net/some-path/index.html 경로 직접 접근: 정상 작동

문제가 발생하는 원인과 해결하기 위한 방법을 찾아보았는데, Stack overflow - Next.js: How to make links work with exported sites when hosted on AWS Cloudfront? 답변에서 힌트를 얻을 수 있었다.

  • "기본 구성의 많은 HTML 서버가 URL 뒤에 / 문자가 표시되면 일치하는 디렉토리 내부에서 index.html을 제공함"
  • "S3도 Website Endpoint 역할을 하도록 설정된 경우에 해당 작업을 수행함"

이로 미루어 보았을 때, 현재 S3 버킷을 Website Endpoint로 사용하고 있지 않아서 발생하는 현상임을 알게되었다. 따라서 이 문제를 고치기 위해 Amazon CloudFront - 파일 이름이 포함되지 않는 요청 URL에 index.html 추가를 수행할 수 있다.

5) CloudFront Function 작성

CloudFront Function에 대한 소개는 이 링크에서 확인 가능하다.

[ CloudFront > 함수 > 함수 생성 ]

function handler(event) {
    var request = event.request;
    var uri = request.uri;

    // URI가 /로 끝나면 요청 경로 끝에 index.html 추가
    if (uri.endsWith('/')) {
        request.uri += 'index.html';
    }
    // URI가 /로 끝나지 않고 .을 포함하지 않으면 요청 경로에 /index.html 추가
    else if (!uri.includes('.')) {
        request.uri += '/index.html';
    }

    return request;
}

작성 후에는 "게시" 탭을 눌러 함수 게시 버튼을 클릭해야만 배포와 함수를 연결할 수 있게 된다.

6) CloudFront 배포에 Function 연결 (✅문제 해결)

[ CloudFront > 배포 > 배포 선택 > 동작 ]
기존에 연결되어 있던 동작을 편집할 것이다. 동작 편집 화면의 하단에 있는 "함수 연결" 영역에서 뷰어 요청에 방금 만든 함수를 연결시켜 준다.

설정 저장 후 배포가 완료될 때까지 잠시간 기다려 준다. 경험 상 수 분 정도 기다리면 완료되었다.
이제 다시 배포된 도메인에 접근하여 여러 라우트로 이동하고 새로고침을 마음껏 눌러도 페이지가 정상적으로 표시되는 걸 확인할 수 있다. 👍


방식 2️⃣: S3 웹사이트 엔드포인트 방식 + CloudFront 이용

  • 해당 방식은 앞서 소개한 방식과는 다르게 별도의 CloudFront Function 구성 필요 X
  • 대신 퍼블릭 액세스를 허용한 S3 버킷의 버킷 정책으로써 웹 사이트 요청에 특정 Referer 헤더가 있을 때만 액세스 할 수 있도록 하는 옵션을 추가하도록 하는 구성 필요

1) S3 버킷 구성

버킷 생성

[ Amazon S3 > 버킷 > 버킷 만들기 ]
S3 버킷 생성은 첫 번째로 소개한 방식에서와 동일하게 생성해 준다. 지금 단계에서는 아직 퍼블릭 액세스를 허용하지 않아도 된다.
생성이 완료되면 이전에 했던 것처럼 빌드 파일 및 폴더 객체를 모두 업로드 해준다.

정적 웹 사이트 호스팅 옵션

[ Amazon S3 > 버킷 > 버킷 선택 > 속성 ]
정적 웹 사이트 호스팅 옵션을 활성화 하도록 설정을 편집한다. 편집 후 저장하게 되면 아래와 같이 엔드포인트가 생성된다.

2) CloudFront 배포 구성

[ CloudFront > 배포 > 배포 생성 ]

  • 원본 도메인: 드롭다운을 눌러 앞서 생성한 S3 버킷 선택 후, 하단의 웹 사이트 엔드포인트 사용 버튼 클릭
    (도메인 형태: <버킷명>.s3-website.<region>.com)
  • (중요)사용자 정의 헤더 추가: 헤더 이름 - Referer, 값 - 본인만 알 수 있는 값 설정
  • 뷰어 프로토콜 정책: Redirect HTTP to HTTPS
  • 웹 애플리케이션 방화벽(WAF): 보안 보호 비활성화 (상황에 따라, 필요한 경우 활성화)
  • 사용자 정의 SSL 인증서: 기존에 발급했던 ACM 인증서 선택
  • 기본값 루트 객체: index.html 입력

3) S3 버킷 권한 설정 추가

[ Amazon S3 > 버킷 > 버킷 선택 > 권한 ]

버킷 정책

CloudFront 배포의 오리진으로 S3 웹 사이트 엔드포인트를 사용하고 있습니다. 403 액세스 거부 오류가 발생하는 이유가 무엇인가요? 페이지에 안내된 내용에 따라, 아래와 같이 버킷 정책을 수정해 준다.

{
  "Version":"2012-10-17",
  "Id":"http referer policy example",
  "Statement":[
    {
      "Sid":"Allow get requests originating from my CloudFront with referer header",
      "Effect":"Allow",
      "Principal":"*",
      "Action":"s3:GetObject",
      "Resource":"arn:aws:s3:::<S3 bucket name>/*",
      "Condition":{
        "StringLike":{"aws:Referer":"<MY_SECRET_TOKEN_CONFIGURED_ON_CLOUDFRONT_ORIGIN_CUSTOM_HEADER>"}
      }
    }
  ]
}

퍼블릭 액세스 차단 해제

마지막으로, 모든 퍼블릭 액세스 차단 설정을 비활성화 해준다.
버킷이 퍼블릭 액세스가 가능하다고 표시되지만, 방금 버킷 정책에서 '특정 referer header가 전달된 경우에만 버킷에 접근 가능하도록 설정'하였기 때문에, 버킷의 웹 사이트 엔드포인트로 직접 접근하더라도 403 Forbidden만 보여질 뿐이다.

4) 배포된 내용 확인

[ CloudFront > 배포 > 배포 선택 > 일반 ]
이제 이전에 얻은 CloudFront 배포 도메인으로 접근해 보면 사이트가 정상적으로 보여짐을 확인할 수 있다.


이후 CloudFront 배포와 Route 53 연결

  • 배포와 Route 53을 연결하는 과정은 CloudFront를 Website Endpoint로 배포했든 REST API Endpoint로 배포했든 구성 방법이 동일
  • 설명에서 사용하는 도메인은 기존에 만들어 두었던 도메인을 활용함

[ Route 53 > 호스팅 영역 > 레코드 생성 ]
레코드 이름, 레코드 유형, 트래픽 라우팅 대상을 아래와 같이 설정해 준다. 이 과정을 마치면 이제 정적 웹 사이트를 자신이 원하는 주소를 통해 제공할 수 있게 된다.


마치며

사실 배포 과정이 간단할 것이라고 생각했었는데, 예상과 다르게 난관에 부딪히게 되어 시간을 많이 잡아먹은 기억이 있다.
처음에는 Next.js로 정적 웹 사이트를 배포할 때 CloudFront + CloudFront Function 조합이 무조건적인 해결 방법인 줄 알았는데, 이번 포스팅을 작성하면서 여러 자료를 찾아보니 CloudFront Function을 이용하지 않고도 해결할 수 있다는 사실을 알게 되었다. 이로써 어떤 일이든 한 가지 방식으로만 해결하려고, 그 방식에만 매달리지 않아야 함을 느꼈다. 문제 해결에 있어 다양한 해결책을 보유하고 있으면 처한 상황에 따라 더 적절한 방식을 선택할 수 있으므로 여러모로 의미가 있는것 같다!
다음 포스트에는 Next.js 정적 웹 사이트 CI/CD 자동화 과정을 담아볼 예정이다.


참고한 링크

profile
배우고 익힌 것을 나만의 언어로 정리하는 공간 ..🛝

0개의 댓글