Docker + Spring Boot + Nginx + React + EC2 배포 (feat.SSL,Let's encrypt)-(3)

Dave.kim·2023년 7월 21일
2

프로젝트

목록 보기
4/8
post-thumbnail

마지막으로, 이번 글에서는 React 컨테이너 배포 및 프록시를 진행했던 것을 기록해보려 한다.

  • 현재 React개발 파트에서는 localhost:3000으로 개발을 진행 중이고, 웹 렌더링도 배포가 되어야 외부에서 접속이 가능하다고 판단했다.
  • 서브도메인을 파서 현재 api url로 적용해버릴 수 있었지만, 또 다시 ssl인증을 진행하다가 수많은 오류가 나올 것 같았다.
  • 그리하여, {현 도메인}/api 로 시작하는 url은 api 서버로 프록시 되도록, {현 도메인}으로 시작하는 url은 웹 페이지로 프록시 되도록 설정을 해놨다.

React 이미지 생성

🏷 1. React -> Dockerfile 작성

FROM node:alpine as builder
WORKDIR /usr/src/app
COPY package.json .
RUN npm install
COPY ./ ./
RUN npm run build

FROM nginx 
EXPOSE 3000
COPY ./default.conf /etc/nginx/conf.d/default.conf 
COPY --from=builder usr/src/app/build /usr/share/nginx/html
  • FROM node:alpine as builder node:alpine 이미지를 기반으로 "builder"라는 이름의 새 빌드 스테이지를 생성한다. node:alpine은 Node.js가 설치된 가벼운 알파인 리눅스를 사용한다.

  • WORKDIR /usr/src/app 작업 디렉토리를 설정하는 부분이다. 이 디렉토리는 이후의 명령들이 실행되는 디렉토리를 지정한다.

  • COPY package.json . 현재 디렉토리의 package.json 파일을 이미지 내 /usr/src/app 디렉토리에 복사하는 명령이다.

  • RUN npm install 의존성을 설치하는 부분이다.

  • COPY ./ ./ 현재 디렉토리의 모든 파일과 폴더를 이미지의 /usr/src/app 디렉토리에 복사한다.

  • RUN npm run build 프로젝트 빌드.


  • EXPOSE 3000 컨테이너가 리슨할 포트를 노출시킨다.

  • COPY ./default.conf /etc/nginx/conf.d/default.conf default.conf 파일을 nginx의 기본 설정 파일로 복사한다.

  • COPY --from=builder /usr/src/app/build /usr/share/nginx/html 앞에서 생성한 빌더 스테이지에서 빌드된 애플리케이션 파일을 nginx의 html 디렉토리에 복사한다.

🏷 2. React -> default.conf 작성

server {
    listen 3000; 

    location / {

        root /usr/share/nginx/html; 
        index index.html index.htm; 
        try_files $uri  $uri/ /index.html; 

    }
}
  • location / 블록은 루트 URL 경로(/)에서 발생하는 모든 요청을 처리한다.
  • root /usr/share/nginx/html; 구문은 정적 파일이 위치한 디렉토리를 지정하고, 이 경우, Dockerfile에서 Nginx 이미지의 해당 위치로 빌드 결과물을 복사하게 된다.
  • index index.html index.htm; 구문은 Nginx가 디렉토리를 제공할 때 사용할 기본 파일 이름을 지정한다.
  • try_files $uri $uri/ /index.html; 구문은 Nginx가 요청을 처리하는 방식을 정의하는 부분이다. Nginx는 우선 $uri가 파일이나 디렉토리에 매핑되는지 확인하고, 그렇지 않은 경우 /index.html 파일을 제공한다.

🏷 3. EC2 -> nginx.conf 수정

server {
    listen 80;
    listen [::]:80;
    server_name www.jaetteoli.shop;
    access_log off;

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

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

server {
    listen 443 ssl;
    server_name www.jaetteoli.shop;

    ssl_certificate /etc/letsencrypt/live/www.jaetteoli.shop/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/www.jaetteoli.shop/privkey.pem;
    include /etc/letsencrypt/options-ssl-nginx.conf;
    ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem;

    location /api {
        proxy_pass http://server:8081;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-Host $server_name;
        proxy_set_header X-Forwarded-Proto $scheme;
    }

    location / {
        proxy_pass http://web:3000;
        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;

            
    }
}
  • /api 경로에 대한 요청을 server:8081로, 그 외 모든 요청web:3000으로 프록시한다는 의미이다.

🏷 4. React -> .github/workflows/npm-publish.yml

name: Deploy

on:
  push:
    branches:
      - main

jobs: 
  build:
    runs-on: ubuntu-latest
    
    strategy: 
      matrix: 
        node-version: [20.3.1] 

    steps:
      - uses: actions/checkout@v2
      - name: Use Node.js ${{ matrix.node-version }}
        uses: actions/setup-node@v1
        with: 
          node-version: ${{ matrix.node-version }} 
      
      - name: web docker build and push
        run: |
          docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
          docker build -t ${{ secrets.DOCKER_REPO }}/jaetteoli-web .
          docker push ${{ secrets.DOCKER_REPO }}/jaetteoli-web
  • GitHub Actions 워크플로우로는 메인 브랜치에 푸시가 발생할 때마다 애플리케이션을 Docker 이미지로 빌드하고, Docker Hub에 푸시하는 과정을 명시했다.

Server 부분 수정

🏷 1. SpringBoot -> docker-compose.yaml 수정

version: '3'
services:
  server:
    container_name: server
    image: kimjiseop/jaetteoli-server
    expose:
      - 8081
    ports:
      - 8081:8081
    restart: "always"
    env_file:
      - .env

  web:
    container_name: web
    image: kimjiseop/jaetteoli-web
    expose:
      - 3000

  nginx:
    container_name: nginx
    image: kimjiseop/jaetteoli-nginx
    restart: unless-stopped
    ports:
      - 80:80
      - 443:443
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    depends_on:
      - "server"
      - "web"
    command : "/bin/sh -c 'while :; do sleep 6h & wait $${!}; nginx -s reload; done & nginx -g \"daemon off;\"'"

  certbot:
    image: certbot/certbot
    restart: unless-stopped
    volumes:
      - ./data/certbot/conf:/etc/letsencrypt
      - ./data/certbot/www:/var/www/certbot
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h & wait $${!}; done;'"
  • 전체적인 내용은 이렇다.
  web:
    container_name: web
    image: kimjiseop/jaetteoli-web
    expose:
      - 3000
  • 여기서 web 컨테이너를 추가하고
depends_on:
  - "server"
  - "web"
  • depends_onweb도 추가한다.

🏷 2. SpringBoot -> .gradle/workflows/gradle.yml 수정

      - name: executing remote ssh commands using password
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.HOST }}
          username: ubuntu
          key: ${{ secrets.KEY }}
          script: |
            if [ "$(docker ps -qa)" ]; then
              sudo docker rm -f $(docker ps -qa)
            fi
            sudo docker pull ${{ secrets.DOCKER_REPO }}/jaetteoli-server
            sudo docker pull ${{ secrets.DOCKER_REPO }}/jaetteoli-web
            sudo docker pull ${{ secrets.DOCKER_REPO }}/jaetteoli-nginx
            sudo docker-compose -f /home/ubuntu/github/workspace/docker-compose.yaml up -d
            docker image prune -f
  • sudo docker pull ${{ secrets.DOCKER_REPO }}/jaetteoli-web 딱 이 부분만 추가했다. 프론트엔드에서 Docker hub에 push를 이미 진행했다고 하고, EC2에 접속하여 pull해서 web 컨테이너를 실행한다.

