AWS S3 + CloudFront + Route53 + GitHub Action을 이용한 React App 배포 과정

늘보·2023년 12월 28일
0

개요

React로 만들어진 B2B 서비스를 AWS + GitHub을 사용하도록 이전하는 과정에 대해 작성한 글입니다.
기존의 B2B 서비스는 CRA로 만들어 Azure App Service + Azure Devops를 사용해 배포 되었습니다. 그러나 Azure는 한국어 자료가 적어 커스텀에 필요한 정보를 얻기 쉽지 않고, 사용자가 더 많고 정보를 얻기 쉬운 AWS + GitHub을 사용해서 배포하려고 했습니다. 또한, B2C 서비스 및 어드민도 모두 AWS + GitHub을 통해 배포하고 있었기 때문에, 한 방향으로 통합해 관리에 필요한 리소스를 줄이자는 생각이 있었습니다.

배포 과정을 기록하고, 같은 작업을 해야할 때 기억하기 위해 작성한 글입니다. 프론트 앱을 배포하시는 분들께도 도움이 되었으면 좋겠습니다..!

이전에 사용한 서비스 및 기술은 다음과 같습니다.

  • AWS S3 (빌드된 정적 웹사이트 배포용)
  • CloudFront (CDN)
  • Github Action (CI / CD)
  • Route 53 (도메인 구매 및 등록)

아래는 그 과정을 나타냈습니다.

과정

1. 프론트엔드 코드를 Github로 이동

프론트엔드 코드를 Azure Devops에서 clone하여, GitHub에서 새로 만든 레포지토리에 연결했습니다.

2. 옮긴 프론트엔드 코드를 AWS Amplify로 배포 시도

B2C 프론트엔드 및 어드민이 Amplify로 배포중이었기 때문에, 옮긴 B2B 프론트엔드 코드를 AWS Amplify를 통해 배포를 시도했습니다. 그러나, 빌드가 실패했습니다.

그 이유는 현재 AWS Amplify에서 프로비저닝 시, Node.js 18버전을 지원하지 않고 있기 때문입니다.

B2B 서비스의 패키지들의 의존성이 Node.js 18버전에 있기 때문에, Node.js 버전을 낮추는 방법보다는 다른 배포 방법을 찾기로 했습니다.

아래는 AWS Amplify에서 각 Node 버전을 설치하는 Docker 코드인데, 18버전이 빠져있는걸 확인할 수 있는 코드입니다. (Node.js 18이 LTS인데 왜 지원을 하지 않지?)

# - AWS Amplify 프로비저닝 시 실행되는 Docker 코드
# - Node.js 18 버전 관련한 설치가 안 보인다.
## Install AWS Amplify CLI for all node versions
RUN /bin/bash -c ". ~/.nvm/nvm.sh && nvm use ${VERSION_NODE_8} && \
    npm config set user 0 && npm config set unsafe-perm true && \
	npm install -g @aws-amplify/cli@${VERSION_AMPLIFY}"
RUN /bin/bash -c ". ~/.nvm/nvm.sh && nvm use ${VERSION_NODE_10} && \
    npm config set user 0 && npm config set unsafe-perm true && \
	npm install -g @aws-amplify/cli@${VERSION_AMPLIFY}"
RUN /bin/bash -c ". ~/.nvm/nvm.sh && nvm use ${VERSION_NODE_12} && \
    npm config set user 0 && npm config set unsafe-perm true && \
	npm install -g @aws-amplify/cli@${VERSION_AMPLIFY}"
RUN /bin/bash -c ". ~/.nvm/nvm.sh && nvm use ${VERSION_NODE_14} && \
    npm config set user 0 && npm config set unsafe-perm true && \
	npm install -g @aws-amplify/cli@${VERSION_AMPLIFY}"
RUN /bin/bash -c ". ~/.nvm/nvm.sh && nvm use ${VERSION_NODE_16} && \
    npm config set user 0 && npm config set unsafe-perm true && \
	npm install -g @aws-amplify/cli@${VERSION_AMPLIFY}"
RUN /bin/bash -c ". ~/.nvm/nvm.sh && nvm use ${VERSION_NODE_17}  && \
    npm config set user 0 && npm config set unsafe-perm true && \
	npm install -g @aws-amplify/cli@${VERSION_AMPLIFY}"

3. AWS S3 + CloudFront를 통한 배포 시도

따라서, 좀 더 편리하게 배포가 가능한 AWS Amplify 대신에 AWS S3 + CloudFront를 통한 배포를 시도했습니다.

AWS S3 버킷에 프론트엔드 앱을 담고, CDN을 CloudFront로 설정해주는 방식입니다. 자세한 과정은 다음과 같아요.

CDN: 지리적 제약 없이 전 세계 사용자에게 빠르게 콘텐츠를 전송하는 기술입니다. 원리 자체는 매우 간단하여, 프록시 서버에서 출발한 웹 캐시의 클라우드화입니다. 전 세계 각지에 캐시 서버를 엄청 많이 설치합니다. 그리고 한국에 있는 사용자가 접속하면 한국 캐시 서버가 정보를 보냅니다. 프랑스에 있는 사용자가 접속하면 프랑스 캐시 서버에서 정보를 보냅니다. 이런 식으로 CDN 자체가 알아서 사용자와 가장 가까운 캐시 서버에서 정보를 찾아 보내는 것입니다.

1. AWS S3 버킷 생성

