우선, 이번에 디자이너 1명과 프론트엔드 2명이 들어왔기에 원할한 개발을 위한 `dev`서버가 필요했다.
그렇기에 이번 포스팅에서는 `StudyWithMe`의 `dev` 서버의 무중단 배포에 관해 포스팅 하려고 한다.
무중단 CI/CD 아키텍처
설명하기에 앞서 구축한 `CI/CD` 아키텍처는 다음과 같다.
미니PC 환경
`StudyWithMe` 서버는 현재 같이 작업하시는 백엔드분의 미니PC에서 동작하고 있다.
현재 미니PC 서버의 동작 컴포넌트들
- 80포트 사용 다용도 NGINX
- Portainer(웹 UI 기반 컨테이너 관리)
- 내가 쓰는 건 아니고 다른 백엔드 분이 사용하신다.
- Private Docker Registry(프라이빗 도커 레지스트리)
- Grafana
- Prometheus
가 존재한다.
위에서 중점적으로 보면 좋은 컴포넌트는
`80포트 사용 NGINX`와 `Private Docker Registry`이다.
Private Docker Registry
먼저, `Private Docker Registry`와 관련해서 설명하자면,
우리가 그냥 `Docker Hub`에 이미지를 올리면, 우선 `application.yml`이 그냥 공개된 것이나 다름없다.
(이는 현재 깃 서브모듈을 사용하는 이유가 사라진다.)
그리고, `application.yml`이 아니더라도 이런저런 민감 정보들이 공개될 수 있다.
그렇기에 미니PC도 있겠다. `Private Docker Registry`를 호스팅했다.
80포트 사용 NGINX
보통 배포를 하는 경우 `AWS` 혹은 `GCE`에서 배포를 한다.
이 경우 보통 `80포트`를 `NGINX`와 연계해 `리버스 프록시` 역할로 사용한다.
그러나, `StudyWithMe`에서는 이미 미니PC를 사용중인 백엔드분께서
다용도(여러 UI 툴)로 사용하고 있으시기에 `80포트`를 사용할 수 없었다.
그래서, 나는 `82포트`를 사용해 구성했다.
구성
1. Github Actions Workflow
추후, `Jenkins`로 변경할 수 있지만, 우선은 프론트분들에게 빨리 `dev` 서버를 제공해드리기 위해
`Github Actions`를 사용했다.
"백문이 불여일견" `Workflow` 코드를 보도록 하자.
name: SWM CI/CD
on:
workflow_dispatch:
push:
branches:
- 'dev'
jobs:
build-and-push:
runs-on: ubuntu-latest
steps:
- name: Checkout Repository
uses: actions/checkout@v3
with:
submodules: false
- name: Configure Git and Update Submodules
run: |
git config --global url."https://${{ secrets.SWM_SUBMODULE_TOKEN }}@github.com/".insteadOf "https://github.com/"
git submodule update --init --recursive swm-backend-secret
- name: Cache Gradle dependencies
uses: actions/cache@v2
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}
restore-keys: |
gradle-
- name: Set up JDK 21
uses: actions/setup-java@v2
with:
java-version: '21'
distribution: 'temurin'
- name: Grant execute permission for gradlew
run: chmod +x gradlew
- name: Project Build
run: ./gradlew clean build -x test
shell: bash
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: {Registry URL}
username: ${{ secrets.BREAKTIME_REGISTRY_USERNAME }}
password: ${{ secrets.BREAKTIME_REGISTRY_PASSWORD }}
- name: Docker Image Build
run: docker build -t {도커 이미지} .
- name: Docker Image Push
run: docker push {도커 이미지}
- name: SWM 서버 접속
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.SSH_HOST }}
username: ${{ secrets.SSH_USERNAME }}
port: ${{ secrets.SSH_PORT }}
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd study_with_me/scripts
./deploy.sh
나름(?) 특별한 부분을 말하자면
1. `workflow-dispatch`를 설정해서 수동으로 `POST Request` 요청 시 Event를 발생시킬 수 있도록 했다.
2. 앞서 언급한 것처럼 `Private Docker Registry`를 사용하기에
`docker/login-action`에서 `registry` 경로를 등록해주었다.
3. `ssh-action` 사용 시 현재 `StudyWithMe`는 `SSH` 포트인 `22번 포트` 대신에
다른 포트를 사용하고 있기에 설정해주었다.
Github Workflow의 To do List
- 현재, `./gradlew clean build -x test`로 되어있는데 추후 테스트 코드를 마저 작성한 뒤 테스트 코드 검사도 수행할 예정이다.
- 이때, 또 추가할 사항은 다음과 같다.
- 테스트 결과를 PR에 코멘트로 등록
- 테스트 실패 시 Check 코멘트 등록
- 빌드 실패 시 Slack 혹은 Discord에 알람
- 이때, 또 추가할 사항은 다음과 같다.
- Workflows 성공/실패 시 Discord에 알람
등을 추가로 하려고 한다.
2. docker-compose-dev.yaml
`StudyWithMe` 백엔드 서버의 `green`, `blue` 상태값의 컨테이너를 생성하기 위한 `YAML` 파일이다.
version: "3.9" # Docker Compose 파일 버전
services:
green:
image: registry.breakti.me/study-with-me:1.0
container_name: green
environment:
SPRING_PROFILES_ACTIVE: dev # Spring Boot 활성화 프로파일 설정
ports:
- "8080:8080" # 호스트의 8080 포트를 컨테이너의 8080 포트와 매핑
networks:
- home_network
blue:
image: registry.breakti.me/study-with-me:1.0
container_name: blue
environment:
SPRING_PROFILES_ACTIVE: dev # Spring Boot 활성화 프로파일 설정
ports:
- "8081:8080" # 호스트의 8081 포트를 컨테이너의 8080 포트와 매핑
networks:
- home_network
networks:
home_network:
external: true # 외부 네트워크 사용
내용은 단순하다.
그냥 같은 이미지를 공유하고 있는 `green`, `blue`에 관한 서비스를 작성해주고
후술할 `deploy.sh`에서 사용한다.
3. deploy.sh
`deploy.sh`는 위의 사진에서 볼 수 있는 것처럼 여러 역할을 한다.
deploy.sh
#!/bin/bash
IS_GREEN=$(docker ps | grep green) # 현재 실행 중인 App이 green인지 확인합니다.
if [ -z "$IS_GREEN" ]; then # green이 없다면 현재는 blue가 실행 중임
echo "### BLUE => GREEN ###"
echo "1. get green image"
docker-compose -f ../docker-compose-dev.yaml pull green # green 이미지 다운로드
echo "2. green container up"
docker-compose -f ../docker-compose-dev.yaml up -d green # green 컨테이너 실행
while true; do
echo "3. green health check..."
sleep 3
REQUEST=$(curl -s http://127.0.0.1:8080) # green으로 request
if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
echo "health check success"
break
fi
done
echo "4. reload nginx"
cp /home/dan/study_with_me/nginx-application/nginx/nginx.green.conf /home/dan/study_with_me/nginx-application/nginx/conf/nginx.conf
docker exec nginx-application nginx -s reload # nginx 컨테이너에서 reload 실행
echo "5. blue container down"
docker-compose -f ../docker-compose-dev.yaml stop blue
docker-compose -f ../docker-compose-dev.yaml rm -f blue
else
echo "### GREEN => BLUE ###"
echo "1. get blue image"
docker-compose -f ../docker-compose-dev.yaml pull blue # blue 이미지 다운로드
echo "2. blue container up"
docker-compose -f ../docker-compose-dev.yaml up -d blue # blue 컨테이너 실행
while true; do
echo "3. blue health check..."
sleep 3
REQUEST=$(curl -s http://127.0.0.1:8080) # blue로 request
if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
echo "health check success"
break
fi
done
echo "4. reload nginx"
cp /home/dan/study_with_me/nginx-application/nginx/nginx.blue.conf /home/dan/study_with_me/nginx-application/nginx/conf/nginx.conf
docker exec nginx-application nginx -s reload # nginx 컨테이너에서 reload 실행
echo "5. green container down"
docker-compose -f ../docker-compose-dev.yaml stop green
docker-compose -f ../docker-compose-dev.yaml rm -f green
fi
스크립트 설명
- 블루/그린 중 어느 컨테이너가 구동 중인지 확인한다.
- 실행 중이 아닌 컨테이너를 실행한다.
- `Health Check` 진행 후 확인되면
- `리버스 프록시 경로`를 변경한다.(NGINX conf 파일을 상황에 맞도록 대체)
- 그 후, 기존에 구동되고 있던 컨테이너를 제거한다.
위와 같이 설정하므로, 우리는 무중단 CI/CD를 구현할 수 있다.
deploy.sh의 To Do List
- 현재 `dev` 버전의 `deploy.sh`만 존재하는데, `prod` 버전도 만들어야 한다.
- 현재는 무한 `Health Check`를 하는데, 횟수나 시간을 추가하는게 좋다.
4. NGINX
앞서, 잠깐 설명을 했지만 현재 미니PC 서버가 `80포트`를 사용하고 있어서 `82포트`를 사용해야 한다.
또한, `Docker`로 `NGINX`를 구동할 것이기에 아래와 같은 `NGINX` YAML 파일을 생성했다.
nginx-docker-compose.yml
version: '3'
services:
nginx-application:
container_name: nginx-application
image: nginx:latest
ports:
- "82:80"
- "444:443"
volumes:
- ./nginx/conf:/etc/nginx/conf.d
- /etc/letsencrypt:/etc/letsencrypt
networks:
- home_network
networks:
home_network:
external: true # 외부 네트워크 사용
- ports
- `82:80` <- 설명한 이유
- `444:443` <- 마찬가지로, `443`포트를 이미 사용중이므로 `444`로 대체한다.
- volumes
- `deploy.sh`가 `블루/그린`에 맞춰 리버스 프록시 경로를 달리해야 하기에 관련 파일에 대해
`NGINX` 컨테이너의 `/etc/nginx/conf.d` 경로로 마운트해준다. - SSL 인증서도 설정해주어야 하므로 현재 미니PC서버에 있는 인증서도
`NGINX` 컨테이너의 `/etc/letsencrypt` 경로로 마운트해준다.
- `deploy.sh`가 `블루/그린`에 맞춰 리버스 프록시 경로를 달리해야 하기에 관련 파일에 대해
만약, "왜? `/etc/nginx/conf.d`로 마운트 해주는 이유가 궁금하신 분이 있다면 아래와 같다.
NGINX에서 제공하는 기본 `nginx.conf` 파일의 내용
이유는 아래 코드처럼 기본으로 제공해주는 `nginx.conf` 파일에서
`include /etc/nginx/conf.d/*.conf` 경로에 있는 모든 파일을 포함하기 때문이다.
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log notice;
pid /var/run/nginx.pid;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for"';
access_log /var/log/nginx/access.log main;
sendfile on;
#tcp_nopush on;
keepalive_timeout 65;
#gzip on;
include /etc/nginx/conf.d/*.conf; # <- 이 부분
}
실제 미니PC의 디렉토리 확인
이렇게 설정되어 있기에 `volume` 작업이 잘 설정되는 것이다.
그럼 `/conf.d`에는 어떤 내용의 `NGINX` 환경설정 파일이 들어갈까?
nginx.blue.conf
server {
listen 80;
server_name {스터디 윗 미 백엔드 서비스 도메인};
return 301 https://{스터디 윗 미 백엔드 서비스 도메인}:444$request_uri;
}
server {
listen 443 ssl;
server_name {스터디 윗 미 백엔드 서비스 도메인};
ssl_certificate /etc/letsencrypt/live/{스터디 윗 미 백엔드 서비스 도메인}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{스터디 윗 미 백엔드 서비스 도메인}/privkey.pem;
location / {
proxy_pass http://blue:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
nginx.green.conf
server {
listen 80;
server_name {스터디 윗 미 백엔드 서비스 도메인};
return 301 https://{스터디 윗 미 백엔드 서비스 도메인}:444$request_uri;
}
server {
listen 443 ssl;
server_name {스터디 윗 미 백엔드 서비스 서브 도메인};
ssl_certificate /etc/letsencrypt/live/{스터디 윗 미 백엔드 서비스 도메인}/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/{스터디 윗 미 백엔드 서비스 도메인}/privkey.pem;
location / {
proxy_pass http://green:8080;
proxy_set_header Host $host;
proxy_set_header Host $http_host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
환경설정 파일에 관해 설명하자면 아래와 같다.
- {스터디 윗 미 백엔드 서비스 서브 도메인}:82로 접근하면 자동으로, `docker-compose`에서 설정한 `NGINX` 컨테이너의 `80포트`로 매핑된다.
- 이후, 위 환경설정 파일에 의거해 다시, `444포트`로 리턴된다.
- 그리고 `444포트`는 `docker-compose`에서 설정한 `NGINX` 컨테이너의 `443포트`로 매핑된다.
- 이후에는 `그린`이냐 `블루`냐에 따라서 적절하게 라우팅 해준다.
여기까지가 `StudyWithMe` `dev` 환경 배포의 모든 내용이다.
마무리하며
이번 포스팅에서는 `StudyWithMe`의 `dev` 환경 `무중단 CI/CD`에 관해 알아보았다.
여지껏 많은 `CI/CD` 작업을 해오며 점점 익숙해지는 것 같다.
잘한다는 느낌보다는 관련 오류 발생 시에 실수 예상 부분이 바로 생각이나는 느낌?
하지만, 내가 `To Do List`에 작성한 것처럼 아직은 `dev` 환경 배포이기에 부족한 것이 많아서
추후 많이 추가해야 한다!
'프로젝트 > StudyWithMe' 카테고리의 다른 글
[StudyWithMe] 유저의 사업자 검수 요청을 처리하며 - 1 (0) | 2025.01.23 |
---|---|
[StudyWithMe] Async Thread Pool과 CompletableFuture (0) | 2025.01.12 |
[StudyWithMe] 영속성 컨텍스트와 관련된 퀴즈 풀어보실 분? (0) | 2024.12.23 |
[StudyWithMe] 프로젝트에서의 쿼리 고민 일기(계속 추가 예정) (0) | 2024.12.10 |
[StudyWithMe] 멀티 스레드에서 트랜잭션 작업 간 정보 불일치 문제 (0) | 2024.12.08 |