현제의 현재이야기

[DevOps] 험난한 리액트 S3 + CloudFront 배포기 본문

Infra

[DevOps] 험난한 리액트 S3 + CloudFront 배포기

현재의 현제 2023. 8. 11. 10:59

AWS 작업을 내가 맡고 있었는데 프론트 배포 + CICD를 구축해보려고 하였다.

온갖 오류를 만났기 때문에 기억을 잃지 않기 위해 기록을 하려고 한다. 

S3 버킷

  • 버킷 이름은 S3만 배포할 때를 대비하여 실제 도메인 이름을 적어준다.
  • 그리고 ACL을 활성화하고 객체 라이터로 변경한다. AWS CLI에 접속하기 위해서이다.
  • 모든 퍼블릭 엑세스 차단을 해제한다.
  • 그리고 버킷 정책을 편집한다. 정책 생성기에서 S3 선택, principle * 입력, GetObject 액션을 선택, ARN 뒤에 /*를 입력한다.
  • 그리고 정적 웹 사이트 호스트 편집에 들어가 활성화를 눌러준다.
  • 그리고 인덱스 및 오류 문서를 index.html로 설정한다.

CloudFront

  • 배포 생성 클릭
  • 원본 도메인에 S3 버킷을 클릭하고 원본(어쩌구 물어본다)을 클릭하여 s3-website가 나오는 주소로 설정해준다.
  • 뷰어 프로토콜을 Redirect HTTP to HTTPS를 클릭하여 https로 트래픽을 전달한다.
  • 캐시 정책은 recommand로
  • 설정이 이제 중요한데 우선 SSL 인증서는 버지니아에 생성하도록 하자.
  • 그리고 도메인 이름이 엄청 중요했는데 꼭 실제 사용할 도메인을 적자. 안 그러면 Route53에서 CloudFront를 찾지 못하더라.
  • 가격 분류는 북미, 유럽, 아시아, 중동 및 아프리카를 사용하자.

Route53

  • 여기서 미리 도메인을 호스팅 해놨으니깐, 전에 로드밸런스를 등록한 것 처럼 A레코드를 등록하고 별칭 on, CloudFront에서 아까 만든 것을 클릭하여 생성
  • 그리고 조금 기다려야 연결이된다. (그거를 못 참고 잘못 설정하였나 하여 삭제를 두번이나 했다. 느긋해지자고?)

버킷에 파일 올리기

  • 모든 래퍼런스가 npm을 기준으로 설명이 되어있었다. yarn으로는 어떻게할까?
  • yarn을 설치하고, yarn build를 하면 dist파일이 생긴다. 이것을 s3에 전부 넣어주면 된다.

그리고 오류 잡기

역시 한번에 끝나면 그건 이상한거다. 문제점이 있었는데 처음 index.html은 잘 떴지만, 그 후에 /booking/이라던가 이런 주소르 들어가면 403, 404에러가 떴었다.

 Failed to load resource: the server https://qrtaxi.co.kr/call/assets/index-7af88cee.js responded with a status of 404 ()

잘 보아하니 index~ 저놈을 찾지 못하는 것 같았다. 그런데 call을 빼고서는 실제로 파일이 있는 것을 확인했다.

그렇다면 뭐가 문제지? 하고 폭풍 검색을 시전했는데 문제는 우리 프론트 팀이 해쉬 라우터를 사용하지 않고 createBrowserRouter를 사용한 것이다.

 S3는 정적 웹 호스팅 서비스로, 싱글 페이지 애플리케이션(SPA)의 라우팅을 기본적으로 지원하지 않습니다.
 때문에, SPA에서 Browser Router (히스토리 API를 사용하는 라우터)를 사용할 경우 문제가 발생할 수 있습니다

S3는 정적웹사이트를 호스팅하는 것인데, 라우터는 동적으로 라우팅을 하기 때문에 S3입장에서는 어디로 가야될 지 몰라서 404에러를 띄우는 것이었다.

찾아보니 해시라우팅으로 바꾸는 것이 가장 간단하고 나왔었다. 그런데 얼마나 많은 사람들이 리액트로 S3 배포를 하는데, 과거의 유산인 HashRouter를 쓴다는 것인가? 이해가 가지 않아서 분명 방법이 있겠거니 열심히 찾아봤다.

방법은 바로 404 error 발생시 index.html로 리다이렉트 되게 하는 것이었다. 이렇게 되면 모든 라우팅이 index.html로 처리되며, 이곳에서 해당 경로에 맞는 컴포넌트를 찾아 렌더링을 하는 것이었다.

자세히 보니 dist에 이런 것이 있었다. 이놈 덕분에 index.html에서 다른 곳으로 잘 찾아갈 수 있는 것이었다!

 <script type="module" crossorigin src="/assets/index-7af88cee.js"></script>

그래서 CloudFront에서 배포 -> 오류페이지로 가서 404에러와 403에러를 200 Ok로 응답코드를 정의해주었고, 응답 페이지 경로도 /index.html로 바꾸었다.

그랬더니 이런 에러가 발생했다.

TypeError: 'text/html' is not a valid JavaScript MIME type.

이 에러는 웹브라우저가 JavaScript 파일을 로드하려고 했는데 이상한게 응답이 와서 실제로 반환된 응답이 text/html MIME 타입의 HTML 문서라서 오류를 뱉은 것이다. 왜 그런고 싶어서 페이지 소스 분석을 하고, stack overflow를 열심히 뒤지다가

<title>큐택 QRTaxi</title>
    <script type="module" crossorigin src="./assets/index-7af88cee.js"></script>
  </head>
  <body>
    <div id="root"></div>

dist에 있는 index.html에서 저 script의 src가 "./"가 되면 안되고 "/"로 상대경로로 고쳐줘야된다는 것이다!

그래서 저거 바꾸고 다시 S3에 업로드했더니 다른 오류가 발생했다.

Mixed Content: The page at 'https://qrtaxi.co.kr/booking/31/' was loaded over HTTPS,
but requested an insecure XMLHttpRequest endpoint 'http://api.qrtaxi.co.kr/call/main/31'.
This request has been blocked; the content must be served over HTTPS.

이 오류는 웹 페이지가 HTTPS를 사용하여 로드되었지만, 그 안에서 HTTP를 사용하여 리소스나 API를 요청하는 경우 발생하는 "Mixed Content" 오류라고 한다. 분명히 근데 https로 모든 base url을 쓰고 있는데 알수 없는 에러였다.. 그래서 

<meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests">

를 추가했더니 CORS에러가 나서 백 서버에서 잘 처리하였다.

그랬더니 문제 없이 잘 홈페이지가 돌아갔다! 감격

그리고 후에 build를 해도 index.html에서 경로와 Mixed Content 문제를 해결하기 위해 파일들을 수정했다.

#/index.html

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="Content-Security-Policy" content="upgrade-insecure-requests"> 
    <!-- <link rel="icon" type="image/svg+xml" href="/vite.svg" /> -->
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <link rel="icon" type="image/svg+xml" href="/icons/QT_initial_v1.svg" />
    <title>큐택 QRTaxi</title>
  </head>
  <body>
    <div id="root"></div>
    <script type="module" src="/src/main.tsx"></script>
  </body>
</html>

#/vite.config.ts
...

export default defineConfig({
  server: {
    port: 3000,
  },
  base: '/', <- 이부분
  plugins: [
    react(),
    tsconfigPaths(),
    
...

캐시 무효화 및 Github Action 생성

근데 S3에 파일을 올려도 CloudFront측에서 변경된 사항을 제때 갱신을 못해주었다. 이유는 캐싱 때문인데 성능을 위해서 CloudFront는 S3의 파일들을 캐싱하여(대략 24시간 동안 유지) 성능 향상을 꾀한다는 것이다. 이 캐싱을 무효화 시켜주는게 CloudFront에 무효화라는 것이 있었다. 여기에서 "/*" 처럼 와일드카드로 모든 디렉토리를 캐시 무효화 시켜줄 수 있고, "/index.html"처럼 특정 경로도 가능하였다.

 

그런데 나는 이거 한번 설정해두면 다음부터 저절로 무효화가 되는 줄 알았는데.. 알고보니깐 한번 바꿀 때 마다 바꾸는 거더라고?

그래서 어차피 main merge가 되면 그 때 파일이 변환되는거니깐 github action 배포 yml에 aws cli를 통해서 무효화를 넣어주기로 하였다.

name: Deploy to S3

on:
  push:
    branches:
      - main

jobs:
  deploy:
    runs-on: ubuntu-latest

    steps:
    - name: Checkout repository
      uses: actions/checkout@v2

    - name: Install dependencies
      run: yarn install --frozen-lockfile

    - name: Build the project
      run: yarn build

    - name: Configure AWS credentials
      uses: aws-actions/configure-aws-credentials@v1
      with:
        aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
        aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
        aws-region: ap-northeast-2

    - name: Deploy to S3
      run: |
        aws s3 sync dist/ s3://${{ secrets.S3_BUCKET_NAME }} --delete
    
    - name: CloudFront invalidation
      run: |
        aws cloudfront create-invalidation --distribution-id ${{ secrets.CLOUDFRONT_DISTRIBUTION_ID }} --paths "/*"

이것이 내가 짠 flow이다. 백보다 훨씬 간단했다.

yarn깔고, 빌드하고, aws cli 접속하고 dist의 모든 파일들을 버킷에 넣어준다.

그리고나서 마지막으로 캐시 무효화를 하여서 배포가 되면 바로 적용이 되게 workflow를 만들었다.

음~ 뿌듯하군. 다음에 플젝을 하게 된다면 프론트도 CICD 및 인프라 구축해두고 개발하면 훨씬 좋을 것 같다는 생각이 들었다. 야미~ 

'Infra' 카테고리의 다른 글

[Infra]우여곡절 https 붙이기  (1) 2023.10.08
[DevOps] 쿠버네티스 관련  (0) 2023.09.20
[DevOps] 도커에 Celery 및 서버 https 추가  (0) 2023.08.08
[DevOps] django channel 연결기  (0) 2023.07.30
[DevOps] static 파일 처리기  (0) 2023.07.30
Comments