아래는 대략적인 생성 과정에 대해 작성했습니다.

  • 버킷 이름은 my-aws-bucket이라고 했습니다.
  • 리전은 ap-northeast-2 (아시아-서울)로 지정했습니다.
  • 현재 버킷의 엑세스 권한을 내 계정에서만 주기 위해 ACL을 비활성화했습니다.
  • 모든 퍼블릭 액세스는 차단했습니다. CloudFront를 통해서만 S3 버킷에 접근할 수 있도록 허용할 것이기 때문입니다.
  • 기타 설정은 기본 설정을 그대로 사용했습니다.
    - 태그는 결제 대시보드에서 비용을 추적할 수 있는 이름을 부여할 수 있어요. 정확히 어떤 장점이 있고 단점이 있는지 파악하지 못했기 때문에 따로 건드리지 않았습니다.
    - 암호화 또한 정확히 어떤 역할을 하는지 모르기 때문에 기본 설정을 그대로 유지했어요.

2. CloudFront 배포 생성

  • 원본 도메인 선택은, 기존의 생성했던 S3 버킷의 도메인을 선택했습니다.
  • 이후, 이름을 정하고 원본 액세스를 ‘Legacy access identities’를 선택했습니다.
  • 그리고, 원본 액세스 ID 또한 만들었던 S3 bucket을 선택했습니다. (새 OAI 생성 X)
  • 그리고, CloudFront 배포를 생성하며 자동으로 버킷 정책이 업데이트 될 수 있도록 “예, 버킷 정책 업데이트”를 선택합니다.

Q. OAI?
A. OAI(Origin Access Identitiy)란, CloudFront가 S3에 저장된 Private 객체에 액세스할 수 있도록 하는 특별한 식별자입니다.

  • 기본 캐시 동작은 아래와 같이 설정했습니다.
  • 다른 것은 기본 설정으로 두고, 뷰어 프로토콜 정책은 Redirect HTTP to HTTPS로 설정했습니다. 배포 환경에서는 HTTPS만 사용하기 때문입니다.
  • 이 말은, 사용자가 http://abcd12345.cloudfront.net/로 접속하더라도 https://abcd12345.cloundfront.net로 설정되는 것을 의미합니다.

3. 다시 AWS S3로 이동해서 권한 - 버킷 정책 확인하기

CloudFront 배포를 생성했다면, 다시 S3로 이동해서 권한 → 버킷 정책을 확인하면 아래와 같은 버킷 정책이 생성된 것을 확인할 수 있습니다.

{
    "Version": "2008-10-17",
    "Id": "PolicyForCloudFrontPrivateContent",
    "Statement": [
        {
            "Sid": "1",
            "Effect": "Allow",
            "Principal": {
                "AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ###########"
            },
            "Action": "s3:GetObject",
            "Resource": "arn:aws:s3:::########/*"        
				}
    ]
}

여기서, 아래 코드의 의미는 CloudFront OAI가 ‘#######’(가려놓은 것입니다!)라면, 이 버킷에 접근할 수 있다는 것을 의미합니다.

"Principal": {
	"AWS": "arn:aws:iam::cloudfront:user/CloudFront Origin Access Identity ###########"
},

4. CloudFront 배포 오류 페이지 → 사용자 정의 오류 응답 생성

  • 이제 만들었던 CloudFront의 배포 도메인을 복사해서 브라우저에 입력하면, 여전히 403 Forbidden 에러가 발생합니다. 만들었던 서비스가 React로 만든 Single Page Application이기 때문에 오류 페이지에서 추가 설정을 해야 합니다.
  • SPA는 하나의 **index.html**만 갖고, JavaScrtipt로 동적 라우팅이 이루어지기 때문에 S3에서 페이지를 찾지 못할 수 있습니다. (이 과정에서 403에러가 발생합니다!)
  • 따라서, S3 객체에서 403 Forbidden 에러가 발생했을 때, index.html로 redirect를 시켜줘야 합니다. 그럼 아래와 같이 설정할 수 있습니다.

5. 배포된 웹사이트 재확인

그런데, 여기까지 진행하더라도 아직은 웹사이트가 보이지 않을 거예요. 아직 S3 버킷에 빌드된 결과물이 업로드되지 않았기 때문입니다.

빌드된 결과물이 develop branch에 PR merge 또는 push될 때마다 자동으로 S3로 올라갈 수 있도록, GitHub Action을 통해 구성해야 합니다.

4. GitHub Action을 통한 배포 자동화

배포할 때마다 S3 버킷에 빌드된 폴더를 올리고, CloudFront에 적용된 캐시를 직접 무효화 시키는 건 비효율적인 작업이기 때문에, GitHub Action을 통한 CI/CD를 구축했습니다.

Q. CloudFront에 적용된 캐시?
A. CloudFront를 통해 한국 또는 한국 근처 어딘가에 있는 캐시 서버로부터 아까 업로드한 S3 객체를 가져올 수 있어요. 그럼, 해당 캐시 서버 또한 오리진 서버(S3)로부터 정보를 받아와서 캐싱해두고 있어요. 그런데, 배포가 되는 순간 즉시 새로운 내용이 반영이 되어야겠죠? 그래서, 배포가 될 때는 기존의 캐시 서버(CloudFront를 통해 접근하는)의 캐싱된 예전 S3 객체를 무효화하고, 새로 배포된 내용을 가져와야 해요.

1. .github/workflows 폴더 생성

먼저, 프로젝트의 루트에 .github이라는 폴더를 생성하고, 그 안에 workflows라는 폴더를 추가로 생성합니다.

그 안에, dev-ci.yml 이라는 파일을 생성합니다.

2. dev-ci 파일 작성

dev-ci 파일에 아래와 같은 코드를 작성합니다.

name: DEV CI

on:
	# develop 브랜치에 push가 될 때마다 이 action이 실행됩니다.
  push:
    branches:
      - develop
    tags:
      - 'development-**'

