월 천원 더 내고 무중단 배포하기

작은곰·2025년 1월 8일
0

진흙탕 뒹굴기

목록 보기
1/1

돈이 없네
운영중인 서비스의 서버를 업데이트하는 것은 생각보다 피곤하다. 로그를 바라보면서 '오 지금 사용자가 없네?'인 타이밍에 실행중인 서버를 종료하고, 빌드를 한 뒤, 실행을 하는 이 빈곤한 업데이트는 결국 새벽 세 시에 터미널을 바라보고 있는 나를 보게 된다. 업데이트하는 주간이 되면 라이프사이클이 고장나고, 상쾌하지 못한 아침을 맞이하며, 쌩쌩한 새벽 시간을 낭비하곤 한다.

돈이 없어 인스턴스 업그레이드나 추가를 하지 못하고 현실과 타협한 결과물을 한번 공유해보고자 한다.

아마 이 예시는 메모리가 부족한 상태에서 무중단 배포를 하고 싶은 사람들에게 도움이 될 것이다. 프리티어 사용자들은 micro 인스턴스에서 진행한다면 가벼운 서비스 정도는 무중단으로 배포할 수 있을 것이다.

작성할 예시의 스펙은 다음과 같다.

  • EC2 t3a.small 인스턴스
  • Ubuntu 운영체제

0. 일단 구조는 이런 식.

바꿀 구조
단순하다.
FE에서 8080포트를 사용하여 통신을 하고 있기에, Nginx를 8080으로 실행하고 있다.
또한, https 통신을 하지 않고 있다는 가정이다.

1. EC2의 디스크 용량을 늘리자.

1) AWS EC2에 접속해서 할 일

EC2 small 인스턴스의 기본 디스크 용량은 8GB이다.
다음 단계인 Swap Memory를 사용하기에 추가 용량이 필요했기에, 용량을 증설했다.

용량이 충분한 사람들은 이 과정을 진행하지 않아도 되기에, 2)로 넘어가면 된다.

모든 스크린샷은 이미 증설한 이후이므로, 볼륨 크기가 달라도 무시하면 된다.

AWS EC2 인스턴스 정보다.
아래 스토리지 탭에 들어가면 해당 인스턴스가 사용하는 스토리지를 확인할 수 있다.
볼륨 ID를 클릭하게 되면 스토리지 정보로 넘어가게 된다.

볼륨의 상세 정보에서 우측 상단의 수정을 클릭하여 용량을 수정하자.

필요한 만큼 용량을 늘리고 수정을 누르자.
늘리는 것은 가능하지만, 줄이는 것은 불가능하기에 신중하게 늘리자.
범용 SSD의 경우, 1GB당 월 요금 $0.1이 추가된다.

작업이 완료되면, 해당 인스턴스로 접속해 보자.

2) 인스턴스에 접속해서 할 일

파일 시스템명은 /dev/root, 파티션명은 /dev/abcd 로 가정한다.

아래 명령어를 실행하면, 각 파일 시스템의 정보가 나오는데, /dev/root 의 크기가 그대로임을 볼 수 있다.
파티션을 재할당해야 변경된다. 파티션을 재할당해 보자.

$ df -h

명령어를 실행하여 우리가 사용하는 파티션을 확인해야 한다.

$ lsblk

명령어를 실행하면 파티션이 확장된다. 파티션이 확장되었으니, 볼륨을 재할당하자.

$ sudo growpart /dev/abcd 1

이제 아래 명령어를 실행하면 볼륨이 재할당된다. 이제 각 정보를 다시 lsblk 명령어로 확인하면 용량이 늘어났음을 볼 수 있다.

$ sudo resize2fs /dev/root

2. Swap을 사용해 메모리를 늘리자.


EC2 small의 경우, 메모리 용량은 2GB이다. 100개 이상의 API를 제공하는 Spring Boot를 2개나 실행하려다가 굉장히 행복한 경험을 할 뻔했다.
medium으로 올리게 되면, 메모리도 2배! 가격도 2배! 가 되어버리니 더 저렴한 방법을 찾아야 한다.
위에 언급했다시피 디스크를 활용하는 Swap 메모리를 활용할 것이다. 디스크 공간을 가상의 메모리로 사용하는 것이다. 속도가 메모리에 비해 한참 느려 성능 저하가 발생할 수 있고, 모든 메모리가 꽉 찰 경우 위험한 일이 발생할 수 있기 때문에 충분한 용량을 확보하길 바란다.

