일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 |
- 19598
- EntityGraph
- 13975
- slice개념
- 이진 탐색
- 1495
- 모니터링
- join제거
- 이분 탐색
- 3187
- 무한페이징
- binary search
- 11501
- Blue/Green
- NCP
- dto projection
- 백준
- Upper bound
- Java
- 12738
- 20115
- 2512
- 로그
- Promtail
- Lower bound
- no offset
- 14921
- greedy
- DP
- 그리디
- Today
- Total
멘지의 기록장
[SpringBoot] Docker + Nginx + Github Action을 사용하여 무중단배포 CI/CD 구축하기 본문
프로젝트를 진행하면서 적용했던 Docker와 Github Action, Nginx를 사용한 무중단 배포를 하는 방법을 작성해보려고 합니다.
무중단 배포
무중단 배포는 서비스의 중단 없이 새로운 버전의 소프트웨어를 배포하는 것을 말합니다.
새로운 버전의 소프트웨어를 배포하기 위해선
1. 기존 서비스 종료
2. 새로운 서비스 시작
이와 같은 2가지 과정이 필요합니다.
기존 서비스를 종료하여 시스템을 사용할 수 없는 시간이 생기게 되면 이 시간을 다운타임이라고 합니다.
이러한 다운타임을 해결하기 위해 무중단 배포를 사용합니다.
무중단 배포 전략에는 3가지 전략이 있습니다. Rolling, Canary, Blue/Green 중 Blue/Green 방식을 사용하였습니다.
Blue/Green
Blue/Green 방식은 트래픽을 한번에 바꾸는 방법입니다. 일반적으로 현재 운영중인 서버를 Blue라고 하고, 새롭게 배포할 서버를 Green이라고 합니다.
트래픽을 한번에 바꾸기 때문에 Rolling, Canary와는 달리 유저마다 다른 버전의 서버를 사용하지 않게 하여 호환성 문제가 발생하지 않습니다. 다만, 실제 운영에 필요한 서버 리소스가 2배 필요하다는 단점이 있습니다.
이를 해결하기 위해 Docker를 사용하였습니다.
Docker를 사용하면 하나의 EC2에 포트를 다르게 하여 스프링 서버를 띄울 수 있기 때문에 여러 EC2를 사용하지 않고도 Blue/Green 방식을 도입할 수 있어 선택하였습니다.
무중단 배포 CI/CD 구축
Docker 설치
우선 관리자 권한으로 필요한 영역으로 들어간 후 패키지를 업데이트 합니다.
sudo su
sudo apt update
이후 https 관련 패키지를 설치합니다.
sudo apt install apt-transport-https ca-certificates curl software-properties-common
이후 docker repository 접근을 위한 gpg 키를 설정합니다.
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
다음으로 docker repository를 등록합니다.
sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu focal stable"
다시 업데이트를 진행합니다.
sudo apt update
다음으로 도커를 설치한 후 아래와 같은 화면이 나오는지 확인합니다.
sudo apt install docker-ce
docker --version
⚡참고로 sudo 명령어 없이 docker 명령어를 사용하려면!
우선 현재 사용자를 docker group에 포함시킵니다.
sudo usermod -aG docker ${USER}
이후 터미널을 재시작 후 아래 명령어를 실행시켜 docker가 포함되어 있는지 확인하면 됩니다.
id -nG
Swap Space 설정
우선 swap 메모리를 생성합니다.
sudo dd if=/dev/zero of=/swapfile bs=128M count=16
// of : swapfile 경로
// bs : Block 사이즈
// count : Block 갯수
다음으로 swapfile 읽기 쓰기 권한을 업데이트 합니다.
sudo chmod 600 /swapfile
이후 리눅스 swap 영역을 설정합니다.
sudo mkswap /swapfile
다음으로는 swap 메모리를 활성화 시킨 후, 절차가 성공했는지 확인합니다.
sudo swapon /swapfile
sudo swapon -s
부팅 시 swap 메모리를 활성화 시키기 위해 아래의 명령어를 실행시키면 완성입니다.
// 파일 열기
sudo vi /etc/fstab
// 파일 가장 마지막에 추가 후 :wq로 저장하고 종료
/swapfile swap swap defaults 0 0
Docker Compose 및 Nginx 설치
아래 2개의 명령어를 차례대로 실행하여 docker compose를 설치합니다.
curl \
-L "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" \
-o /usr/local/bin/docker-compose
chmod +x /usr/local/bin/docker-compose
Docker의 설치가 완료된 후 아래의 명령어로 Nginx를 설치합니다. (Docker image로 자동 세팅)
docker pull nginx
설치가 완료되면 Nginx를 실행시킵니다.
docker container run --name nginxserver -d -p 80:80 -p 443:443 nginx
Nginx를 실행시킨 후 아래의 코드를 사용해 Nginx에 접속합니다.
docker exec -it nginxserver bash
이후 Blue 또는 Green 컨테이너의 Nginx를 설정해주기 위해 conf.d 설정파일이 있는 디렉토리로 이동합니다.
cd /etc/nginx/conf.d
위의 경로에 있는 default.conf 파일을 수정하기 전,
Nginx 컨테이너에는 vim editor가 없기 때문에 먼저 설치를 해줍니다.
apt update
apt upgrade
apt install vim
설치가 완료되면 default.conf 파일을 수정하기 위해 아래 명령어를 실행합니다.
vim default.conf
기본 default.conf 파일은 아래와 같습니다.
우선 upstream 변수를 설정해줍니다.
upstream 변수는 server 설정에서 Nginx가 받아들인 요청을 어떤 서버로 흘려보내 줄 것인지(load balancing) 결정할 때 사용됩니다.
EC2에서 설정한 Private IP 주소 뒤에 8080 포트로 온 요청을 blue 서버로, 8081 포트로 온 요청을 green 서버로 보내주기 위해 아래와 같이 설정하였습니다.
(탄력적 IP가 아닌, Private IP를 사용하는 이유는 EC2 내부에서 IP를 찾아가야 하기 때문입니다.)
include /etc/nginx/conf.d/service-env.inc 에는 Nignx에서 사용할 환경 변수(service_url)를 넣을 예정입니다.
location / {} 의 의미는 'http://탄력적 IP/'로 오는 모든 요청을 받아오겠다는 것입니다.
proxy_pass http://$service_url
⏩ 현재 위치에서 요청을 전달한 upstream 서버를 지정하겠다는 의미입니다.
service_url에는 blue 또는 green이 들어가게 됩니다.(실제 요청이 전달될 서버의 주소)
blue가 들어간다면 ⏩ upstream blue를 찾아서 실행
green이 들어간다면 ⏩ upstream green을 찾아서 실행
이제는 service_url을 넣을 파일인 service-env.inc를 만듭니다.
vim service-env.inc
파일의 내용
set #service_url green;
위의 모든 작업은 마치게 되면 exit을 통해 처음 화면으로 돌아갑니다.
이후 docker-compose.yml 파일을 생성해줍니다.
vim docker-compose.yml
아래와 같이 docker-compose.yml 파일에 작성합니다.
컨테이너를 실행할 때 도커 이미지와 컨테이너 이름, 외부포트 내부포트를 지정해줍니다.
version: '3.8'
services:
blue:
image: Docker_계정명/프로젝트명:prod
container_name: blue
ports:
- "8080:8080"
environment:
- ENV=blue
green:
image: Docker_계정명/프로젝트명:prod
container_name: green
ports:
- "8081:8080"
environment:
- ENV=green
Github Action 설정
EC2에서 설정해야하는 것은 모두 끝났고 이제 Github Action을 실행시키기 위한 yml 파일을 생성합니다.
main 브랜치에 push가 되는 경우에 실행하도록 설정하였습니다.
Github Secrets에 넣어놓은 application-prod.yml 파일 내용을 가져와 'src/main/resources' 디렉토리를 생성한 후 yml 파일을 만들어 넣어줍니다.
이후 Docker image를 생성한 후 Docker hub에 push 해줍니다.
health check를 통해 현재 실행되고 있는 docker 컨테이너의 이름을 확인하고,
CURRENT_UPSTREAM(제거할 Docker 컨테이너 이름)과 TARGET_UPSTREAM(앞으로 실행할 컨테이너 이름)을 설정합니다.
이후 위에서 설정했던 docker compose를 실행시킨 후 nginx를 통해 upstream을 변경 후 재실행합니다.
마지막으로 현재 도커 컨테이너를 stop 및 remove 함으로써 마무리가 됩니다.
name: Java CI with Gradle
on:
push:
branches: [ "main" ]
permissions:
contents: read
jobs:
build:
runs-on: ubuntu-latest
env:
APPLICATION_PROD: ${{ secrets.APPLICATION_PROD }}
permissions:
contents: read
steps:
- uses: actions/checkout@v4
- name: Set up JDK 17
uses: actions/setup-java@v4
with:
java-version: '17'
distribution: 'corretto'
# 설정 파일 생성
- name: application yml setting
run: |
mkdir -p ./src/main/resources/
echo ${{ env.APPLICATION_PROD }} | base64 --decode > ./src/main/resources/application-prod.yml
# Gradle 실행권한 부여
- name: Grant execute permission to gradlew
run: chmod +x ./gradlew
# Spring boot application 빌드
- name: Build with gradle
run: ./gradlew clean build
# DockerHub에 로그인
- name: Login to DockerHub
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }}
# Docker 이미지 build 후 DockerHub에 push
- name: Build Docker
run: docker build --platform linux/amd64 -t ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod .
- name: Push Docker
run: docker push ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod
deploy:
needs: build
runs-on: ubuntu-latest
steps:
- name: Set target IP
run: |
STATUS=$(curl -o /dev/null -w "%{http_code}" "http://${{ secrets.SERVER_DOMAIN }}/global/health-check")
echo $STATUS
if [ $STATUS = 200 ]; then
CURRENT_UPSTREAM=$(curl -s "http://${{ secrets.SERVER_DOMAIN }}/global/health-check")
else
CURRENT_UPSTREAM=green
fi
echo CURRENT_UPSTREAM=$CURRENT_UPSTREAM >> $GITHUB_ENV
if [ $CURRENT_UPSTREAM = blue ]; then
echo "CURRENT_PORT=8080" >> $GITHUB_ENV
echo "STOPPED_PORT=8081" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=green" >> $GITHUB_ENV
else
echo "CURRENT_PORT=8081" >> $GITHUB_ENV
echo "STOPPED_PORT=8080" >> $GITHUB_ENV
echo "TARGET_UPSTREAM=blue" >> $GITHUB_ENV
fi
- name: Docker compose
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.SERVER_DOMAIN }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker pull ${{ secrets.DOCKERHUB_USERNAME }}/${{ secrets.DOCKER_IMAGE_NAME }}:prod
sudo docker-compose -f docker-compose.yml up -d ${{ env.TARGET_UPSTREAM }}
- name: Check deploy server URL
uses: jtalk/url-health-check-action@v4
with:
url: http://${{ secrets.LIVE_SERVER_IP }}:${{ env.STOPPED_PORT }}/global/health-check
max-attempts: 5
retry-delay: 10s
- name: Change nginx upstream
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.SERVER_DOMAIN }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true
script: |
sudo docker exec -i nginxserver bash -c 'echo "set \$service_url ${{ env.TARGET_UPSTREAM }};" > /etc/nginx/conf.d/service-env.inc && nginx -s reload'
- name: Stop current server
uses: appleboy/ssh-action@master
with:
username: ubuntu
host: ${{ secrets.SERVER_DOMAIN }}
key: ${{ secrets.EC2_SSH_KEY }}
script_stop: true # 실행 중 문제가 생기면 더 진행 X
script: |
sudo docker stop ${{ env.CURRENT_UPSTREAM }}
sudo docker rm ${{ env.CURRENT_UPSTREAM }}
프로젝트에 있는 Dockerfile도 아래와 같이 설정해줍니다.
FROM amazoncorretto:17.0.7-alpine
COPY build/libs/server-0.0.1-SNAPSHOT.jar 프로젝트명.jar
ENV TZ Asia/Seoul
ARG ENV
ENTRYPOINT ["java", "-jar","-Dspring.profiles.active=prod", "-Dserver.env=${ENV}", "프로젝트명.jar"]
⚡ Github Secrets에 넣은 값들입니다
EC2 서버 동작 확인
green 서버가 잘 떠 있는 것을 확인할 수 있습니다.
무중단배포와 Github Actions를 사용하기 전, 수동배포를 하며 많은 불편함이 있었고,
또한, 서버가 내려가 있는 동안 프론트가 요청을 보내지 못하는 등의 문제점이 있었기 때문에
무중단 배포를 통해 유저와 프론트가 서비스를 지속적으로 사용할 수 있다는 점과, 배포 자동화를 통해 반복적이고 불필요한 행동을 줄일 수 있다는 것이 얼마나 많은 도움이 되는지 알 수 있었습니다.
'SpringBoot' 카테고리의 다른 글
No Offset을 적용한 무한 페이징 구현하기 (성능 비교) (1) | 2024.12.20 |
---|---|
N+1 문제 해결 방법 (0) | 2024.07.31 |
스프링 오답노트 (2) | 2024.04.05 |
@Valid를 이용한 유효성 검증 (0) | 2024.03.23 |
Lock의 종류 (0) | 2024.03.23 |