jobs:
  Deploy:
    runs-on: ubuntu-latest

    steps:
			# 1 - 소스코드 복사
      - name: Checkout source code
        uses: actions/checkout@v3
			
			# 2 - node_modules 캐싱
      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}-
			
			# 3 - 의존성 패키지 설치
      - name: Install Dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: yarn install
			
			# 4 - 빌드
      - name: Build
        run: yarn build
			
			# 5 - AWS 인증
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

			# 6 - AWS S3에 빌드 결과물 배포
      ## CRA로 생성한 React App은 build 폴더에 빌드 결과물이 들어있습니다. (dist 폴더가 아님!)
      - name: Deploy to S3
        run: aws s3 sync ./build s3://${{ secrets.AWS_BUCKET_NAME }} --delete

			# 7 - CloudFront 캐시 무효화
      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }} --paths "/*"

각 코드의 상세 내용은 다음과 같습니다.

1. 깃헙 레포지토리 체크아웃

워크플로우가 액세스할 수 있도록 깃헙 레포지토리에 액세스 할 수 있도록 현재의 레포지토리를 체크아웃 합니다. 즉, CRA로 만든 React App의 코드를 그대로 복제하는 것입니다!

# 1 - 소스코드 복사
- name: Checkout source code
	uses: actions/checkout@v3

2. 의존성 패키지 캐싱

매번 워크플로우가 실행 될 때마다 패키지를 재설치하게 되면 앱이 의존성이 많아질수록 빌드&배포 시간이 늘납니다.

따라서 의존성 캐싱 작업을 통해 의존성 변경이 없으면 이전에 설치했던 패키지를 재사용하도록 하여 빌드&배포 시간을 줄일 수 있습니다.

이 의존성 캐싱을 통해, 의존성에 변경이 없을 경우 약 1분 정도 배포에 걸리는 시간을 단축했습니다.

# 2 - node_modules 캐싱
- name: Cache node modules
	
	# **uses** 키워드는 GitHub Actions 마켓플레이스에서 제공하는 액션을 사용하겠다는 것을 나타냅니다. 
	# 여기서는 **actions/cache@v3**라는 액션을 사용하며, 이는 파일이나 디렉토리를 캐싱하는 데 사용되고 있습니다.
	uses: actions/checkout@v3
	
	# **with** 키워드는 사용할 액션에 전달할 **입력값**들을 지정합니다.
	with:

		# **path** 키워드는 캐싱할 파일 또는 디렉토리의 경로를 지정합니다.
		# 여기서는 **node_modules** 디렉토리가 캐시 대상임을 나타냅니다.
		path: node_modules
		
		# **key**는 캐시의 고유 식별자를 설정합니다. 이 식별자는 캐시를 생성하고 검색하는 데 사용됩니다.
		# **{{ runner.OS }}**는 실행 중인 러너의 운영 체제를 나타냅니다.
		# **hashFiles('**/yarn.lock')**는 **yarn.lock** 파일의 해시값을 계산하여, 
		# 의존성이 변경될 때마다 새로운 캐시를 생성하도록 합니다.
		key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}

		# **restore-keys**는 캐시 키가 정확히 일치하지 않을 경우 사용할 대체 키를 제공합니다. 
		# 이는 가장 최근에 일치하는 캐시를 검색하는 데 사용됩니다.
		# **{{ runner.OS }}-build-**와 **{{ runner.OS }}-**는 일치하는 최신 캐시를 찾는 데 사용되는 대체 키입니다.
		restore-keys: |
			${{ runner.OS }}-build-
			${{ runner.OS }}-

3. 의존성 패키지 설치

의존성 패키지들을 설치하는 단계입니다. 만약, 새로운 의존성 패키지를 추가 또는 변경하게 된다면, GitHub Actions에서 새로운 캐시 키를 생성하게 합니다. 이러한 변경으로 인해 이전에 저장된 캐시와 현재의 **yarn.lock** 파일이 일치하지 않게 되고, 결과적으로 캐시 히트(cache-hit)가 발생하지 않습니다. 그러면, **yarn install** 명령어를 실행하게 됩니다.

Q. 캐시 히트(cache-hit)?
A. 리소스가 캐시에 존재하여 성공적으로 검색이 되었는지를 나타내는 용어입니다.

# 3 - 의존성 패키지 설치
- name: Install Dependencies
	
	# **steps.cache.outputs.cache-hit -** 이 부분은 이전 단계(여기서는 **Cache node modules** 단계)에서 설정된 출력 값을 참조합니다. 
	# cache 액션은 캐시 히트 여부를 **cache-hit**이라는 출력 값으로 반환합니다.
	if: steps.cache.outputs.cache-hit != 'true'
	run: yarn install

4. 빌드

소스 코드 및 관련 리소스를 실행 가능한 소프트웨어로 변경하는 과정인 빌드를 진행합니다.

Q. 빌드가 정확히 뭐예요?

  1. 컴파일(Compile): 소스 코드 파일을 컴퓨터가 이해할 수 있는 실행 파일이나 바이너리 코드로 변환합니다.
  2. 트랜스파일(Transpile): 특정 소스 코드(예: TypeScript 또는 최신 JavaScript(ECMAScript) 버전)를 다른 형태의 소스 코드(예: 순수 JavaScript)로 변환합니다. 이는 브라우저 호환성을 높이거나 최신 언어 기능을 사용하기 위해 필요할 수 있습니다.
  3. 패키징(Packaging): 컴파일된 코드와 필요한 리소스(이미지, 스타일시트, 설정 파일 등)를 함께 묶어 배포 가능한 형태(예: .jar, .exe, .apk 파일 등)로 만듭니다.
  4. 최적화(Optimization): 코드를 최적화하여 성능을 개선하고, 파일 크기를 줄이며, 보안을 강화합니다. 이는 코드를 압축하거나 불필요한 부분을 제거하는 과정을 포함할 수 있습니다.
  5. 테스트 실행: 빌드 프로세스의 일환으로 단위 테스트 또는 통합 테스트를 실행하여, 코드가 예상대로 정확하게 작동하는지 검증합니다.
