JavaScript
React
ExpressJS
Mysql
Python
Cron
Docker
NginX
GitHub Actions
AWS EC2
Naver Open API
Slakc API
한국투자증권 API
흐름은 이러하다.
stockey/ # 메인 프로젝트 폴더
│
├── Backend/ # 백엔드 관련 코드 및 Dockerfile
│ ├── Dockerfile # 백엔드 Dockerfile
│ ├── .github/ # 백엔드 레포지토리의 CI/CD 설정
│ │ └── workflows/ # 백엔드 GitHub Actions 워크플로우 설정
│ │ └── deploy.yml # 백엔드 배포 워크플로우
│ └── ... # 기타 백엔드 관련 파일들 (소스 코드 등)
│
├── Frontend/ # 프론트엔드 관련 코드 및 Dockerfile
├── nginx.conf # Nginx 웹 서버의 설정 파일
├── Dockerfile # 프론트엔드 Dockerfile
├── .github/ # 프론트엔드 레포지토리의 CI/CD 설정
│ └── workflows/ # 프론트엔드 GitHub Actions 워크플로우 설정
│ └── deploy.yml # 프론트엔드 배포 워크플로우
└── ... # 기타 프론트엔드 관련 파일들 (소스 코드 등)
Docker
+ GitHub Actions
+ NginX
를 통해 CI/CD를 진행하기위한 기본적인 프로젝트 구조이다.
백엔드와 프론트엔드를 각각의 컨테이너에서 실행시키기 때문에 서로의 network를 연결해주는 과정이 필요하다.
Docker Compose
- 여러 개의 서비스가 필요하거나, 환경 설정을 일관되게 유지할 수 있다.
Dockerfile: Docker 이미지를 생성하기 위한 설정 파일
Docker Compose : 여러 개의 컨테이너를 하나의 설정 파일(docker-compose.yml)로 정의
name: CI/CD Pipeline for Backend
on:
push:
branches:
- main # 배포할 브랜치
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
# 코드 체크아웃
- name: Checkout Code
uses: actions/checkout@v3
# Docker와 Docker Compose 설치 (필요한 경우에만 설치)
- name: Set up Docker
run: |
# Docker 설치 (GitHub Actions의 ubuntu-latest 환경에는 기본적으로 Docker가 설치되어 있음)
if ! command -v docker &> /dev/null; then
echo "Docker가 설치되어 있지 않습니다. 설치 중..."
sudo apt-get update
sudo apt-get install -y docker.io
sudo systemctl start docker
sudo systemctl enable docker
else
echo "Docker가 이미 설치되어 있습니다."
fi
# Docker Hub 로그인
- name: Log in to Docker Hub
uses: docker/login-action@v2
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
# Docker 이미지를 빌드하고 푸시
- name: Build and Push Docker Image
run: |
# Docker 이미지를 빌드
docker build -t ${{ secrets.DOCKER_USERNAME }}/컨테이너명:VERSION .
# Docker 이미지를 Docker Hub에 푸시
docker push ${{ secrets.DOCKER_USERNAME }}/컨테이너명:VERSION
# 서버에 배포 명령어 실행
- name: Deploy to Server
uses: appleboy/ssh-action@v0.1.7
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/ubuntu
# 서버에서 Docker 이미지를 pull하고 실행
docker pull ${{ secrets.DOCKER_USERNAME }}/컨테이너명:VERSION
# app-network 네트워크가 없으면 생성
docker network inspect 네트워크명 || docker network create 네트워크명
# 기존에 실행 중인 컨테이너가 있으면 중지하고 제거
docker stop stockey-express || true
docker rm stockey-express || true
# 새로운 컨테이너 실행 (app-network 네트워크와 연결)
docker run -d --name 컨테이너명 --network 네트워크명 -e DB_HOST=${{ secrets.SERVER_HOST }} -e DB_USER=${{ secrets.DB_USER }} -e DB_PASSWORD=${{ secrets.DB_PASSWORD }} -e DB_DATABASE=${{ secrets.DB_DATABASE }} -e APP_KEY=${{ secrets.APP_KEY }} -e APP_SECRET=${{ secrets.APP_SECRET }} -p 3000:3000 ${{ secrets.DOCKER_USERNAME }}/stockey-express:latest
secrets.XXX : GitHub Secrets에 저장된 데이터
-e : 환경 변수를 설정하는 Docker 명령어
--network : 컨테이너의 네트워크를 설정을 정의하는 Docker 명령어
NginX는 정적 파일을 서빙(serve)할 수 있다.
React프로젝트를 Build하면 정적 파일(html, css, js)들이 생성된다.
빌드를 통해 생성된 산출물들을 NginX에 정적 파일로 집어넣어 실행시키게 된다.
# 1단계: 빌드 단계 (node:22 사용)
FROM node:22 AS build
# 작업 디렉토리 설정
WORKDIR /app
# 의존성 파일 복사
COPY package*.json ./
# 의존성 설치
RUN npm install
# 소스 코드 복사
COPY . .
# 빌드 실행
RUN npm run build
# 2단계: Nginx로 배포 (경량화된 Nginx 이미지를 사용)
FROM nginx:alpine
# Dockerfile과 같은 경로에 있는 nginx.conf 파일을 컨테이너 내부로 복사
COPY nginx.conf /etc/nginx/nginx.conf
# 빌드된 파일만 Nginx로 복사
COPY --from=build /app/dist /usr/share/nginx/html
# 80 포트 개방
EXPOSE 80
# Nginx 실행
CMD ["nginx", "-g", "daemon off;"]
# 필수 이벤트 블록
events {}
http {
# MIME 타입 설정 파일 포함
# 확장자 자동 매핑?
include /etc/nginx/mime.types;
# MIME 타입을 알 수 없는 파일에 대해 기본적으로 사용될 MIME 타입을 지정
# 이진 데이터의 기본 MIME 타입
default_type application/octet-stream;
server {
listen 80;
# 서버 이름 설정
server_name localhost;
# 정적 파일의 기본 경로 설정
root /usr/share/nginx/html;
# SPA를 위한 기본 라우팅 설정
location / {
try_files $uri /index.html;
}
# 백엔드 (Express) API 요청 처리
location /api/ {
proxy_pass http://컨테이너명:3000; # Express 백엔드 컨테이너로 요청 전달
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;
}
}
}
nginx.conf파일을 정의해야한다.
기본 경로로 오는 요청들은 Nginx에 집어넣은 React 정적 파일로 라우팅 해야하고
/api/로 오는 요청들은 백엔드로 보내주어 처리해야한다.
==>Reverse Proxy
# 백엔드 (Express) API 요청 처리
location /api/ {
proxy_pass http://컨테이너명:3000/; # Express 백엔드 컨테이너로 요청 전달
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 /api/ {
proxy_pass http://컨테이너명:3000; # Express 백엔드 컨테이너로 요청 전달
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;
}
둘의 차이가 보이시는가??
1. proxy_pass http://컨테이너명:3000/
2. proxy_pass http://컨테이너명:3000
뒤에 슬래시('/')하나 차이지만 이는 다르게 처리된다.
1번은 http://컨테이너명:3000/ 로 요청이 처리되고
2번은 http://컨테이너명:3000/api/ 로 요청이 처리된다.
이는 NignX의 경로 처리방식 때문이라고 한다.
'
'
'
졸업작품 프로젝트를 하면서 직접 배포하는 방식이 비효율적이고 생산성이 좋지않다고 느꼈다.
CI / CD를 처음부터 끝까지 혼자 구현해보며 많은 어려움이 있었지만, 관련 지식이 한층 더 올라간 기분이다.
아직 프로젝트 마무리가 일주일 남았다. 화이팅 😬
// ChattingPage.jsx
// 소켓 백엔드와 연결
export const socket = io(`${import.meta.env.VITE_SERVER_HOST}`, {
transports: ['websocket'],
});
// nginx.conf 에 추가
# WebSocket 요청 프록시
location /socket.io/ {
proxy_pass http://stockey-express:3000; # Express 서버로 전달
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "Upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
// www (express.js config file)
const io = new Server(server, {
cors: {
// origin: [process.env.CLIENT_URL, "http://localhost:5173"],
origin: ["서버 IP주소"],
methods: ["GET", "POST", "DELETE", "PUT"],
credentials: true,
},
});
socketHandler(io);
Front의 io객체의 기본 path는/socket.io/
Back의 io객체도 기본 path는 /socket.io/
고로 둘이 경로를 맞춰줄 필요없음.
→ 서버의 `${import.meta.env.VITE_SERVER_HOST}` 는 `/` 이다.
→ `/`로 설정하면 `/socket.io/` 로 요청을 보내고 Nginx의 설정을 통해 백엔드로 `/socket.io/`로 요청을 보냄
→ 서버의 www파일에서 Server객체를 통해 기본 경로인 `/socket.io/` 로 들어온 경로를 소켓을 열게되고 성공적으로 연결됨.
개발, 서버를 고려하여 연결 URL을 환경변수로 설정하였다.
VITE에서는 프로젝트가 빌드되는 시점에만 환경변수를 지정할 수 있다. 고로 빌드되기 전 환경변수를 설정해주어야함.
React와 ExpressJS를 각각의 컨테이너에서 실행시키고 있다면,
하나의 네트워크로 묶었다고해서 localhost:3000과 localhost:5173로 서로 접근할 수 있는 것이 아니다.
각각의 localhost는 각각 컨테이너의 가르키고 있기때문에 컨테이너의 이름으로 접근해야한다.