Full Stack 배포 시리즈의 마지막 포스팅입니다.
이번 시간에는 GitHub Action을 사용하여 develop branch에 push하면 개발서버 EC2에 배포되고 main branch에 push하면 운영서버 EC2에 배포 되도록 기존 소스를 수정해보도록 하겠습니다.
전체 배포 과정을 시리즈로 다루고 있습니다.
궁금하신 분들은 여기에서부터 순차적으로 따라와주시길 바랍니다.
구체적인 순서는 다음과 같습니다.
해당 포스팅은 이전 내용에서 부터 이어집니다.
비용을 줄이기 위해 EC2는 2대, RDS는 db명을 분리하여 구성하였습니다.
제가 진행하고있는 팀프로젝트의 경우엔 S3도 활용하고 있는데요.
이것 역시 폴더 명을 분리하여 한대로 유지하도록 구성하였습니다.
시간이 된다면 S3 업로드도 포스팅하도록 하겠습니다.
스프링부트의 경우 실행 시점에 -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
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"]
기존 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에 변경된 이미지 명으로 적용해줍시다.
필자의 경우는 -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에 옮겨주시고 프론트쪽 작업을 이어가도록 하겠습니다.
리액트로 프로젝트를 만들면 다음과 같이 환경변수를 분리할 수 있습니다.
간단하게 생각하면 다음과 같이 적용된다고 볼 수 있습니다.
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이 적용되도록 진행해보도록 하겠습니다.
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;"]
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 기반으로 팀 프로젝트를 진행하고 배포까지 하시려는 분들에게 도움이 되었기를 바랍니다.
감사합니다.