# 4 - 빌드
- name: Build
	run: yarn build

5. AWS 인증

지금 AWS S3 + CloudFront를 통해 이 React App을 배포하려 하고 있기 때문에, AWS에 접근하기 위한 인증 과정을 거쳐야 합니다.


## 5. AWS 인증
- name: Configure AWS Credentials
	
	# **aws-actions/configure-aws-credentials@v2** 액션을 사용합니다. 이 액션은 AWS의 서비스 인증을 위해 사용됩니다.
	uses: aws-actions/configure-aws-credentials@v2
	with:
		# 액세스 키? 시크릿 액세스 키?
		aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
		aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
		
		# 지역은 S3 버킷을 생성할 때, ap-northeast-2로 했으니, 이 값을 환경변수에 넣어주면 됩니다.
		aws-region: ${{ secrets.AWS_REGION }}

그런데, 코드를 보니 AWS 액세스 키 / 시크릿 키 / 지역이 필요합니다.

이 중, 마지막 AWS_REGION은 쉽게 알 수 있습니다. 그런데, 액세스 키와 시크릿 키는 어디서 발급받을까요?

6. AWS 액세스 /시크릿 키 가져오기

AWS 액세스 / 시크릿 키를 얻으려면 먼저, AWS 리소스에 대한 액세스 관리 서비스인 IAM에 접근해야 합니다.

1. AWS IAM 접속

2. 왼쪽 사이드바에서 ‘사용자’ 클릭

3. ‘사용자 생성’ 클릭

4. 사용자 이름 지정

저는 여기서 test-iam이라고 지정했습니다. 그 아래 체크박스는 체크하지 않았습니다. (콘솔 액세스 권한을 제공해야 하나?라는 생각이 들어서 안했는데, 정확한 건 잘 모릅니다!)

5. 권한 정책 설정

두 개의 권한 정책을 부여합니다. 현재 AWS S3 + CloudFront를 이용해 배포를 하니, S3 전체 접근 권한과 CloudFront 전체 접근 권한을 부여합니다. 맨 아래 ‘권한 경계 설정’은 따로 건드리지 않았습니다.

  • 좀 더 Full Access가 아닌 좀 더 세부적인 권한을 설정할 수 있지만, 정확히 배포를 위한 세부적인 권한이 어떤 것이 필요한지는 파악하지 못해서, Full Access로 진행했습니다.

    AmazonS3FullAccess 정책 선택


CloudFrontFullAccess 정책 선택

6. 검토 및 생성

마지막 검토 및 생성에서 두 개의 권한이 보이면, 잘 선택된 것입니다. 이후, 우측 하단의 ‘사용자 생성’ 버튼을 눌러서 생성해줍니다. 맨 아래 태그는 따로 추가하지 않았어요!

7. 생성된 사용자 확인

화면을 보시면, 맨 아래 test-iam이라는 이름으로 사용자가 생성된 것을 확인할 수 있습니다. 이러면, S3와 CloudFront에 접근할 수 있는 사용자를 만든 것입니다. 이제, 생성한 사용자를 클릭합니다.

8. 액세스 키 생성

사용자를 클릭하면 아래와 같은 화면이 보일 것입니다. 그럼, 우측 상단의 액세스 키 만들기 버튼을 클릭해주세요.

이 액세스 키가 바로 아까 환경변수에서 봤던 AWS_ACCESS_KEY_ID입니다. test-iam이라는 사용자에 접근할 수 있는 ID를 의미해요.

이후, Command Line Interface(CLI)를 선택한 뒤, 아래 권장 사항 이해 부분을 체크해줍니다. dev-ci.yml 파일의 코드를 보면 AWS CLI를 사용해서 S3, CloudFront에 접근하기 때문입니다.

현재 React App을 S3 + CloudFront로 배포하는 과정은 다른 사용사례에 모두 해당하지 않습니다.

설명 태그 설정은 스킵했습니다. ‘React App 배포를 위한 액세스 키’라는 설명을 추가해줘도 됩니다. 선택사항입니다! 이후, 우측 하단의 ‘액세스 키 만들기’ 버튼을 클릭해주세요.

9. 생성된 액세스 키 확인
액세스 키가 생성되었다면, 빨간색 영역으로 표시한 부분에서 액세스 키와 시크릿 키를 확인할 수 있습니다. 액세스 키와 시크릿 키를 복사하여 따로 어딘가에 저장합니다.

(시크릿 키는 이 화면이 아니면 확인할 수 없기 때문에, 꼭 어딘가에 따로 저장해 놓는 것을 추천합니다!)

완료 버튼을 누르면, 빨간색으로 표시한 영역에 액세스 키(+시크릿 키)가 생성된 것을 확인할 수 있어요.

여기까지만 해도 많이 했는데, 이제 GitHub Action에 환경변수로 지정을 해야 해요. 그래야 dev-ci.yml 파일에서 해당 환경변수를 읽고 쓸 수 있게 됩니다!

7. GitHub Actions 환경변수 지정

그럼, 이제 React App Repository로 돌아와서 상단 가장 오른쪽 메뉴에 있는 Settings를 클릭합니다.