배포 방식

  • 프론트엔드는 로컬환경에서 개발을 하고 github main 브랜치에 push를 진행한다.

    • 그러면, React 도커 이미지가 Docker Hub에 push되고, 여기까지가 프론트엔드의 워크플로우다.
  • 백엔드에서는 로컬환경에서 개발을 하고 github main 브랜치에 push를 진행한다.

    • server, web, nginx, certbot 이 네 가지 컨테이너가 EC2 인스턴스에 배포되어 실행된다.

결과 확인

Front-end

Back-end

  • 현재 SpringBoot 프로젝트 내에서 /api @Requestmapping에 매치되는 url의 기본에는 "hello world"를 Return하기 때문에 저렇게 뜨는 것이다.

여기까지 전반적인 배포구조를 설계해보았다. 미흡하지만 꽤나 인프라 지식이 조금은 늘어난 것 같다.


향후 계획

1. JPA개발

  • 소셜 로그인, Refresh Token 및 Redis 도입
  • 비즈니스 로직 개발

2. 무중단 배포

  • 수많은 무중단 배포방식이 있지만, 블루-그린 배포를 추후에 진행해보려고 한다.

참고 블로그


읽어주셔서 감사합니다:)

4개의 댓글

comment-user-thumbnail
2023년 7월 21일

아주 유익한 내용이네요!

1개의 답글
comment-user-thumbnail
2024년 1월 23일

글 너무 잘읽었습니다. certbot은 ec2에서 도메인에 따라 발급받고 난 후의 기준으로 이 글을 작성하신거 맞을까요?

1개의 답글