본문 바로가기
dev/프로젝트

[프로젝트] 프론트 배포 (S3 + CloudFront + Route 53 + Gabia) + EC2 재배포 시 참고 적용 ver1

by dev-everyday 2025. 2. 11.
반응형

1. 테스트 코드 고치기

기존에 Session으로 구현했던 로그인을 JWT+Oauth2+Redis로 바꾸면서 accessToken, refreshToken을 DB에서 삭제하였는데 반영해야 한다.

테스트 코드에서 사용한 부분들을 수정해보자.

accessToken, refreshToken 삭제 후 반영하였다.

2. 프론트엔드 배포 (S3 + CloudFront + Route 53 + Gabia)

프론트엔드 코드 역시 배포를 해보려고 한다.

build를 해서 S3에 배포를 할 것인데 먼저 S3 버킷을 생성해보자.

내 계정으로 EC2와 S3에 접근할 것이라 ACL 비활성화로 설정하였다.

그리고 Amazon S3 > 버킷 > fortuneappbucket > 정적 웹 사이트 호스팅 편집을 통해서 정적 웹 사이트 호스팅을 활성화하였다.

build 후 build 폴더를 S3 버킷에 업로드하였다.

다음은 CloudFront를 생성하려고 한다.

원본 도메인에는 아까 생성한 S3 주소를 선택하면 된다.

원본 액세스는 원본 액세스 제어 설정을 선택하였고 Enable Origin Shield를 아싱아 태평양(서울) 리전을 선택하고 예를 선택하였다.

Origin Shield를 찾아보다보니 현재 사용에는 필요하지 않을 거 같아서(와 비용이 들어서) 아니오로 선택하였다.

뷰어 프로토콜 정책을 Redirect HTTP to HTTPS로 설정하여 HTTP로 접속하여도 HTTPS로 전환하도록 설정하였다.

 

허용된 HTTP 방법은 정적 리소스만 배포할 예정이라 GET, HEAD만 허용하였다.

가격 분류는 북미, 유럽, 아시아, 중동 및 아프리카에서 사용으로 선택하고 대체 도메인 이름에 가비아에서 구매한 연 550원 합리적 가격 도메인을 입력하였다.

그 전에 Route53에 도메인 등록을 먼저 해주려고 한다.

Route 53 > 호스팅 영역 > 호스팅 영역 생성을 통해서 구매한 도메인 이름을 입력하고 생성한다.

생성 후에 ns 유형의 주소가 4개 생긴 것을 확인할 수 있다.

이후 가비아에 접속하여 도메인 관리에 접속하여 네임서버 설정을 선택한다.

 

예시로 ns-xxxx.awsdns-xx.org. 이렇게 생성되는데 뒷 . 을 제외하고 네 주소 모두 입력해준다.

그리고 다시 돌아와서 인증서 생성을 완료해주면 되는데 도메인 > Route 53에서 레코드 생성에서 생성한 레코드를 선택하면 된다.

아직 인증서 상태가 검증 대기 중인데 찾아보니 꽤 시간이 걸린다고 한다. 우선은 인증서는 나중에 확인해봐야할 거 같다.

 

설정을 하면서 글을 작성하다 보니 순서가 헷갈릴 수 있는데 정리하자면 아래와 같다.

1. 가비아에서 도메인 구매

2. Route 53에서 호스팅 영역 생성을 통해 DNS를 검증한다(Route 53에서 구매하지 않음) + 가비아에서 NS 추가

3. ACM에서 인증서 선택을 하는데 이 인증서의 도메인을 Route 53에서 생성한 레코드를 가져온다

4. 발급 완료까지 대기하기..

 

성격이 급해서 발급됨으로 변경되기 전까지 뭔가 잘못 되었나하고 다른 방법을 엄청 찾아보았다.

약 30분 이내로 반영되는거 같다.

 

바로 CloudFront로 돌아와서 Custom SSL certificate에 아까 발급 완료된 인증서를 연결하였다.

CloudFront 생성을 완료하면 정책 복사가 가능한데 닫기를 눌렀더라도 CloudFront > 원본 편집을 통해서 정책을 확인할 수 있다.

 

복사한 정책을 Amazon S3 > 버킷 > 본인S3버킷이름 > 버킷 정책 편집을 통해서 버킷 정책을 추가하자.

 

그리고 Route53에서 Record를 아래와 같이 추가하자.

별칭(alias) 사용을 통해서 CloudFront 이후 설정한 내 cloudfront 선택을 해주고 레코드 생성을 하고 도메인으로 접속하면 S3에 올린대로 잘 접속되는 것을 확인할 수 있다.

주소로 접속했을 때 index.html로 잘 이동하는데(비주얼 버그 났나?) 서버와 연결을 하지 않아서 Google 로그인이나 다른 게 작동하지 않는다.

아무튼 http로 입력해도 https로 잘 변경되는 것도 확인할 수 있다.

3. EC2와 S3 연동하기

우선 프로젝트 동작 방식은 아래와 같다.