이후, 우측 상단의 New respository secret 버튼을 클릭합니다.

그럼, 아래와 같은 화면이 나옵니다. 이름은 AWS_ACCESS_KEY_ID로 지정해줬고, Secret에 아까 복사했던 액세스 키를 넣어줍니다.

그럼, 이렇게 AWS_ACCESS_KEY_ID라는 환경변수가 생성된 것을 확인할 수 있습니다.
같은 방법으로 AWS_SECRET_ACCESS_KEY라는 환경변수도 하나 더 생성해주고, 아까 복사해뒀던 시크릿 키를 붙여 넣어줍니다.

추가로, AWS_REGION이라는 환경변수도 생성하고, ap-northeast-2이라고 입력해주면 됩니다. 아까 S3 버킷을 생성할 때 지역을 ap-northeast-2라고 설정했기 때문입니다!

그럼, 이렇게 아래처럼 환경변수가 생성된 것을 확인할 수 있습니다. 여기는 Last Updated가 2 weeks ago라고 되어있습니다. (제가 2주전에 작업을 이미 했기 때문입니다! 지금 작업하신 분들은 1 minutes ago라고 보일 것 같아요.)

여기까지 진행했다면, 이제 5번 AWS 인증 단계를 마무리할 수 있습니다. 그럼, 이제 AWS S3에 배포하는 과정만남았습니다.

8. AWS S3에 빌드 결과물 배포

## CRA로 생성한 React App은 build 폴더에 빌드 결과물이 들어있습니다. (dist 폴더가 아님!)
- name: Deploy to S3
	run: aws s3 sync ./build s3://${{ secrets.AWS_BUCKET_NAME }} --delete

이제, S3에 빌드된 결과물을 배포합니다. 그 전에, AWS_BUCKET_NAME 이라는 GitHub Actions 환경변수를 하나 더 추가해줍니다. 이 환경변수는 아까 생성한 S3 버킷의 이름인 my-aws-bucket으로 설정합니다.

그럼, 이제 각 명령어에 대한 설명입니다.

  • aws: AWS CLI를 호출하는 명령어입니다.
  • s3 sync: sync 명령어는 지정된 리소스(여기서는 ./build 디렉토리)와 S3 버킷 간에 파일을 동기화합니다.
  • ./build: 이 부분은 build 디렉토리를 가리킵니다.
  • --delete: 이 옵션은 S3 버킷에서 build 디렉토리에 없는 파일들을 삭제합니다. 즉, S3 버킷의 내용을build 디렉토리와 정확히 일치시키는데 사용됩니다.

즉, 빌드 디렉토리에 있는 리소스들이 생성한 S3 버킷인 my-aws-bucket 업로드된다는 의미입니다.

9. CloudFront 캐시 무효화

아까, CDN을 CloudFront로 설정한다고 했었습니다. CDN에 대한 설명에는 오리진 서버가 아닌 캐시 서버에서 리소스를 가져온다고 설명했었습니다. 그럼, 새롭게 배포되어 S3 버킷은 내용이 달라졌지만, 캐시 서버에는 아직 새로운 배포가 반영되지 않았을 수 있습니다.

그래서, 캐시 서버가 새로운 배포 결과물을 저장할 수 있도록 배포 시마다 캐시 무효화를 진행합니다.

이 과정 진행 전에, 환경변수를 하나 더 추가합니다. 아까 배포했던 CloudFront 배포의 ID를 AWS CloudFront에서 확인합니다.(빨간색 영역) 그리고, 이 ID를 복사해서 AWS_DISTRIBUTION_ID라는 GitHub Actions 환경변수에 추가합니다.

- name: Invalidate CloudFront Cache
	run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }} --paths "/*"

각 명령어에 대한 설명입니다.

  • aws cloudfront create-invalidation: AWS CLI 명령어로, CloudFront 배포의 캐시를 무효화하는 데 사용됩니다.
  • --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID }}: CloudFront 배포의 고유 ID를 지정합니다.
  • --paths "/*": 캐시를 무효화할 경로를 지정합니다. "/*"는 모든 파일에 대한 캐시를 무효화하라는 의미입니다.

여기까지 진행했다면, develop 브랜치에 코드가 push될 때마다 GitHub Action이 정상적으로 작동할 겁니다.

5. CloudFront에서 사용자 정의 오류 응답 생성

배포가 정상적으로 완료되었다면, 아까 만들었던 S3 Bucket을 다시 보면 파일들이 잘 들어와있는 것을 확인할 수 있습니다.

그럼, 이제 정상적으로 웹사이트가 보이는지 확인해봐야 합니다. 아래 빨간색 영역의 배포 도메인 이름이라는 곳을 확인하고, 해당 도메인을 복사해서 브라우저에서 접속해보세요!

그러면 403에러 또는 404에러가 발생할겁니다. 에러가 발생하지 않는 분들도 새로고침을 하면 발생할 겁니다!

이 에러를 해결하기 위해 사용자 정의 오류 응답을 생성해야 합니다. 그럼, 이 에러를 해결하기 전에 왜 에러가 발생하는지부터 확인해 보겠습니다.

에러 원인

  • SPA에서는 모든 요청이 index.html로 라우팅되어야 하지만, S3와 CloudFront는 SPA의 라우팅 메커니즘을 기본적으로 인식하지 못합니다. 따라서 사용자가 새로고침을 하거나 URL을 직접 입력하여 접근하면, S3와 CloudFront는 해당 URL에 매핑되는 실제 파일을 찾으려고 시도합니다.
  • 예를 들어, 사용자가 https://example.com/about URL로 접근하려고 하면, S3와 CloudFront는 about이라는 이름의 파일을 찾으려고 시도합니다. SPA에서는 이러한 파일이 존재하지 않기 때문에, 결과적으로 404 에러가 발생합니다.
  • CloudFront는 존재하지 않는 리소스에 대한 요청을 S3로 전달할 때, S3 버킷의 권한 설정에 따라 404 Not Found 대신 403 Forbidden 오류를 반환할 수 있습니다.