1) Swap 파일 만들기

EC2에 접속해서 아래의 명령어를 입력하자.

$ sudo dd if=/dev/zero of=/swapfile bs=128M count=32

'/dev/zero''swapfile' 을 128MB의 블록을 32개 생성한다.
해당 명령어를 사용하면 128MB * 32 = 4096MB = 4GB의 Swapfile을 생성하는 것이다. 용량은 잔여 디스크 용량 내에서 잘 조절하기를 바란다.

2) Swap 설정

생성한 swapfile에 읽기 및 쓰기 권한을 주자.

$ sudo chmod 600 /swapfile

이제 swapfile을 통해 영역을 설정하자.

$ sudo mkswap /swapfile

Swap 영역에 Swap 파일을 설정하자.

$ sudo swapon /swapfile

Swap 파일이 설정되어 있는지 확인해보고

$ sudo swapon -s

부팅 시에 자동으로 활성화 하도록 파일시스템을 수정할 것이다.

$ sudo vi /etc/fstab

맨 아래에 아래 문장을 추가하고 저장한다.

/swapfile swap swap defaults 0 0

이제 Swap 메모리가 설정됐을 것이다. 아래 명령어로 메모리 상태를 확인해보면

$ free

Swap이 생겼음을 확인할 수 있다.
와! 스왑메모리!

이제 메모리 여유를 확보했으니까, 행복하게 Spring Boot 애플리케이션을 두 개를 켜보자!

3. Nginx를 설치해 리버스 프록시를 설정하자.

리버스 프록시를 위해 Nginx를 사용할 것이다. 8080 포트로 요청을 받고, 8081과 8082 포트에 각 Spring Boot 애플리케이션을 실행할 것이다. 8081 포트가 구버전이었다면 8082 포트를 신버전으로, 8082 포트가 구버전이었다면 8081 포트를 신버전으로 실행할 것이다.

우선 apt-get을 이용하여 Nginx를 설치하자. apt-get update 등의 내용은 생략한다.

$ sudo apt-get install nginx

nginx가 설치되었는지 확인해 보자.

$ nginx -v

버전이 출력됐다면 설치가 된 것이다.

이제 8080 포트가 실행 중인 스프링부트 애플리케이션을 바라보도록 설정할 것이다. 우선 8080 포트로 서버가 실행 중이었다면, 종료한 뒤에 nginx 설정을 하자.
아래 명령어를 입력하면 설정 파일이 실행된다.

$ sudo vi /etc/nginx/sites-available/default

vi를 이용하여 아래와 같이 수정한다.

아래의 수정한 내용을 설명하자면
1. service-url.inc를 읽어온다.
2. 요청이 오면 {service_url}로 전달한다.
3. proxy_set_header A B => 실제 요청에 A라는 헤더에 B를 할당한다.

service_url을 제외한 변수들은 nginx에서 제공하기 때문에, 구글에 검색해서 알아볼 수 있다.
service_url은 service-url.inc에 기록하자.

$ sudo vi /etc/nginx/conf.d/service-url.inc

우선은 service_url을 8081 포트로 두자.

set $service_url http://127.0.0.1:8081;

이 service-url.inc의 위치를 잘 기억해 두고 이제 Spring Boot를 설정하러 가보자.

4. 다른 포트로 두 개의 Spring Boot를 실행하자.


Spring Boot의 포트 기본값은 8080이다. 이를 application.properties (혹은 .yml)을 통해 변경할 수 있다. 로컬과 배포 환경이 다를 경우, 이미 프로필을 나누어 놓았을 것이다. 배포 환경으로 구성된 두 개의 프로필을 만들자. 각 프로필에 server.profiles 값을 8081, 8082로 설정하여 저장하자.


두 개의 Spring Boot 애플리케이션을 실행했다고 가정하자.
1. 신버전이 실행되었을 때 ps 명령어를 사용해서 '신버전이 실행되었는가?' 확인하고
2. Nginx가 바라보는 포트를 변경한 뒤
3. 구버전을 종료할 수 있을 것이다.
우리는 이걸 딸깍 한 번으로 해결해 보자.

