[Full Stack 배포] 개발 및 운영 서버 분리하기

장성준·2024년 1월 25일
1

Full Stack 배포

목록 보기
7/8
post-thumbnail
post-custom-banner

Full Stack 배포 시리즈의 마지막 포스팅입니다.

이번 시간에는 GitHub Action을 사용하여 develop branch에 push하면 개발서버 EC2에 배포되고 main branch에 push하면 운영서버 EC2에 배포 되도록 기존 소스를 수정해보도록 하겠습니다.

전체 배포 과정을 시리즈로 다루고 있습니다.
궁금하신 분들은 여기에서부터 순차적으로 따라와주시길 바랍니다.


구체적인 순서는 다음과 같습니다.
해당 포스팅은 이전 내용에서 부터 이어집니다.

Backend

  1. application.properties 분리 (local, dev, prod)
  2. Dockerfile 분리 (Dockerfile.dev, Dockerfile.prod)
  3. Workflow 분리 (deploy-dev.yml, deploy-prod.yml)
  4. docker-compose.yml 변경

Frontend

  1. env 분리 (env.development, env.production)
  2. Dockerfile 분리 (Dockerfile.dev, Dockerfile.prod)
  3. Workflow 분리 (deploy-dev.yml, deploy-prod.yml)
  4. 테스트

비용을 줄이기 위해 EC2는 2대, RDS는 db명을 분리하여 구성하였습니다.

제가 진행하고있는 팀프로젝트의 경우엔 S3도 활용하고 있는데요.
이것 역시 폴더 명을 분리하여 한대로 유지하도록 구성하였습니다.

시간이 된다면 S3 업로드도 포스팅하도록 하겠습니다.

Backend

application.properties 분리 (local, dev, prod)

스프링부트의 경우 실행 시점에 -Dspring.profiles.active=으로 프로파일 옵션을 줄 수 있습니다.

로컬 환경에서는 h2를 사용하고 개발 및 운영 환경에서는 해당 RDS를 사용하도록 분리해줍시다.

application.properties

spring.profiles.active=local

기본 실행 시 application-local.properties를 사용합니다.

application-local.properties

spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driverClassName=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=

spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop

H2를 DB로 사용한다는 내용입니다.
프로젝트에 맞게 변경하시면 되겠습니다.

다음 파일도 추가해주고 서버 환경에 맞게 설정해 줍시다.

application-dev.properties

application-prod.properties

Dockerfile 분리 (Dockerfile.dev, Dockerfile.prod)

GitHub Action 시 각각의 설정으로 이미지를 빌드하도록 분리하여줍시다.
-Dspring.profiles.active부분만 추가되었습니다.

Dockerfile.dev

# OpenJDK 17을 기반으로 하는 경량화 스프링 부트 이미지
FROM openjdk:17-alpine

# 작업 디렉토리 설정
WORKDIR /app