해결 방법

생성했던 CloudFront 배포로 들어가서, 오류 페이지 탭을 클릭합니다. 이후, 사용자 정의 오류 응답 생성 버튼을 클릭합니다.

  • 그리고, 오류 코드는 아래 이미지와 같이 지정하고, 응답 페이지 경로를 /index.html로 지정합니다.
    • CloudFront에서 404 또는 403에러가 발생한다면 index.html 파일의 경로로 이동하라는 의미입니다.
  • 이후, HTTP 응답 코드는 200으로 지정해줍니다.

아래 이미지에서는 404 에러만 오류 응답을 생성했는데, 403 에러에 대해서도 한개 더 오류 응답을 생성해줍니다.

404, 403에러에 대해 모두 오류 응답을 설정했다면, 다시 CloudFront의 배포 도메인으로 접속해보면, 정상적으로 배포한 웹사이트가 보이는 것을 확인할 수 있습니다!

그런데, 웹사이트는 배포 되었는데 테스트를 위한 develop(dev) 환경과 실제 서비스가 동작할 production(prd) 환경이 분리되지 않았습니다. 여기도 추가로 작업을 진행해 볼게요.

6. 배포한 웹사이트를 dev/prd 환경으로 분리

dev/prd 환경을 분리하는 것은 간단합니다. 3, 4, 5번 과정을 그대로 한번 더 반복하면 됩니다. 요약하면 다음과 같습니다.

  1. S3 Bucket 새로 하나 더 생성
  2. CloudFront 배포 새로 하나 더 생성
  3. GitHub Action 코드 수정

1. S3 Bucket 새로 하나 더 생성

버킷 이름은 dev 환경을 위한 my-aws-bucket-dev로 하겠습니다. 위에서 생성했던 과정과 동일하게 생성해주세요.

2. CloudFront 배포 새로 하나 더 생성

위에서 생성했던 CloudFront 배포 방법 그대로 한번 더 반복하시면 됩니다! 마지막에 사용자 정의 오류 페이지도 생성해주셔야 합니다.

3. GitHub Action 코드 수정

이전에 만들었던 dev-ci.yml의 파일명을 먼저 바꿔줄게요. 왜냐하면, 지금 새로 만든 버킷을 dev 환경으로, 이전에 만들었던 버킷을 prd 환경으로 사용할 것이라서요! dev-ci.ymlmain-ci.yml

그리고, 일부 코드를 수정해야 합니다. main-ci.yml로 바뀌게 되면서 달라진 부분은 다음과 같습니다.

# 1) 여기 이름 바뀌었습니다!
name: MAIN CI

on:
	# 2)main 브랜치에 push가 될 때마다 이 action이 실행됩니다. (이 부분도 바뀌었어요)
  push:
    branches:
      - main
    tags:
      - 'production-**'

jobs:
  Deploy:
    runs-on: ubuntu-latest

    steps:
			# 1 - 소스코드 복사
      - name: Checkout source code
        uses: actions/checkout@v3
			
			# 2 - node_modules 캐싱
      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}-
			
			# 3 - 의존성 패키지 설치
      - name: Install Dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: yarn install
			
			# 4 - 빌드
      - name: Build
        run: yarn build
			
			# 5 - AWS 인증
      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}
			
			# 6 - AWS S3에 빌드 결과물 배포
      ## CRA로 생성한 React App은 build 폴더에 빌드 결과물이 들어있습니다. (dist 폴더가 아님!)
			## 3) 환경변수 이름이 바뀌었어요. 환경변수를 편집해주셔야 해요! AWS_BUCKET_NAME -> AWS_BUCKET_NAME_PRD
      - name: Deploy to S3
        run: aws s3 sync ./build s3://${{ secrets.AWS_BUCKET_NAME_PRD }} --delete
			
			# 7 - CloudFront 캐시 무효화
			## 4) 환경변수 이름이 바뀌었어요. 환경변수를 편집해주셔야 해요! AWS_DISTRIBUTION_ID_PRD
      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID_PRD }} --paths "/*"
  • GitHub Action의 이름이 MAIN CI로 변경되었습니다.
  • 대상 브랜치가 develop → main으로 변경되었습니다.
  • S3 버킷 이름을 나타내는 환경변수명이 수정되었습니다. AWS_BUCKET_NAME → AWS_BUCKET_NAME_PRD
  • CloudFront 배포 ID를 나타내는 환경변수명이 수정되었습니다. AWS_DISTRIBUTION_ID → AWS_DISTRIBUTION_ID_PRD
  • main 브랜치에 코드를 한번 push 또는 pull request 이후 merge를 해서 이 MAIN CI를 한번 실행해주세요.

그럼, 이제 기존 버킷은 main 브랜치에 코드가 push되는 경우 MAIN CI(GitHub Actions)가 실행될거예요.

이제 새로 만든 my-aws-bucket-dev에 대한 DEV CI를 다시 만들어 볼게요. 파일 이름은 dev-ci.yml로 만들어주세요.

name: DEV CI

on:
  push:
    branches:
      - develop
    tags:
      - 'development-**'

