서론
주로 필자는 소형 프로젝트를 배포할 때, React 프로젝트를 빌드한 후, Spring 정적 자원으로 넣은 후 빌드하여 생성된 jar 파일을 배포한다. 하지만 이러한 방식은 프로젝트에 수정 사항이 생길 때마다 같은 작업을 해주어야 한다는 단점이 있다. 따라서 최근 필자는 주로 도커와 Github Actions를 사용한 자동화 배포 방식을 사용하는데, 이에 대해 소개해보고자 한다.
도커(Docker)란?
하나의 백엔드 프로젝트가 있다고 생각해 보자. 이 프로젝트는 Spring Boot와 mongoDB, Redis를 사용한다. 따라서 개발자는 이러한 환경을 모두 갖춘 상태에서 개발을 마친 후, 배포할 서버에도 똑같은 환경을 갖춘 후 배포하여야 한다. 하지만 가상화 컨테이너인 도커를 활용한다면, 단지 프로젝트와 그 환경을 담은 이미지 하나만 배포하면 어디서든지 같은 환경으로 실행할 수 있다.
도커 컴포즈(Docker-Compose)란?
만약 프로젝트에서 Node.js, Java, mongoDB, Redis와 같이 여러 기술을 사용한다면, 4개의 이미지를 활용하여 컨테이너를 만들어야 한다. 만약 프로젝트가 수정되었다면, 다시 4개의 이미지를 불러와 컨테이너를 만들어야 한다. 이러한 과정은 매우 번거롭기 때문에, 여러 도커 이미지 및 컨테이너를 단일 파일로서 관리할 수 있도록 도커 컴포즈가 나오게 되었다.
실습을 해보자!
간단한 익명 게시판 프로젝트를 만들어보자. 이 프로젝트는 프론트엔드로 React JS를, 백엔드로 Spring Boot를, 데이터베이스로 mongoDB를 사용한다.
데이터베이서 컨테이너
백엔드 서버에서 데이터베이스 서버인 mongoDB를 사용하기 때문에, Docker에서 mongoDB 컨테이너를 만들어야 한다.
docker pull mongodb/mongodb-community-server:latest
docker run --name mongodb -d mongodb/mongodb-community-server:latest
백엔드 컨테이너
백엔드는 간단하게 POST /api/board로 요청이 들어오면 모든 게시판을 반환하고, PUT /api/board로 { title, description } body와 함께 요청이 들어오면 게시판을 추가하는 간단한 서버이다.
Spring에서 mongoDB 설정할 땐, spring.data.mongodb.host를 mongoDB의 컨테이너 이름(여기서는 mongodb)로 설정하면 연결할 수 있다. (이 외에도 도커 컨테이너 사이 모든 네트워크 연결은 컨테이너 이름으로 연결할 수 있다.)
server:
port: 8080
spring:
data:
mongodb:
host: mongodb
port: 27017
이 프로젝트 상위 폴더에 Dockerfile을 만들어 이미지를 빌드한 후, 컨테이너를 실행시켰다.
FROM openjdk:21-jdk
WORKDIR /app
COPY build/libs/app.jar /app/app.jar
CMD ["java", "-jar", "app.jar"]
docker build --tag backend .
docker run --name backend --link mongodb -d backend
프론트엔드 컨테이너
프론트엔드를 개발할 때, 백엔드 API가 필요하기 때문에 임시로 8080 포트를 받는 backend-dev 컨테이너를 만들어줬다.
docker run --name backend --link mongodb --port 8080:8080 -d backend
프론트엔드 내 Proxy를 사용하여 CORS를 우회한 다음, axios를 통해 API 요청을 하였고, 간단하게 새 게시글 작성 컴포넌트와 기존 게시글을 모두 볼 수 있는 컴포넌트를 만들었다.
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
},
}
}
})
프론트엔드 개발이 끝났으니, 아까 임시로 생성했던 backend-dev 컨테이너는 삭제시켰다.
이 프로젝트 상위 폴더에 Dockerfile을 만들어 이미지를 빌드한 후, 컨테이너를 실행시켰다. 이때, 웹 엔진으로 nginx를 사용했다.
FROM nginx:alpine
COPY ./dist /usr/share/nginx/html
COPY nginx.conf /etc/nginx/conf.d/default.conf
CMD ["nginx", "-g", "daemon off;"]
server {
listen 8080;
server_name localhost;
location / {
root /usr/share/nginx/html;
try_files $uri /index.html;
}
}
docker build -t frontend .
docker run --name frontend -d frontend
웹서버 컨테이너
이제 프론트엔드와 백엔드 개발이 끝났으니, 이를 직접 외부로 호스팅 하는 nginx 컨테이너가 필요하다. 방금 위에서 nginx를 사용하지 않았느냐?라는 의문이 생길 수 있지만, 위 프론트엔드 nginx 컨테이너는 React의 html을 호스팅 하기 위한 컨테이너였고, 이제는 외부에서 오는 요청을 적절히 프론트엔드와 백엔드로 나눌 수 있도록 리버스 프록시 서버를 구축하는 것이다.
아래와 같이 이미지를 빌드한 후, 컨테이너를 실행시켰다.
FROM nginx:alpine
COPY nginx.conf /etc/nginx/nginx.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
user nginx;
worker_processes 1;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
sendfile on;
keepalive_timeout 65;
upstream frontend {
server frontend:8080;
}
upstream backend {
server backend:8080;
}
server {
listen 80;
location /api/ {
proxy_pass http://backend;
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;
}
location / {
proxy_pass http://frontend;
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;
}
}
}
docker build -t web .
docker run --name web --link frontend --link backend -p 80:80 -d web
이제 웹 브라우저에서 접속해보면 잘 작동하는 것을 볼 수 있다.
Docker Compose화 시키기
위 Web Repository를 Project-Web과 같이 팔 수도 있지만, 필자의 최종 목표는 Docker-Compose를 사용하는 것이기 때문에, Project-Total을 만든 후, 그 안에 Git Submodule로서 프론트엔드와 백엔드를 추가할 것이다.
일단 하나의 빈 Repository를 만든 후, 내 로컬 환경에 클론받았다. 이제 프론트엔드와 백엔드 프로젝트를 Submodule로 등록하겠다.
git submodule add git@github.com:son-daehyeon/CICD-Practice-Frontend.git frontend
git submodule add git@github.com:son-daehyeon/CICD-Practice-Backend.git backend
그리고 docker-compose.yml을 만들어줬다. 필자는 맥 환경에서도 해당 이미지를 사용해야 하기에 platforms 옵션을 통해 멀티 플랫폼 빌드로 진행하였다.
services:
mongodb:
image: mongodb/mongodb-community-server:latest
container_name: mongodb
volumes:
- mongodb_data:/data/db
networks:
- cicd-practice
backend:
image: ioloolo/cicd-practice-backend:latest
build:
context: backend
platforms:
- "linux/amd64"
- "linux/arm64"
container_name: backend
depends_on:
- mongodb
links:
- mongodb
networks:
- cicd-practice
frontend:
image: ioloolo/cicd-practice-frontend:latest
build:
context: frontend
platforms:
- "linux/amd64"
- "linux/arm64"
container_name: frontend
networks:
- cicd-practice
web:
image: ioloolo/cicd-practice-web:latest
build:
context: web
platforms:
- "linux/amd64"
- "linux/arm64"
container_name: web
depends_on:
- frontend
- backend
links:
- frontend
- backend
networks:
- cicd-practice
ports:
- "80:80"
volumes:
mongodb_data:
networks:
cicd-practice:
이제 터미널에서 아래와 같은 명령어를 실행하면, 4개의 컨테이너가 모두 빌드고 푸시할 수 있고, 한번에 컨테이너를 실행/중단할 수 있다.
docker-compose build // 이미지 빌드
docker-compose push // 이미지 푸시
docker-compose down // 컨테이너 중단
docker-compose up -d // 컨테이너 실행 (데몬)
Submodule 동기화 자동화하기
Git의 Submodule은 Submodule에 새로운 commit이 발생하더라도, 부모가 자식을 가리키는 HEAD는 변하지 않는다. 따라서 Github Actions로 이를 자동으로 동기화되도록 설정해 보자.
백엔드 Repository의 Github Actions을 아래와 같이 추가했다.
name: Sync Submodule
on:
push:
branches:
- main
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
with:
repository: son-daehyeon/CICD-Practice-Total
token: ${{ secrets.PRIVATE_TOKEN_GITHUB }}
submodules: true
- name: Pull & update submodules recursively
run: |
git submodule update --init --recursive
git submodule update --recursive --remote
- name: Commit
run: |
git config user.email "actions@github.com"
git config user.name "GitHub Actions - update submodules"
git add --all
git commit -m "Sync: Backend Module" || echo "No changes to commit"
git push
그리고, 부모 Repository를 직접 다루어야 하기에, Github Token이 필요한데, 이를 비밀 변수로 설정해 주었다. (Github Token 발급)
앞으로 Backend 프로젝트를 수정하여 main branch에 commit이 될 때마다, 부모(Total) Repository에서도 실시간으로 동기화가 된다.
같은 방식으로 Frontend에도 추가해 주자.
EC2 배포 자동화하기
이제 부모(Total) Repository에 변화가 생겼을 때(백엔드, 프론트엔드 Repository 수정 포함), Github Actions를 사용하여 Docker Compose를 빌드한 이미지를 Docker hub에 Push 한 후, EC2(혹은 다른 VPS 서버)에 ssh로 접속한 다음 이미지를 내려받아 실행시키는 작업을 만들어줄 것이다.
아래처럼 Github Actions를 만들어주자.
name: Build and Deploy
on:
push:
branches:
- main
jobs:
build-frontend:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install Frontend Dependencies
run: |
cd frontend
yarn install
- name: Build Frontend
run: |
cd frontend
yarn build
- name: Build and Push Frontend Docker images
uses: docker/build-push-action@v6
with:
context: ./frontend
push: true
tags: ioloolo/cicd-practice-frontend:latest
build-backend:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set up JDK
uses: actions/setup-java@v4
with:
distribution: 'adopt'
java-version: '21'
- name: Build backend
run: |
cd backend
./gradlew build
- name: Build and Push Backend Docker images
uses: docker/build-push-action@v6
with:
context: ./backend
push: true
tags: ioloolo/cicd-practice-backend:latest
build-web:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v4
with:
submodules: recursive
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Build and Push Web Docker images
uses: docker/build-push-action@v6
with:
context: ./web
push: true
tags: ioloolo/cicd-practice-web:latest
deploy:
runs-on: ubuntu-latest
needs: [build-frontend, build-backend, build-web]
steps:
- name: SSH into server and deploy
uses: appleboy/ssh-action@v1.0.3
with:
host: ${{ secrets.SERVER_HOST }}
port: ${{ secrets.SERVER_PORT }}
username: ${{ secrets.SERVER_USERNAME }}
password: ${{ secrets.SERVER_PASSWORD }}
script: |
cd ~/project
git pull
/usr/local/bin/docker-compose pull
/usr/local/bin/docker-compose down
/usr/local/bin/docker-compose up -d
이제 프론트엔드, 백엔드 프로젝트 수정이나, 루트 프로젝트를 수정하는 모든 경우, 자동으로 Github Actions가 EC2로 도커를 활용하여 배포된다.
'Other' 카테고리의 다른 글
[HTTP] 실시간으로 서버로부터 데이터를 받아보자. (0) | 2024.05.03 |
---|