1) S3 → 정적 웹사이트(React, Vue, HTML, CSS, JS) 배포 (기본적으로  무료 티어 제공)
2) CloudFront → S3의 정적 웹사이트를 HTTPS & 캐싱하여 배포 (매월 1TB 트래픽 무료 티어 사용 가능)
3) EC2 → 백엔드(API 서버, 데이터베이스) 운영
4) Route 53 → 도메인 연결

사용자 → 도메인(example.com) → CloudFront → S3 (정적 파일)
               └→ API 요청 (example.com/api) → EC2 (API 서버)

 

EC2를 분명 중단해놨는데 사라져서 자동으로 삭제가 되는 기능이 있는 줄 알았다.

하지만 리전이 미국이었다.. 한국에서 다시 내 EC2를 찾아 켰다..

먼저 EC2에 S3에 대한 권한을 부여하기 위해 IAM > 역할 > 역할 생성을 해주자.

읽기 쓰기 둘 다 가능한 AmazonS3FullAccess를 선택하였다.

 

그리고 프론트엔드 깃허브로 와서 github Actions를 생성하였다.

백엔드 github Actions와 동일하게 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, ENV_FILE 등을 입력해야 하는데 AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY를 위해서 IAM으로 이동하여 사용자를 생성해주어야 한다.

IAM으로 이동해서 사용자를 추가한 후 액세스 키 만들기를 클릭한다.

생성 후 액세스 키의 ID와 비밀 액세스 키를 github에 등록해주자.

그리고 .env.prod 파일을 생성해서 아래의 정보를 추가하였다.

REACT_APP_API_URL=퍼블릭 IP 주소
REACT_APP_S3_BUCKET_NAME=내 bucket 이름
REACT_APP_CLOUDFRONT_URL=cloudfront URL
REACT_APP_ENV=production

base64로 변환한 후 github setting actions secret에 추가하였다.

그리고 백엔드 github actions를 실행하는데 기존에 설정한 variable인 public ip 주소가 변경되었기 때문에(EC2 껐다 켰다의 이슈) 변경해주고 실행해주자.

그리고 로그인 방식을 변경하면서 JWT token을 추가하였는데 deploy.yml에 docker 실행 시 JWT_TOKEN을 알 수 있도록 github에 secrets으로 추가하였다.

오랜만에 보는 내 EC2 health check API, 아무튼 백엔드 서버 실행 확인해주고 프론트 deploy.yml도 실행시킨다.

name: Deploy Frontend Code to S3

on:
  push:
    tags:
      - "v*"  # tag rule (v1.0.0)
  workflow_dispatch:

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout Repository
        uses: actions/checkout@v3

      - name: Load Environment Variables
        run: echo "${{ secrets.ENV_FILE }}" > .env

      - name: Install Dependencies
        run: npm install

      - name: Build Project
        run: npm run build

      - name: Verify Build Folder
        run: ls -la 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: Clear S3 Bucket (Remove Existing Files)
        run: aws s3 rm s3://${{ secrets.S3_BUCKET_NAME }} --recursive

      - name: Upload Build to S3
        run: aws s3 sync build/ s3://${{ secrets.S3_BUCKET_NAME }}

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

 

S3에 잘 올라간 것을 확인할 수 있다.

그리고 route53에 public IP 로 A 레코드로 등록해줘야한다.

그리고 기존에 localhost로 설정된 프론트엔드, 백엔드 코드 전부 수정하였다.

 

도커에 용량이 많을 때면 github actions에서 배포가 실패하기도 한다. 아래 명령어를 실행해주자.

docker container prune -f
docker image prune -af
docker volume prune -f
docker system prune -af --volumes

 

간략히 과정을 정리하면 아래와 같다.

1) npm install & build 수행

2) build 폴더가 정상적으로 생성되었는지 확인

3) AWS CLI 로그인

4) S3 기존 파일 삭제 후 새로운 빌드 파일 업로드

 

도메인 등록 고민

나는 도메인을 프론트와 연결해놓고 EC2는 public IP로 요청하면 API 처리가 가능할 줄 알았는데 Google Cloud Console에서 OAuth2에 등록을 하려면 따로 도메인이 필요하다는 것을 몰랐다.

그래서 해결법을 찾아보았는데 아래와 같이 세 해결법을 적용하려고 하였다.

1) 프론트는 도메인으로 쓰고 백은 api.도메인으로 레코드를 분리해서 사용 + nginx 설정하기

2) 등록을 EC2 public domain으로 하기

3) ALB + 1번 방법

2번의 경우 정보도 많이 적혀 있고 부적절한 사용법이라 해서 1번으로 해보기로 하였다.

 

Route 53에 A 레코드로 api.도메인.com으로 따로 백엔드용 레코드를 생성하였고 axios에서 배포 시에는 해당 주소로 접근하게 설정하였다.

  const handleGoogleLogin = () => {
    const API_BASE_URL = process.env.REACT_APP_API_BASE_URL || "http://localhost:8080";
    window.location.href = `${API_BASE_URL}/oauth2/authorization/google`;
  };