jobs:
  Deploy:
    runs-on: ubuntu-latest

    steps:
      - name: Checkout source code
        uses: actions/checkout@v3

      - name: Cache node modules
        uses: actions/cache@v3
        with:
          path: node_modules
          key: ${{ runner.OS }}-build-${{ hashFiles('**/yarn.lock') }}
          restore-keys: |
            ${{ runner.OS }}-build-
            ${{ runner.OS }}-

      - name: Install Dependencies
        if: steps.cache.outputs.cache-hit != 'true'
        run: yarn install

      - name: Build
        run: yarn build:development

      - name: Configure AWS Credentials
        uses: aws-actions/configure-aws-credentials@v2
        with:
          aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
          aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
          aws-region: ${{ secrets.AWS_REGION }}

      - name: Deploy to S3
        run: aws s3 sync ./build s3://${{ secrets.AWS_BUCKET_NAME_DEV }} --delete

      - name: Invalidate CloudFront Cache
        run: aws cloudfront create-invalidation --distribution-id ${{ secrets.AWS_DISTRIBUTION_ID_DEV }} --paths "/*"
  • AWS_BUCKET_NAME_DEV라는 GitHub Actions 환경변수를 새로 추가해주세요. 이 환경변수의 값은 my-aws-bucket-dev입니다.
  • AWS_DISTRIBUTION_ID_DEV라는 GitHub Actions 환경변수를 새로 추가해주세요. 이 환경변수의 값은 새로 배포한 CloudFront 배포의 ID가 됩니다.

여기까지 끝났다면, 이제 dev/prd 환경이 분리되었습니다. 각각의 CloudFront 도메인으로 접속해서 만들었던 웹사이트가 잘 보이는지 확인해주세요!

7. 커스텀 도메인 연결

이제 마지막 단계입니다. CloudFront 도메인의 경우 d1ajsbnahs.cloudfront.net같은 형태의 도메인으로 보일 거예요. 이제, my-nice-aws-bucket.com이라는 커스텀 도메인을 적용해 보겠습니다.

커스텀 도메인을 적용하기 위해서는 도메인을 구매해야 하는데, 여러 도메인 구매 방법이 있지만 저는 이미 다른 서비스들도 AWS의 Route53을 통해 도메인을 구매 및 관리해오고 있기 때문에 Route 53을 사용하겠습니다.

1. Route 53 접속

2. 도메인 등록하기

등록된 도메인 → 도메인 등록 버튼을 클릭합니다.

이후, 자신이 사용할 도메인(여기서는 my-nice-aws-bucket.com)을 입력하고, 이 도메인이 사용 가능한지 확인합니다. 사용 가능하다면, 3번의 선택 버튼을 클릭합니다.

이후 사용할 도메인을 결제합니다. 13$네요. (저는 실제로 다른 도메인을 결제했기 때문에 이후부터는 도메인이 비공개 처리됩니다! my-nice-aws-bucket.com을 결제했다고 가정할게요.)

결제가 진행되고, 도메인 등록을 조금 기다리면(약 5분 ~ 20분까지도 소요될 수 있습니다.) 아래 스크린샷과 같이 도메인이 등록된 것을 확인할 수 있습니다.

3. Route 53 호스팅 영역 레코드 편집

Route 53에서 레코드를 편집하거나 추가하는 이유는, 사용자가 정의한 도메인 이름(my-nice-aws-bucket.com 같은)을 특정 AWS 서비스(예: Amazon S3 버킷, CloudFront 배포)에 연결하기 위해서입니다.

아래와 같이 설정하면, my-nice-aws-bucket.com 도메인으로 들어오는 트래픽을 CloudFront 배포(d1ajsbnahs.cloudfront.net)로 라우팅합니다. 이를 통해 사용자는 도메인 이름을 통해 CloudFront를 통해 호스팅되는 웹사이트(배포한 S3 Bucket 내부의 리소스)에 접근할 수 있습니다.

  • 5번의 경우에, my-nice-aws-bucket은 prd 환경이기 때문에, prd 환경에 해당하는 CloudFront 배포 ID를 찾아서 선택해주셔야 합니다!

레코드 생성을 마무리 했다면, 저렇게 레코드가 보이게 될 거예요. 첫번째 빨간색 박스로 가려진 부분은, my-nice-aws-bucket.com이겠고, 값/트래픽 라우팅 대상은 d1ajsbnahs.cloudfront.net로 보이게 될 겁니다.

그런데, www.my-nice-aws.bucket.com으로 접속하더라도 배포한 웹사이트가 보여야 합니다.

그래서, 위와 동일한 과정을 거쳐 레코드를 하나 더 생성해줍니다. 한 가지 다른점은, 1번 과정의 레코드 이름에 www가 추가된 것입니다. (이미지에는 www.인데, www만 입력해주시면 됩니다!)

그리고, dev 환경도 커스텀 도메인으로 연결해줍니다. dev 환경의 도메인 이름은 dev.my-nice-aws-bucket.com으로 진행할게요.

달라진 점은 레코드 이름 앞에 1번 과정에서 dev라는 prefix가 붙었다는 것이고, 5번 과정에서 이번엔 dev 환경에 해당하는 CloudFront 배포 ID를 찾아서 선택해주셔야 합니다!

4. CloudFront 배포에 대체 도메인 이름 설정

이제, 배포한 prd 환경의 CloudFront로 돌아옵니다. 1번 과정을 확인을 먼저 해주세요. 1번 영역의 대체 도메인 이름, 사용자 정의 SSL 인증서가 적혀있는게 없을 거예요. 그럼, 2번 과정 편집을 눌러서 대체 도메인 이름을 등록해야 합니다.