이 구식 방법을 활용하기로 했다면, 최대한 구식으로 가보자. 우리는 정말로 ps 명령어를 사용해 일하고 있는 Spring Boot가 사용하는 포트를 확인할 것이다.
실행 중인 Java는 Spring Boot 애플리케이션 하나만 있다고 생각하자.

#!/bin/bash

# java의 PID를 찾는다.
java_process=$(ps -eo pid,comm | grep java | awk '{print $1}' | head -n 1)

# pid가 없을 경우, 8082로 가정. 있을 경우 사용중인 포트 번호를 저장.
if [ -z "$java_process" ]; then
    current_port=8082
else
    current_port=$(lsof -Pan -p $java_process -i | grep LISTEN | awk '{print $9}' | sed 's/.*://')
fi

그럼 우리는 현재 사용 중인 포트를 찾았으니, 어떤 프로필의 Spring Boot가 실행 중인지 알 수 있다. 8081의 프로필을 set1, 8082의 프로필을 set2라고 가정하자.
set2를 새로 실행시켰을 때, set2가 정상적으로 작동하는지 확인을 한 뒤, Nginx에 연결하고, set1을 종료하자.

if [ $current_port == 8082]; then
	current_profile=set2
    profile=set1
    port=8081
else
	current_profile=set1
    profile=set2
    port=8082
fi

# 새로운 앱 실행
nohup java -jar -Dspring.profiles.active=$profile $path &

# 10초 후 배포 상태 확인
sleep 10

for count in {1...10}
do
	response=$(curl -s http://localhost:$port/health
    up_cnt=$(echo $response | grep 'UP' | wc -l)
    
    # 배포 상태 확인 시 'UP'이라는 단어가 있는지 확인
    if [ $up_cnt -ge 1 ]
    then
    	echo "> 배포 정상"
        break
    else
    	echo "> 배포 비정상"
        echo "> Health check: ${response}"
    fi
    
    # 최대 10회 배포 상태 확인 시도. 10회가 끝나면 스크립트 종료.
    if [ $count -eq 10 ]
    then
    	echo "> 배포 실패."
        echo "> 배포 과정을 종료합니다."
        exit 1
    fi
    
    # 10초 후 상태 확인 재시도
    echo "> 배포 상태 확인 재시도"
    sleep 10
done

# 포트 전환
echo "set \$service_url http://127.0.0.1:${port};" | sudo tee /etc/nginx/conf.d/service-url.inc

# Nginx 재시작
sudo service nginx reload

# 이전 버전 Spring Boot 정상 종료
kill -15 $java_process

5. 뭐여 안되는디.


복붙으로 안된다. 이런 구식으로 무중단 배포를 하는 사람이 있을 리가...

만약 이 구식을 원한다면 Gracefult Shutdown 설정, Spring Boot Actuator 의존성 추가, 배포 스크립트 완성을 하면 된다.

이 방법은 실제 서비스에 23년 11월에 적용한 방법이다. 메모리를 2GB 가까이 잡아먹는 100개 이상(150개에 가까운)의 API를 제공하는 녀석을 어떻게든 낮에도 배포를 하고 싶었다. 당시 나 혼자서 구상할 수 있는 최대한의 방법이었다. 근데 아무도 이렇게는 안했으면 한다.
Docker도 쓰고, Jenkins나 Github Actions로 CI도 구현하고, CodeDeploy도 사용해서 더 좋은 방법으로 하면 아마 이거보다 훨씬 더 좋은 글을 쓸 수 있는 사람이 될 것이다.
이런 글(경험담)을 또 쓴다면 '진흙탕 뒹굴기 시리즈' 정도로 칭할 듯

참고 링크를 남기고 이만 끝.

혹시 진짜 만약에 이 방법을 택하고 하다가 막히면 언제든지 아래 이메일 보내주세요.
growing.lilbear@gmail.com

참고 링크

  1. Swap Memory - https://repost.aws/ko/knowledge-center/ec2-memory-swap-file
  2. 무중단 배포 - https://github.com/jojoldu/springboot-webservice/blob/master/tutorial/7_NGINX_SSL_%EB%AC%B4%EC%A4%91%EB%8B%A8%EB%B0%B0%ED%8F%AC.md
profile
growing?

0개의 댓글

관련 채용 정보