1. 개요
이전까지 무중단 배포를 위한 기초적인 설정을 마쳤다. 이제 이를 기반으로 배포 시 작동하는 스크립트 코드를 작성해보고자 한다.
2. git action yml
//프로젝트 빌드 및 docker push
...
- name: Deploy to ec2
uses: appleboy/ssh-action@master
with:
host: ${{ secrets.EC2_HOST }} # EC2 퍼블릭 IPv4 DNS
username: ubuntu
key: ${{ secrets.PRIVATE_KEY }}
port: 22
script: |
sudo touch prod.env
echo "${{ secrets.ENV_VARS_PROD }}" | sudo tee prod.env > /dev/null
sudo touch docker-compose-prod.yml
echo "${{ vars.DOCKER_COMPOSE_PROD }}" | sudo tee docker-compose-prod.yml > /dev/null
sudo docker login -u ${{ secrets.DOCKER_USERNAME }} -p ${{ secrets.DOCKER_PASSWORD }}
chmod +x /home/ubuntu/prod-deploy.sh
/home/ubuntu/prod-deploy.sh
다른 과정은 모두 동일하다. 환경 변수 파일과 docker-compose 파일을 만들어 넣고, 스크립트가 길어지는 관계로 배포 스크립트를 따로 ec2에 저장 후 이를 실행시키도록 하였다.
3. docker container 구조
도커 구조는 다음과 같이 설계하였다.
ec2 하나에 개발 서버와 운영 서버를 모두 올렸다. 이에 따라 각 서버에 따라 두 개의 네트워크를 미리 생성하고, DB로 쓸 컨테이너를 공통으로, 그 외의 컨테이너를 필요에 따라 따로 배치하였다.
4. 스크립트 작성
name: tripot-prod
services:
web-prod-blue:
container_name: web-prod-blue
image: ***/tripot-prod
env_file:
- prod.env
environment:
- TZ=Asia/Seoul
ports:
- "8081:8080"
networks:
- doc-net-prod
web-prod-green:
container_name: web-prod-green
image: ***/tripot-prod
env_file:
- prod.env
environment:
- TZ=Asia/Seoul
ports:
- "8082:8080"
networks:
- doc-net-prod
nginx:
container_name: nginx
image: nginx
environment:
- TZ=Asia/Seoul
volumes:
- ./nginx/conf.d/:/etc/nginx/conf.d/
ports:
- "80:80"
- "443:443"
networks:
- doc-net-prod
networks:
doc-net-prod:
external: true
blue, green을 담당할 두 컨테이너를 만들어준다. 모두 doc-net-prod 네트워크를 사용한다.
4-1.prod-deploy.yml
APPLICATION=tripot-prod
IP="IP"
echo $APPLICATION
IS_RUNNING=$(sudo docker compose ls | grep $APPLICATION | grep running | sed 's/.*/true/')
#운영용 애플리케이션을 pull(빌드는 git action에서 진행)
sudo docker pull ***/tripot-prod:latest
#만약 실행중이면 현재 실행중인 어플리케이션을 확인
if [ "$IS_RUNNING" = "true" ]
then
echo $APPLICATION 실행중
#Blue가 현재 실행중인지 체크
IS_BLUE_RUNNING=$(sudo docker compose -p $APPLICATION ps | grep web-prod-blue | grep Up | sed 's/.*/true/')
echo $CUR_APPLICATION
#만약 Blue가 실행중이라면
if [ "$IS_BLUE_RUNNING" = "true" ]
then
CUR_APPLICATION="web-prod-blue"
DEPLOY_APPLICATION="web-prod-green"
NEW_PORT="8082"
#Green이 실행중이라면
else
CUR_APPLICATION="web-prod-green"
DEPLOY_APPLICATION="web-prod-blue"
NEW_PORT="8081"
fi
#service.inc의 값을 DEPLOY_APPLICATION으로 변경
echo "> 리버스 프록시 방향 변경"
sudo sed -i "s/${CUR_APPLICATION}/${DEPLOY_APPLICATION}/" ~/nginx/conf.d/service-env.inc
echo 사용하지 않는 이미지 정리
sudo docker rmi $(sudo docker images -f "dangling=true" -q)
echo 배포시작 : $DEPLOY_APPLICATION
# 배포할 어플리케이션만 실행하도록 설정
sudo docker compose -f docker-compose-prod.yml --env-file ./prod.env up -d --no-deps $DEPLOY_APPLICATION
# 어플리케이션이 정상적으로 실행될때까지 기다리기 위해 Sleep 설정
#이건 직접 어플리케이션 요청을 계속 보내면서 확인하는게 가장 좋은 방법 같습니다.
sleep 35
# 배포된 어플리케이션의 상태를 확인 -> health check 10초당 1회 총 10회 실시
for retry_count in $(seq 10)
do
response=$(curl -s http://$IP:$NEW_PORT/actuator/health)
up_count=$(echo $response | grep 'UP' | wc -l)
echo "> ${retry_count} : ${response} : ${up_count}"
if [ $up_count -ge 1 ]
then
echo "> 서버 Health check 성공"
#Nginx의 설정정보를 Reload해줍니다.
sudo docker compose -p $APPLICATION exec nginx nginx -s reload
#이전에 실행중이던 어플리케이션을 종료해줍니다.
sudo docker compose -p $APPLICATION stop $CUR_APPLICATION
break
fi
if [ $retry_count -eq 10 ]
then
echo "> 서버 Health check 실패"
exit 1
fi
echo "> 실패 10초 후 재시도"
sleep 10
done
else
echo $APPLICATION 실행중이지 않음
# docker-compose 실행(여기서 blue, green 모두 실행)
docker compose -f docker-compose-prod.yml --env-file ./prod.env up -d
#service.inc의 값을 blue으로 변경
echo "> 리버스 프록시 방향 초기 설정 - blue"
echo "set \$service_url web-prod-blue;" > ~/nginx/conf.d/service-env.inc
# 어플리케이션이 정상적으로 실행될때까지 기다리기 위해 Sleep 설정
#이건 직접 어플리케이션 요청을 계속 보내면서 확인하는게 가장 좋은 방법 같습니다.
sleep 70
#Nginx의 설정정보를 Reload해줍니다.
sudo docker compose -p $APPLICATION exec nginx nginx -s reload
#Green 어플리케이션을 종료해줍니다.
sudo docker compose -p $APPLICATION stop web-prod-green
echo $APPLICATION 실행
fi
파트 별로 하나씩 살펴보자.
4-1-1. 애플리케이션을 초기 실행하는 경우
APPLICATION=tripot-prod
IP="IP"
echo $APPLICATION
IS_RUNNING=$(sudo docker compose ls | grep $APPLICATION | grep running | sed 's/.*/true/')
#운영용 애플리케이션을 pull(빌드는 git action에서 진행)
sudo docker pull ***/tripot-prod:latest
...
/// 애플리케이션 실행 중인 경우에 대한 처리
...
else
echo $APPLICATION 실행중이지 않음
# docker-compose 실행(여기서 blue, green 모두 실행)
docker compose -f docker-compose-prod.yml --env-file ./prod.env up -d
#service.inc의 값을 blue으로 변경
echo "> 리버스 프록시 방향 초기 설정 - blue"
echo "set \$service_url web-prod-blue;" > ~/nginx/conf.d/service-env.inc
# 어플리케이션이 정상적으로 실행될때까지 기다리기 위해 Sleep 설정
#이건 직접 어플리케이션 요청을 계속 보내면서 확인하는게 가장 좋은 방법 같습니다.
sleep 70
#Nginx의 설정정보를 Reload해줍니다.
sudo docker compose -p $APPLICATION exec nginx nginx -s reload
#Green 어플리케이션을 종료해줍니다.
sudo docker compose -p $APPLICATION stop web-prod-green
echo $APPLICATION 실행
fi
우선 변수들을 선언해준다. APPLICATION에는 docker-compose의 이름을 기입한다. 해당 docker compose가 실행 중인지 docker compose ls로 확인한다.
sed 명령어로 grep의 결과가 존재하면 true로 바꾼다. git action에서 build and push 했으므로 pull 하여 이미지를 받아온다.
실행중이지 않을 경우는 간단하다. 운영용 docker compose를 up으로 실행시켜준다. 이전 포스팅에서 web-prod-blue, web-prod-green의 upstream 변수를 선언해주었다. 초기에는 어떤 것이든 상관없으나 blue를 처음 실행시키기로 하고, 해당 변수를 nginx가 인식할 수 있도록 service-env.inc 파일을 작성해준다.
현재 환경에서 docker compose up 후 두 개의 애플리케이션이 실행되는 데 1분정도 걸렸으므로 넉넉히 70초 정도 기다려 애플리케이션의 정상 동작을 기다린다. 그 후 nginx가 변수를 읽어들일 수 있도록 재시작, blue를 사용할 것이므로 green을 꺼준다.
4-1-2. 애플리케이션이 실행 중인 경우
#만약 실행중이면 현재 실행중인 어플리케이션을 확인
if [ "$IS_RUNNING" = "true" ]
then
echo $APPLICATION 실행중
#Blue가 현재 실행중인지 체크
IS_BLUE_RUNNING=$(sudo docker compose -p $APPLICATION ps | grep web-prod-blue | grep Up | sed 's/.*/true/')
echo $CUR_APPLICATION
#만약 Blue가 실행중이라면
if [ "$IS_BLUE_RUNNING" = "true" ]
then
CUR_APPLICATION="web-prod-blue"
DEPLOY_APPLICATION="web-prod-green"
NEW_PORT="8082"
#Green이 실행중이라면
else
CUR_APPLICATION="web-prod-green"
DEPLOY_APPLICATION="web-prod-blue"
NEW_PORT="8081"
fi
#service.inc의 값을 DEPLOY_APPLICATION으로 변경
echo "> 리버스 프록시 방향 변경"
sudo sed -i "s/${CUR_APPLICATION}/${DEPLOY_APPLICATION}/" ~/nginx/conf.d/service-env.inc
echo 사용하지 않는 이미지 정리
sudo docker rmi $(sudo docker images -f "dangling=true" -q)
echo 배포시작 : $DEPLOY_APPLICATION
# 배포할 어플리케이션만 실행하도록 설정
sudo docker compose -f docker-compose-prod.yml --env-file ./prod.env up -d --no-deps $DEPLOY_APPLICATION
# 어플리케이션이 정상적으로 실행될때까지 기다리기 위해 Sleep 설정
#이건 직접 어플리케이션 요청을 계속 보내면서 확인하는게 가장 좋은 방법 같습니다.
sleep 35
# 배포된 어플리케이션의 상태를 확인 -> health check 10초당 1회 총 10회 실시
for retry_count in $(seq 10)
do
response=$(curl -s http://$IP:$NEW_PORT/actuator/health)
up_count=$(echo $response | grep 'UP' | wc -l)
echo "> ${retry_count} : ${response} : ${up_count}"
if [ $up_count -ge 1 ]
then
echo "> 서버 Health check 성공"
#Nginx의 설정정보를 Reload해줍니다.
sudo docker compose -p $APPLICATION exec nginx nginx -s reload
#이전에 실행중이던 어플리케이션을 종료해줍니다.
sudo docker compose -p $APPLICATION stop $CUR_APPLICATION
break
fi
if [ $retry_count -eq 10 ]
then
echo "> 서버 Health check 실패"
exit 1
fi
echo "> 실패 10초 후 재시도"
sleep 10
done
이 경우 해야할 일은 다음과 같다.
- 현재 실행중인 컨테이너를 확인한다.
- nginx가 실행중이지 않은(실행 예정의) 애플리케이션의 포트 번호를 가리키도록 설정 파일을 변경한다.
- 해당 컨테이너를 실행한다.
- 실행이 완료되면 이전에 실행중이었던 구버전의 애플리케이션을 종료한다.
하나씩 진행해보자.
echo $APPLICATION 실행중
#Blue가 현재 실행중인지 체크
IS_BLUE_RUNNING=$(sudo docker compose -p $APPLICATION ps | grep web-prod-blue | grep Up | sed 's/.*/true/')
echo $CUR_APPLICATION
#만약 Blue가 실행중이라면
if [ "$IS_BLUE_RUNNING" = "true" ]
then
CUR_APPLICATION="web-prod-blue"
DEPLOY_APPLICATION="web-prod-green"
NEW_PORT="8082"
#Green이 실행중이라면
else
CUR_APPLICATION="web-prod-green"
DEPLOY_APPLICATION="web-prod-blue"
NEW_PORT="8081"
fi
blue가 올라가 있다면 새로 배포할 애플리케이션은 green에, 반대의 경우에는 green에서 blue로 변경하는 작업을 진행하게 된다. 이를 확인하여 종료할 컨테이너와 새로 켤 컨테이너 및 포트를 선언해준다.
#service.inc의 값을 DEPLOY_APPLICATION으로 변경
echo "> 리버스 프록시 방향 변경"
sudo sed -i "s/${CUR_APPLICATION}/${DEPLOY_APPLICATION}/" ~/nginx/conf.d/service-env.inc
echo 사용하지 않는 이미지 정리
sudo docker rmi $(sudo docker images -f "dangling=true" -q)
service-env.inc의 파일의 현재 컨테이너의 이름을 새로 실행할 컨테이너의 이름으로 변경한다. 만약 blue가 실행중이었다면 해당 파일은 다음과 같이 바뀌게 된다.
set $service_url web-prod-green;
echo 배포시작 : $DEPLOY_APPLICATION
# 배포할 어플리케이션만 실행하도록 설정
sudo docker compose -f docker-compose-prod.yml --env-file ./prod.env up -d --no-deps $DEPLOY_APPLICATION
# 어플리케이션이 정상적으로 실행될때까지 기다리기 위해 Sleep 설정
#이건 직접 어플리케이션 요청을 계속 보내면서 확인하는게 가장 좋은 방법 같습니다.
sleep 35
이제 새로 실행할 애플리케이션을 실행한다. 이 때는 현재 환경에서 30초가량 걸렸으므로 넉넉히 35초를 기다려준다.
for retry_count in $(seq 10)
do
response=$(curl -s http://$IP:$NEW_PORT/actuator/health)
up_count=$(echo $response | grep 'UP' | wc -l)
echo "> ${retry_count} : ${response} : ${up_count}"
if [ $up_count -ge 1 ]
then
echo "> 서버 Health check 성공"
#Nginx의 설정정보를 Reload해줍니다.
sudo docker compose -p $APPLICATION exec nginx nginx -s reload
#이전에 실행중이던 어플리케이션을 종료해줍니다.
sudo docker compose -p $APPLICATION stop $CUR_APPLICATION
break
fi
if [ $retry_count -eq 10 ]
then
echo "> 서버 Health check 실패"
exit 1
fi
echo "> 실패 10초 후 재시도"
sleep 10
done
스프링 액츄에이터를 통해 현재 서버가 정상 실행중인지 확인할 수 있다. 다양한 방법이 있지만 요청과 응답이 정상적으로 동작하는 지 확인하기 위해 이를 이용하였다. 아래 설정을 통해 health check 응답을 받을 수 있다.
build.gradle
// Spring Actuator
implementation 'org.springframework.boot:spring-boot-starter-actuator'
application.yml
management:
endpoints:
web:
exposure:
include: health
curl -s 명령어는 입력한 주소로 요청을 보내여 결과 응답을 출력한다. 정상적으로 실행중일 경우 응답은 다음과 같이 받게 되므로 UP의 포함 여부를 확인한다.
{
"status": "UP"
}
이를 10회 정도 충분히 확인한 후 실행이 확인되었다면 nginx를 재실행하여 리버스 프록시 방향을 바꿔주고, 구버전 애플리케이션의 실행을 종료한다.
6. 참고 문헌
https://amepistheo.tistory.com/42
[SpringBoot] Docker + Nginx + Github Action을 사용하여 무중단배포 CI/CD 구축하기
프로젝트를 진행하면서 적용했던 Docker와 Github Action, Nginx를 사용한 무중단 배포를 하는 방법을 작성해보려고 합니다. 무중단 배포무중단 배포는 서비스의 중단 없이 새로운 버전의 소프트웨
amepistheo.tistory.com
Spring Boot Docker-Compose 무중단 배포
프로젝트가 거의 완성되가고 있어서 AWS EC2에 어플리케이션을 배포하게 되었는데 배포할때 마다 서비스가 계속 중단되는 것을 방지하기 위해 NginX 웹서버에 있는 로드밸런서 기능을 이용해서 무
velog.io
'Infra' 카테고리의 다른 글
무중단 배포 - Nginx default.conf 파일 작성하기 (0) | 2025.02.01 |
---|