https://nymagicshop16.tistory.com/138
[CI/CD] React.js + Spring Boot 웹 서비스 Docker, Github Action으로 EC2에 자동 배포하기 - 1. Nginx
이번 시간에는 Docker와 Github Action을 사용하여 React.js와 Spring Boot 웹 서비스를 CI/CD로 자동 배포하는 파이프라인을 다뤄보도록 하겠습니다.각 단계에서 발생한 트러블슈팅 내용들을 보다 상세히
nymagicshop16.tistory.com
지난편에서 이어집니다.
시간이 없어서 일단 의식의 흐름으로 블로그를 작성해봅니다 ㅎㅎㅎ.. 나중에 더 가독성있게 수정할게요
최종 프로젝트 아키텍처
아키텍처 설명
하나의 EC2 안에 React 서버와 Springboot 서버를 각각 도커 이미지화 하며 관리하였습니다.
이전 프로젝트에서 React와 Springboot를 서로 다른 도메인에 배포했었는데 CORS 에러로 무진장 힘들었어서 이번에는 처음부터 무조건 같은 서버에다가 배포했습니다.
ML 서버는 Flask를 사용하였고, 생성형 이미지 모델이어서 GPU를 사용할 수 있는 EC2를 선택해야 했습니다. 그래서 서버 비용이 15만원 넘게 나왔다는 것은 안비밀.... AWS 비용 지원해주세요...
RAG 기반의 챗봇을 구현하는 데에 Pinecone vectorDB를 사용하였고, GPT API를 이용하여 챗봇 답변을 생성하였습니다.
Docker
문제
React와 SpringBoot를 동일한 EC2 서버에 구성하면서 CI/CD 파이프라인을 구축할 때, 각 도커 이미지를 새로 빌드하고 배포하는 과정에서 서로의 컨테이너를 방해하지 않도록 설계하는 부분이 어려웠습니다.
프론트엔드와 백엔드는 각각 독립된 GitHub 레포지토리를 사용하고 있었기 때문에, GitHub Actions에서 별도의 workflow 파일을 작성해야 했습니다. 하지만 두 애플리케이션 모두 같은 서버에 배포되기 때문에, 서로 간섭 없이 독립적으로 빌드·배포되도록 CI/CD 파이프라인을 설계하고 실행하는 데 많은 고민이 필요했습니다.
해결 방법
Docker-compose.yml
version: '3'
services:
backend:
container_name: backend
image: snoony/starlight-be
ports:
- "8080:8080"
networks:
- network
frontend:
container_name : frontend
image: snoony/starlight-fe
ports:
- "3000:3000"
depends_on:
- backend
networks:
- network
networks:
network:
여기서 중요한 점은 container_name으로 백엔드와 프론트엔드의 컨테이너 이름을 각각 명시해 주어야 합니다.
또한 백엔드와 프론트엔드 컨테이너가 같은 네트워크를 공유해야 했습니다. 원래 backend와 frontend 각각 다른 docker compose 파일을 두어서 따로 배포시키게 하려고 했는데 이 이유로 다시 하나의 docker compose 파일에 넣었습니다.
-> 이 부분은 저도 이해가 잘 안되서 더 알아보겠습니다
FE Github Actions workflow
name: React 배포
on:
push:
branches: [ "deploy" ]
jobs:
deploy:
runs-on: ubuntu-latest # 작업이 실행될 환경
steps:
- name: 체크아웃
uses: actions/checkout@v3
- name: make .env
run: |
touch .env
echo "${{ secrets.SECRET_ENV }}" > .env
- 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 .
- name: 도커허브에 이미지 푸시
run: docker push ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe
- name: AWS EC2에 ssh 접속 후 배포
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AWS_IP }}
port: 22
username: ubuntu
key: ${{ secrets.AWS_KEY }}
script: |
echo "AWS 연결"
# 기존 프론트엔드 컨테이너 중지 및 삭제
docker stop frontend || true
docker rm frontend || true
# 기존 프론트엔드 이미지 삭제
docker rmi ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe || true
docker pull ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-fe
# docker-compose에서 frontend 컨테이너만 재시작
docker-compose up -d --force-recreate --build frontend
# 상태 확인
docker ps -a
docker logs frontend
Docker를 이용하여 배포되는 과정은 다음과 같습니다.
1. 도커 이미지 빌드
- secrets에 지정해준 도커 username, image name으로 빌드함
2. 도커허브에 이미지 푸시
3. AWS EC2에 ssh 접속 후 배포
재배포 과정
1. 기존 컨테이너 중지 및 삭제
2. 기존 이미지 삭제
3. docker-compose에서 frontend 컨테이너만 재시작
=> 이 부분이 너무 중요했다 왜냐면 재배포 시 frontend 컨테이너만 재시작하도록 명령을 주지 않으면 backend 컨테이너가 계속 같이 죽어버렸기 때문 ..
BE Github Actions workflow
name: starlight BE 배포
on:
push:
branches: [ "main" ]
jobs:
deploy:
runs-on: ubuntu-latest # 작업이 실행될 환경
steps:
- name: 체크아웃
uses: actions/checkout@v3
- name: application-private.properties 덮어쓰기
run: |
echo ${{ secrets.PROPERTIES }} | base64 --decode
echo ${{ secrets.PROPERTIES }}| base64 --decode > ./src/main/resources/application-private.properties
cat ./src/main/resources/application-private.properties
touch ./src/main/resources/application-private.properties
shell: bash
- name: application-private.properties에 추가설정
run : |
echo "" >> ./src/main/resources/application-private.properties
echo -e "\nkakao.redirect.uri=http://${{ secrets.AWS_API }}:3000/api/auth/kakao/callback" >> ./src/main/resources/application-private.properties
cat ./src/main/resources/application-private.properties
- name: JDK 17 사용
uses: actions/setup-java@v3
with:
java-version: '17'
distribution: 'temurin'
- name: Gradle Wrapper 실행 권한 추가
run: chmod +x gradlew
- 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 .
- name: 도커허브에 이미지 푸시
run: docker push ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be
- name: AWS EC2에 ssh 접속 후 배포
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.AWS_IP }}
port: 22
username: ubuntu
key: ${{ secrets.AWS_KEY }}
script: |
docker stop backend || true
docker rm backend || true
# 기존 백엔드 이미지 삭제
docker rmi ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be || true
docker pull ${{ secrets.DOCKER_USER_NAME }}/${{ secrets.DOCKER_IMAGE_NAME }}-be
# 백엔드 컨테이너만 재시작
echo "🔄 백엔드 컨테이너 재시작"
docker-compose up -d --force-recreate --build backend
# 상태 확인 및 로그 출력
echo "✅ 상태 확인"
docker ps -a
docker logs backend
frontend 이미지 배포와 거의 동일합니다.
개선 방안
점점 코드가 늘어날수록 이미지 빌드 시간이 너무 오래걸려서 프로젝트 완성단계에서는 프론트엔드 배포가 거의 3분이 걸렸다..
이 프로젝트에서는 인프라와 배포 속도 개선보다는 일단 기능 구현이 급해서 간단하게 구성해본 배포 파이프라인이었다
앞으로 프로젝트 디벨롭을 하면서 도메인도 붙이고 HTTPS로 다시 배포해서 Docker 기반의 더욱 안정적인 환경을 만들어 보겠다!
