2024년 8월 6일 작성한 글입니다.
서비스를 개발하며 벼르고 있던 작업 중 하나였습니다. 구현 작업이 느슨해질 때 쯤 작업했었네요. 이 작업은 개발/배포 환경에 영향을 많이 받다보니 외부 코드를 차용하지 못하고 직접 코드를 많이 타이핑했었던 기억이 남아있습니다..
1. 문제 인식 | “어? 서버 오류났어요”
사실 ‘무중단 배포’ 구축 작업 자체는 워낙 유명한 CD 작업이기 때문에, 문제가 발생하기 전에도 ‘이건 나중에 꼭 작업해야지’ 라고 인식하고 있던 작업이였습니다.
직접적으로 해당 작업 필요성에 대해 필요성을 느끼게 된 건 배포 작업이 잦아지면서부터였습니다. 기능 개발과 버그 픽스가 빠르게 이루어지며 배포 주기가 짧아지고, 특히 테스트를 위해 개발 서버에 변경 사항을 반영할 때 문제가 두드러졌습니다.
2. 문제 상황 | “아 배포중입니다”
실 예시를 들자면.. 테스트를 위해 개발 서버에 자동 배포 중 다운타임이 약 1분 간 발생하게 되는데 이때 앱 개발자분께서 “어? 서버 오류났어요” 라고 말씀하시는 상황입니다.
그럴 때마다 항상 “아 죄송해요 지금 배포중입니다” 라고 말씀드리고 넘겼지만 가끔 잡담하느라 배포중인 걸 나도 깜빡했을 때는 진짜 가슴이 철렁하기도 하고, 앱 개발자분이 자꾸 서버 에러를 경험하는 일이 발생하니 죄송스럽기도 하더라구요.(서버 배포때문에 작업이 딜레이되는 경우도 있으니) 이런 불필요한 커뮤니케이션 증가 + 개발 작업 딜레이 + 클라이언트 단에서의 다운타임과 같은 문제를 해결하기 위해 3월 초 ‘무중단 배포’ 작업을 진행하기로 했습니다.
사실 ‘무중단 배포’는 클라이언트가 서버 점검 상황 시 체감하는 다운타임 문제를 해결하기 위해 사용하는 방법인데 저희 팀의 경우엔 서비스에 실 이용자가 많지 않다보니 개발 환경에서 문제가 더 체감이 되었네요
3. 해결 방법 탐색 과정
무중단 배포를 구축하는 개념은 크게 ‘Rolling’, ‘Blue/Green’, ‘Canary’ 이렇게 크게 세가지로 분류됩니다. 이 중 서비스 API 안정성 확보를 위해 Blue/Green 방식을 채택했습니다. 서버 API가 추가되고 변경되는 일이 적지않다보니 Blue/Green 환경이 적합하다고 판단했습니다.
배포 방법은 크게 세 가지로 나뉩니다.
- 서버 단위 로드 밸런싱
- 가동 서버를 2대로 하여 차례대로 업데이트를 진행하는 방식입니다. 가장 단순하지만 서버 비용이 2배로 든다는 단점이 있습니다.
- 내부 포트 포워딩
- 외부 트래픽을 서버의 리버스 프록시에서 받은 후 내부 어플리케이션으로 다시 포워딩하는 방식입니다.
- 프로세스 매니저
- node.js 에는 pm2 라는 프로세스 매니저가 있습니다. 간단히 설명하자면 싱글 쓰레드로 구동되는 node.js 어플리케이션을 멀티 쓰레드로 구동할 수 있도록 해주는 프로세스 매니저입니다. 이 매니저를 사용하여 여러 개로 실행되는 프로세스를 하나씩 업데이트하는 방식입니다.
이 중 2번 내부 포트 포워딩을 선택했습니다. 현재 Docker 를 통해 서버를 구동하고 있기 때문에 다른 방법들에 비해 환경 구축이 가장 간단하고 추가 비용이 발생하지않는다는 점 때문이였습니다.
4. 해결 내용
레거시 github actions 배포 코드 먼저 보겠습니다.
deploy:
needs: build
name: Deploy
runs-on: [develop]
steps:
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username:
password:
- name: Docker run
run: |
docker stop ${{ env.NAME }} && docker rm ${{ env.NAME }} && docker rmi ${{ env.DOCKER_IMAGE }}:latest
docker run -d -p 80:3000 --name ${{ env.NAME }} --restart always ${{ env.DOCKER_IMAGE }}:latest
build 단계에서 push 받은 image 를 컨테이너로 구동하는 방식입니다.
기존에는 github actions 에서 직접 컨테이너를 구동시키면 됐지만 이젠 작업해야할 사항이 많아졌습니다. 포트 포워딩, 구동 컨테이너 지정 등..
일단 포트 포워딩을 위해 리버스 프록시 nginx 를 설치했습니다. 그리고 blue / green 상태에 따라 각자 다른 포트로 포워딩 시켜야하기 때문에 nginx.blue.conf 파일과 nginx.green.conf 파일을 만들었습니다.
// /etc/nginx/nginx.blue.conf
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
}
http {
include mime.types;
client_max_body_size 10m;
upstream backend {
server localhost:8081; # GREEN - 8080 포트로 연결합니다.
# BLUE 설정 시 server 127.0.0.1:8081; 로 변경합니다.
}
server {
listen 80; # or another port that NGINX will listen on
location / {
proxy_pass http://backend; # Upstream 이름을 사용합니다.
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;
}
}
}
// /etc/nginx/nginx.green.conf
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
}
http {
include mime.types;
client_max_body_size 10m;
upstream backend {
server localhost:8080; # GREEN - 8080 포트로 연결합니다.
# BLUE 설정 시 server 127.0.0.1:8081; 로 변경합니다.
}
server {
listen 80; # or another port that NGINX will listen on
location / {
proxy_pass http://backend; # Upstream 이름을 사용합니다.
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 파일을 적용하고 컨테이너를 구동시킬 쉘 스크립트를 구성했습니다.
user nginx;
worker_processes auto;
pid /run/nginx.pid;
include /etc/nginx/modules-enabled/*.conf;
events {
worker_connections 1024;
}
http {
include mime.types;
client_max_body_size 10m;
upstream backend {
server localhost:8080; # GREEN - 8080 포트로 연결합니다.
# BLUE 설정 시 server 127.0.0.1:8081; 로 변경합니다.
}
server {
listen 80; # or another port that NGINX will listen on
location / {
proxy_pass http://backend; # Upstream 이름을 사용합니다.
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;
}
}
}
[ec2-user@ip-172-31-57-168 ~]$ cat deploy_zero_down_time.sh
#!/bin/bash
IS_GREEN=$(docker ps | grep green)
DEFAULT_CONF="/etc/nginx/nginx.conf"
if [ -z $IS_GREEN]; then
echo "###BLUE => GREEN###"
#docker image pull
docker pull ghcr.io/eightbloom/hwadam-client-server-dev:latest
#docker tag 변경 latest->recent
docker tag ghcr.io/eightbloom/hwadam-client-server-dev:latest ghcr.io/eightbloom/hwadam-client-server-dev:recent-green
#green 컨테이너 실행
docker run -d -p 8080:3000 --name floom-client-server-dev-green --restart always ghcr.io/eightbloom/hwadam-client-server-dev:recent-green
#green 컨테이너 상태 확인
while [ 1 = 1 ]; do
echo "3. green health check..."
sleep 3
REQUEST=$(curl http://127.0.0.1:8080) # green으로 request
if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
echo "health check success"
break ;
fi
done;
#nginx가 green 컨테이너를 향하도록 설정 변경 및 리로드
echo "4. reload nginx"
sudo cp /etc/nginx/nginx.green.conf /etc/nginx/nginx.conf
sudo nginx -s reload
echo "5. blue container down"
docker stop floom-client-server-dev-blue
docker rm floom-client-server-dev-blue
echo "6. delete not used image"
docker rmi ghcr.io/eightbloom/hwadam-client-server-dev:latest
docker rmi ghcr.io/eightbloom/hwadam-client-server-dev:recent-blue
else
echo "###GREEN => BLUE###"
#docker image pull
docker pull ghcr.io/eightbloom/hwadam-client-server-dev:latest
#docker tag 변경 latest->recent
docker tag ghcr.io/eightbloom/hwadam-client-server-dev:latest ghcr.io/eightbloom/hwadam-client-server-dev:recent-blue
#blue 컨테이너 실행
docker run -d -p 8081:3000 --name floom-client-server-dev-blue --restart always ghcr.io/eightbloom/hwadam-client-server-dev:recent-blue
#blue 컨테이너 상태 확인
while [ 1 = 1 ]; do
echo "3. blue health check..."
sleep 3
REQUEST=$(curl http://127.0.0.1:8081) #blue으로 request
if [ -n "$REQUEST" ]; then # 서비스 가능하면 health check 중지
echo "health check success"
break ;
fi
done;
#nginx가 blue 컨테이너를 향하도록 설정 변경 및 리로드
echo "4. reload nginx"
sudo cp /etc/nginx/nginx.blue.conf /etc/nginx/nginx.conf
sudo nginx -s reload
echo "5. green container down"
docker stop floom-client-server-dev-green
docker rm floom-client-server-dev-green
echo "6. delete not used image"
docker rmi ghcr.io/eightbloom/hwadam-client-server-dev:latest
docker rmi ghcr.io/eightbloom/hwadam-client-server-dev:recent-green
fi
이제 마지막으로 github actions 에 배포를 해당 쉘 스크립트를 실행하도록 하면 끝입니다.
deploy:
needs: build
name: Deploy
runs-on: [develop]
steps:
- name: Login to ghcr
uses: docker/login-action@v1
with:
registry: ghcr.io
username:
password:
- name: Docker run
run: |
sudo chmod 755 /home/ec2-user/deploy_zero_down_time.sh
/home/ec2-user/deploy_zero_down_time.sh
5. 개선 결과
배포할 떄 발생했던 1~2초 텀이 참 불편했는데 속이 후련했습니다. 그리고 개발 서버 배포 중 체감되는 일시적 오류와 같은 개발 환경에서의 불편함이 많이 개선되었습니다. 무엇보다 서버 오류에 대한 제보를 듣는 빈도가 낮아져서 가장 좋았습니다