그래도 계속 localhost로 redirect되는 현상이 발생하였는데 이것저것 고치다가 환경 변수가 잘 적용되지 않는 걸 확인하였고 아래와 같이 수정하였다.

      - name: Build Project
        run: |
          export $(grep -v '^#' .env.prod | xargs) && npm run build:prod

그리고 SecurityConfig에서 기존에 login page를 백에서 구현하여서 backendurl로 이동하게 설정해놨는데 더 이상 .loginPage를 설정할 필요가 없기 때문에 삭제하였다.

그리고 EC2에 환경 변수를 nano /home/ubuntu/config/application.properties 를 통해서 직접 등록하였다.

- Oauth2

- Frontend, Backend URL

- JPA

기존에는 loadUser 후에 아래와 같이 return을 하였었다.

    if (existingUser.isPresent()) {
        user = existingUser.get();
    } else {
        user = User.builder()
                .email(email)
                .name(name)
                .provider(provider)
                .providerUid(providerUid)
                .isRegistered(false)
                .build();
        userRepository.save(user);
    }

    return oAuth2User;

이러면 Security Context에 저장되지 않는다.

Spring Security가 OAuth2UserService가 반환하는 OAuth2User 객체가 Security Context에 저장하는데 저장되지 않는다면 이후 요청에서 인증 정보가 사라지는 문제가 발생할 수 있다.

또한 OAuth2 인증 후 사용자에게 ROLE_USER와 같은 권한을 부여해야하는데 그 과정이 빠져있다.

그래서 아래와 같이 바꿔주기로 하였다.

    return new DefaultOAuth2User(
        Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")), // 권한 부여
        oAuth2User.getAttributes(), // 기존 OAuth2User 속성 유지
        "email" // 식별자
    );

application.properties에도 누락된 값들을 다 올려서 반영하였다.

spring.security.oauth2.client.registration.google.client-id=
spring.security.oauth2.client.registration.google.client-secret
spring.security.oauth2.client.registration.google.redirect-uri=
frontend.url=
backend.url=
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.format_sql=true
spring.datasource.url=
spring.datasource.username=
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.data.redis.host=
spring.data.redis.port=
jwt.secret=

google cloud console에 http://api.도메인.com/ 로 설정해놨는데 http라서 응답을 받지 못하는 문제가 생겼다.

1) ALB 유료로 이용하기

2) nginx + Let's Encrypt 이용하기

2번으로 가기로 하였다.

sudo apt update
sudo apt install nginx -y

그리고 기존에 80 port로 실행하던 fortune-container도 다시 8080로 변경하였고 nginx가 80을 사용하게 한다.

기존 설정을 백업 하고 새로운 파일을 생성하였다.

sudo mv /etc/nginx/sites-available/default /etc/nginx/sites-available/default.bak
sudo nano /etc/nginx/sites-available/default
server {
    listen 80;
    server_name 도메인;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;
    }
}

그리고 SSL 인증서를 발급하기 위해 Let's Encrypt를 사용할 것인데 certbot을 설치하고 인증서를 발급한다.

sudo apt install certbot python3-certbot-nginx -y
sudo certbot certonly --webroot -w /var/www/html -d api.fortunebara.shop --force-renewal

인증서 발급 후 nginx 설정 파일을 수정하자.

sudo nano /etc/nginx/sites-available/default

기존 HTTP 설정에 HTTPS 설정을 추가하고 SSL 인증서를 적용한다.

server {
    listen 80;
    server_name api.fortunebara.shop;

    location /.well-known/acme-challenge/ {
        root /var/www/html;
        allow all;
    }

    location / {
        return 301 https://$host$request_uri;  # 모든 HTTP 요청을 HTTPS로 리디렉션
    }
}

server {
    listen 443 ssl;
    server_name api.fortunebara.shop;

    ssl_certificate /etc/letsencrypt/live/api.fortunebara.shop/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/api.fortunebara.shop/privkey.pem;
    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_ciphers HIGH:!aNULL:!MD5;
    ssl_prefer_server_ciphers on;

    location / {
        proxy_pass http://localhost:80;  # 백엔드 서버로 요청 전달
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

그리고 nginx를 재시작해준 후 방화벽 및 포트를 열어준다.

sudo systemctl restart nginx
sudo ufw allow 443/tcp
sudo ufw allow 80/tcp
sudo ufw reload

SSL 자동 갱신 설정을 해주는데 매주 새벽 3시에 자동 갱신하게 설정해주자.

sudo crontab -e
0 3 * * 0 certbot renew --quiet && systemctl reload nginx

 

자, 이제 잘 되나 확인해보자!

확인해보니 proxy_pass http://localhost:80;  # 백엔드 서버로 요청 전달 << 여기서 무한 루프가 발생하여서 8080으로 변경하였다.

그리고 백엔드 deploy.yml에도 80 포트에 배포하고 있어서 수정하였다.

이 부분 해결법만 써도 글 하나가 뚝딱 나올 거 같긴하다..

EC2 + OAuth2 시 다들 AWS EC2 domain을 등록하는데 권장하지 않는 것으로 알고 있어서 다른 방식을 꼭 찾으려고 한다.

 

글이 길어져서 2부에서 다시 이어서 작성하려고 한다.

반응형