아까 만들었던 도메인을 대체 도메인 이름에 추가합니다. prd 환경이기 때문에 하나는 my-nice-aws-bucket.com, 다른 하나는 www.my-nice-aws-bucket.com으로 설정해주시면 됩니다. 그런데, 사용자 정의 SSL 인증서가 보이지 않을 거예요. 그래서, AWS Certificate Manager(ACM) 서비스를 통해 인증서를 요청해야 합니다. 1번 과정에 보이는 인증서 요청 버튼을 클릭합니다.

Q. 왜 ‘사용자 정의 SSL 인증서’를 요청해야 하나요?

A. 이 단계는 사용자가 소유한 도메인(my-nice-aws.bucket.com)을 연결하고, 이 도메인에 대해 HTTPS 연결을 보안적으로 처리하기 위한 단계입니다.

  • SSL 인증서의 필요성: HTTPS를 통해 웹 트래픽을 보안적으로 처리하기 위해서는 SSL(Secure Sockets Layer) 또는 TLS(Transport Layer Security) 인증서가 필요합니다. 이 인증서는 데이터 전송의 암호화를 보장하고, 웹사이트의 신뢰성을 증명합니다.
  • 사용자 정의 인증서: CloudFront는 AWS Certificate Manager(ACM)에서 생성하거나 가져온 SSL/TLS 인증서를 사용할 수 있습니다. 사용자 정의 SSL 인증서를 선택함으로써, 자신의 도메인에 대한 HTTPS 요청을 처리할 수 있게 됩니다.

Q. AWS Certificate Manager(ACM)이 무슨 서비스인가요?

A. AWS Certificate Manager는 SSL/TLS 인증서를 쉽게 생성, 관리 및 배포할 수 있는 서비스입니다. 사용자는 ACM을 통해 인증서를 생성하고, 이를 CloudFront 배포에 연결할 수 있습니다.

5. ACM에서 인증서 요청

인증서 요청 버튼을 클릭하면 아래 화면이 나옵니다. 퍼블릭 인증서 요청을 선택하고 다음 버튼을 클릭합니다.

그리고, 도메인 이름을 아래와 같이 작성해줍니다. 다른 건 기본값을 그대로 유지한 채, 요청 버튼을 클릭합니다.


그러면, 아래와 같이 인증서가 보이게 될거예요. 그런데, 상태가 발급 진행중으로 보이게 될겁니다.

그럼, 인증서를 클릭해서 상세 페이지로 들어옵니다. 그리고, Route 53에서 레코드 생성이라는 버튼을 클릭합니다. 이 버튼을 클릭하면, ACM이 자동으로 필요한 DNS 레코드를 Route 53의 해당 도메인 호스팅 영역에 추가합니다.

Q. 'Route 53에서 레코드 생성’ 이라는 과정이 왜 필요한 건가요?

A. ACM에서 SSL/TLS 인증서를 발급하는 과정에는 도메인 소유권 검증이 필수적으로 포함됩니다. Route 53에서 레코드 생성 버튼을 클릭하여 DNS 레코드를 생성하는 것은, 인증서에 대한 도메인 소유권을 검증하는 데 사용되는 표준 방법 중 하나입니다. 이 과정을 통해 ACM은 인증서를 발급할 권한이 있는지 확인합니다.

저는 이미 이 과정을 모두 진행했기 때문에, 일치하는 항목이 없다고 보입니다. 그러나 이 과정을 진행중인 분들은 3개의 일치하는 항목이 보이게 될 거예요. 그대로 레코드 생성을 진행해주시면 됩니다.

이 과정까지 모두 끝내면, 인증서가 발급됨 상태로 바뀔 거예요. 혹시 바로 바뀌지 않는 분들은 2~3분 정도 기다렸다가 다시 시도하면 바로 바뀌어 있을 겁니다.

6. CloudFront 배포에 대체 도메인 이름 설정 (재시도)

이제, 다시 아까의 prd 환경에 대한 CloudFront 배포로 돌아오면, 아까 ACM을 통해 발급한 사용자 정의 SSL 인증서를 선택할 수 있습니다. 선택하고, 변경 사항 저장 버튼을 클릭합니다.

그럼, 이제 대체 도메인이 등록되었습니다. 이제 my-nice-aws-bucket.com 또는 www.my-nice-aws-bucket.com으로 접속하면 배포된 prd 환경의 웹사이트가 정상적으로 보일 거예요.

마지막입니다. dev 환경도 연결해줘야 합니다. dev 환경에 해당하는 CloudFront 배포에 대해 위 과정과 똑같이 대체 도메인을 등록하고, 사용자 정의 SSL 인증서도 등록해줍니다.

아래는 최종적으로 보여지는 CloudFront입니다. 이제, dev.my-nice-aws-bucket.com으로 접속하면 dev 환경도 보이게 될 겁니다.

결론

S3 + CloudFront로 배포하는 다른 글도 많은데, dev/prd 환경을 각각 GitHub Action을 이용해서 배포를 자동화 시키는 과정과, 커스텀 도메인 설정까지 모두 합쳐진 과정이 없어서 글을 작성하게 되었습니다. 그렇게 어려운 내용은 아니지만, 프론트 앱을 배포하시는 분들께 도움이 되었으면 좋겠네요..!

혹시 틀린 부분, 수정할 내용 또는 개선할 내용이 있으면 댓글로 알려주시면 감사하겠습니다 :)

참고

s3 - cloudfront - route53 연동

카카오웹툰은 GitHub Actions를 어떻게 사용하고 있을까? | 카카오엔터테인먼트 FE 기술블로그

CSR 배포 전략 with S3, Cloud Front, Github Actions

S3 버저닝(S3 Versioning) 설명

0개의 댓글