# JAR 파일을 컨테이너에 복사(jar파일이 하나만 생기도록 설정해줘야 함.)
COPY build/libs/*.jar app.jar

# 환경 변수 설정
ENV SPRINGDOC_SWAGGER_UI_PATH /doc

# 포트 설정
EXPOSE 8080

# 실행 명령어
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=dev","app.jar"]

Dockerfile.prod

# OpenJDK 17을 기반으로 하는 경량화 스프링 부트 이미지
FROM openjdk:17-alpine

# 작업 디렉토리 설정
WORKDIR /app

# JAR 파일을 컨테이너에 복사(jar파일이 하나만 생기도록 설정해줘야 함.)
COPY build/libs/*.jar app.jar

# 환경 변수 설정
ENV SPRINGDOC_SWAGGER_UI_PATH /doc

# 포트 설정
EXPOSE 8080

# 실행 명령어
ENTRYPOINT ["java","-jar","-Dspring.profiles.active=prod","app.jar"]

Workflow 분리 (deploy-dev.yml, deploy-prod.yml)

기존 deply.yml 파일을 분리해줍시다.
각각의 EC2에 ssh로 접속할 수 있도록 GitHub Secret도 추가해줍니다.

deploy-dev.yml

name: 개발서버에 배포

on:
  push:
    branches: [ "develop" ]

jobs:
  build:
    runs-on: ubuntu-latest # 작업이 실행될 환경
    steps:
    - name: 체크아웃
      uses: actions/checkout@v3
    - name: JDK 17 사용
      uses: actions/setup-java@v3
      with:
        java-version: '17'
        distribution: 'temurin'
    - name: Gradle Wrapper 실행 권한 추가
      run: chmod +x gradlew
    - name: application-dev.properties 덮어쓰기
      run: |
        cd ./src/main/resources
        touch ./application-dev.properties
        echo "${{ secrets.SETTING_DEV }}" > ./application-dev.properties
      shell: bash
    - name: Gradle로 빌드(CI)
      run: ./gradlew build
    - name: 도커허브에 로그인
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKER_USER_NAME }}
        password: ${{ secrets.DOCKER_USER_PW }}
    - name: 이미지 빌드
      run: docker build -t ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be-dev -f Dockerfile.dev .
    - name: 도커허브에 이미지 푸시
      run: docker push ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be-dev
    - name: AWS EC2에 ssh 접속 후 배포
      uses: appleboy/ssh-action@master     
      with:
        host: ${{ secrets.AWS_IP_DEV }}
        port: 22
        username: ubuntu
        key: ${{ secrets.AWS_KEY }}
        script: |
          docker pull ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be-dev
          docker-compose up -d

deploy-prod.yml

name: 운영서버에 배포

on:
  push:
    branches: [ "main" ]

jobs:
  build:
    runs-on: ubuntu-latest # 작업이 실행될 환경
    steps:
      - name: 체크아웃
        uses: actions/checkout@v3
      - name: JDK 17 사용
        uses: actions/setup-java@v3
        with:
          java-version: '17'
          distribution: 'temurin'
      - name: Gradle Wrapper 실행 권한 추가
        run: chmod +x gradlew
      - name: application-prod.properties 덮어쓰기
        run: |
          cd ./src/main/resources
          touch ./application-prod.properties
          echo "${{ secrets.SETTING_PROD }}" > ./application-prod.properties
        shell: bash
      - name: Gradle로 빌드(CI)
        run: ./gradlew build
      - name: 도커허브에 로그인
        uses: docker/login-action@v1
        with:
          username: ${{ secrets.DOCKER_USER_NAME }}
          password: ${{ secrets.DOCKER_USER_PW }}
      - name: 이미지 빌드
        run: docker build -t ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be-prod -f Dockerfile.prod .
      - name: 도커허브에 이미지 푸시
        run: docker push ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be-prod
      - name: AWS EC2에 ssh 접속 후 배포
        uses: appleboy/ssh-action@master
        with:
          host: ${{ secrets.AWS_IP_PROD }}
          port: 22
          username: ubuntu
          key: ${{ secrets.AWS_KEY }}
          script: |
            docker pull ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be-prod
            docker-compose up -d

docker-compose.yml 변경

기존 docker-compose.yml에 변경된 이미지 명으로 적용해줍시다.
필자의 경우는 -dev, -prod를 붙여주는 식으로 진행하였습니다.

version: '3'
services:
  backend:
    image: [도커허브 아이디]/[도커허브 이미지명]-be-dev
    ports:
      - "8080:8080"
    networks:
      - network

  frontend:
    image: [도커허브 아이디]/[도커허브 이미지명]-fe-dev
    ports:
      - "80:80"
    depends_on:
      - backend
    networks:
      - network

networks:
  network:

각각의 EC2에 옮겨주시고 프론트쪽 작업을 이어가도록 하겠습니다.

Frontend

env 분리 (env.development, env.production)

리액트로 프로젝트를 만들면 다음과 같이 환경변수를 분리할 수 있습니다.
간단하게 생각하면 다음과 같이 적용된다고 볼 수 있습니다.

  • env.development : npm start 시 적용
  • env.production : npm run build 시 적용

예시

env.development

REACT_APP_API_URL=http://~~
REACT_APP_API_MODE=개발서버

env.production

REACT_APP_API_URL=http://~~
REACT_APP_API_MODE=운영서버

사용

axios.defaults.baseURL = process.env.REACT_APP_API_URL

로컬 환경에서는 개발서버 API에 통신하여 개발을 진행하고 develop branch에 push하면 env.development이 적용되고 main branch에 push하면 env.production이 적용되도록 진행해보도록 하겠습니다.

Dockerfile 분리 (Dockerfile.dev, Dockerfile.prod)

GitHub Action 시 각각의 설정으로 이미지를 빌드하도록 분리하여줍시다.

Dockerfile.dev

# Node.js를 기반으로 하는 리액트 앱 이미지
FROM node:16-alpine as build

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 설치 및 빌드(CI)
COPY package.json .
RUN npm install
COPY . .
COPY .env.development .env.production
RUN npm run build

# Nginx를 기반으로 하는 최종 이미지
FROM nginx:alpine

# Nginx 설정 파일 복사
COPY nginx/nginx.conf /etc/nginx/nginx.conf

# mime.types 파일을 복사
COPY nginx/mime.types /etc/nginx/mime.types

# 빌드된 리액트 앱을 Nginx의 HTML 디렉토리로 복사
COPY --from=build /app/build /usr/share/nginx/html

# 포트 설정
EXPOSE 80

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]

Dockerfile.prod

# Node.js를 기반으로 하는 리액트 앱 이미지
FROM node:16-alpine as build

# 작업 디렉토리 설정
WORKDIR /app

# 의존성 설치 및 빌드(CI)
COPY package.json .
RUN npm install
COPY . .
COPY .env.production .env.production
RUN npm run build

# Nginx를 기반으로 하는 최종 이미지
FROM nginx:alpine

# Nginx 설정 파일 복사
COPY nginx/nginx.conf /etc/nginx/nginx.conf

# 빌드된 리액트 앱을 Nginx의 HTML 디렉토리로 복사
COPY --from=build /app/build /usr/share/nginx/html

# 포트 설정
EXPOSE 80

# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]

Workflow 분리 (deploy-dev.yml, deploy-prod.yml)

deploy-dev.yml

name: 개발서버에 배포

on:
  push:
    branches: [ "develop" ]

jobs:
  deploy:
    runs-on: ubuntu-latest # 작업이 실행될 환경
    steps:
    - name: 체크아웃
      uses: actions/checkout@v3
    - name: 도커허브에 로그인
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKER_USER_NAME }}
        password: ${{ secrets.DOCKER_USER_PW }}
    - name: 이미지 빌드
      run: docker build -t ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe-dev -f Dockerfile.dev .
    - name: 도커허브에 이미지 푸시
      run: docker push ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe-dev
    - name: AWS EC2에 ssh 접속 후 배포
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.AWS_IP_DEV }}
        port: 22
        username: ubuntu
        key: ${{ secrets.AWS_KEY }}
        script: |
          echo "AWS 연결"
          docker pull ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe-dev
          docker-compose up -d

eploy-prod.yml

name: 운영서버에 배포

on:
  push:
    branches: [ "main" ]

jobs:
  deploy:
    runs-on: ubuntu-latest # 작업이 실행될 환경
    steps:
    - name: 체크아웃
      uses: actions/checkout@v3
    - name: 도커허브에 로그인
      uses: docker/login-action@v1
      with:
        username: ${{ secrets.DOCKER_USER_NAME }}
        password: ${{ secrets.DOCKER_USER_PW }}
    - name: 이미지 빌드
      run: docker build -t ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe-prod -f Dockerfile.prod .
    - name: 도커허브에 이미지 푸시
      run: docker push ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe-prod
    - name: AWS EC2에 ssh 접속 후 배포
      uses: appleboy/ssh-action@master
      with:
        host: ${{ secrets.AWS_IP_PROD }}
        port: 22
        username: ubuntu
        key: ${{ secrets.AWS_KEY }}
        script: |
          echo "AWS 연결"
          docker pull ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe-prod
          docker-compose up -d

테스트

깃허브 시크릿에 제대로 설정을 해주었는지 먼저 확인해주세요.
이제 각각의 레포지토리를 깃허브에 푸시해봅시다.

처음에는 docker-compose.yml의 depends_on: 부분 때문에 안켜져 있을 수 있으니 확인차 해줍시다. (자세히는 설명 안하겠습니다.)

docker-compose up -d

로그를 보고 싶다면 다음 명령어도 기억해 두세요.

docker-compose logs

마무리

두개의 EC2 인스턴스에 각각 배포가 되시나요?

COPY .env.development .env.production

필자의 경우 이 부분에 시간을 많이 빼았겼는데요.

기존에 COPY .env.development .env.development로 넣었더니 실제 개발서버에서 운영서버 환경변수가 적용되어 이유를 찾지 못해 한참을 삽질했었습니다.

결국 스택오버플로우의 도움을 받아 해결할 수 있었지요.

빌드 시에는 production을 사용하기 때문에 .env.development에 있는 것을 적용하기 위해서는 .env.production 파일명으로 넣어줘야 했습니다.

배포 과정은 쉬운 듯 하면서도 어딘가 하나 삐끗하면 안되는 부분이 참 힘들었던 것 같습니다.

지금까지 따라와 주셔서 감사합니다.

저와 같이 React + Springboot 기반으로 팀 프로젝트를 진행하고 배포까지 하시려는 분들에게 도움이 되었기를 바랍니다.

감사합니다.

profile
Backend Engineer
post-custom-banner

0개의 댓글