티스토리 뷰

Overview

현재 space club 프로젝트를 리팩토링 하며 하나하나 뜯어 고치는 중이다.

프로젝트때 나는 인프라 부분을 맡지 않았다. 팀원 중 한명이 인프라에 관심을 가지고 있던 친구여서 모든 백엔드 환경을 잘 구성해 줬었기에 스프링 애플리케이션 개발에만 집중할 수 있었다.

하지만 리팩토링을 진행하면서 좀 더 나은 방식을 적용할 수 있겠다는 생각이 들었고 이 모든 것을 바로잡고 블로그 글을 쓰기 까지 총 일주일이 넘는 시간이 걸렸다.

 

내가 이번에 프로젝트 리팩토링을 진행하면서 겪은 문제점은 두 가지이다.

 

1️⃣ HTTPS 설정 시, EC2에 Application Load Balancer를 통한 과금 문제

2️⃣ CI/CD 중 배포 문제

  2-1. CI/CD 중 불필요한 파일이 S3에 업로드 되는 문제

  2-2. 배포 시 백엔드 서버가 중단되는 문제

 

1️⃣ HTTPS 설정 시, EC2에 Application Load Balancer를 통한 과금 문제

[문제점]

출처 : aws 공식 사이트

프로그래머스에서 지원해 주던 서버 비용이, 프로그래머스 데브코스가 종료됨에 따라 데브코스 aws 계정에 접근이 더 이상 불가하게 되면서 개인 서버로 이전 작업을 진행해야 했다. (이전 작업은 여러 다른 블로그에 잘 나와 있어 EC2와 RDS, S3를 모두 스냅샷을 통해 내 계정으로 옮겼다)

그때는 사용하고 싶은 기술들을 마음껏 사용할 수 있었지만, 내 개인 서버로 옮기면 내 돈 아닌가!

(가난한 취준생인..)나는 과금 걱정이 들었다. 💸 지난 인프라 학습시 dynamoDB를 계속 켜두니 한달에 8만원 가까운 폭탄을 맞은 적이 있어 더 그랬던 것 같다.

 

기존 HTTPS 설정 시 ACM과 Route53, ALB을 통한 HTTPS 설정을 마치고보니, ALB를 사용하면 시간당 0.0225달러가 지출되는 것을 확인했다. (이는 30일을 계쏙 켜 놓을경우 16달러에 해당된다).

따라서 HTTPS를 설정하는 다른 방식이 없을까 해서 방식을 찾아 보다, 웹서버인 Nginx를 리버스 프록시로 사용해 로드밸런서 기능을 할 수 있게 함으로서 ALB를 대체할 수 있다는 것을 알게 되었다.

 

[해결]

처음에 한 고민은, HTTPS 설정 시, SSL 인증서를 제공해주는 CA를 ACM(Amazon Certificate Manager)에서 발급받아 nginx에서 사용할 수는 없는가였다. 

결론은 그럴수 없었다. 

대신, Nginx는 Let’s Encrypt이라는 자동화된 무료 개방형 인증 기관(CA)에서 SSL/TLS 인증서를 무료로 제공 받을 수 있다. certbot을 통해서 Nginx에서 Let's Encrypt CA의 인증서를 발급 받을 수 있었다.

 

[결과]

그렇게 기존 ACM과 ALB를 통한 HTTPS 설정에서 Nginx와 certbot을 통해서 HTTPS 설정 방식을 변경함으로서 ELB에 들어가는 요금을 줄일 수 있었다.

 

물론, 트래픽이 몰려 nginx가 버티지 못하거나, 로드밸런싱 시 오토스케일링 기능이 필요하다면 ELB를 선택하는것이 합리적이겠지만, space club 프로젝트 상으로는 nginx를 사용하는 방식이 더 합리적이라고 판단하였다.

 

2️⃣ CI/CD 중 배포 문제 - 서버가 중단되는 문제

2-1. CI/CD 중 불필요한 파일이 S3에 업로드 되는 문제

[문제점]

기존 space club의 전체적인 파이프라인은 다음과 같다.

CI/CD 파이프라인 구조

  1. github action을 통해 CI 과정 및 Continuous Delivery 과정을 진행한다.
  2. 프로젝트 전체 파일을 빌드 후 jar 파일로 압축한다.
  3. AWS IAM으로 AWS 권한 인증을 완료하고
  4. S3에 jar 파일을 올린다.
  5. S3의 jar 파일을 code deploy를 통해 배포 한다.

위 방식에서, jar 파일을 압축할 때 기존에서는 전체 프로젝트를 압축했었다.

기존 workflow - 변경된 workflow

[해결]

하지만 스프링 애플리케이션을 실행하는데, 테스트 파일이나 프로덕션 코드까지 압축할 필요는 없다고 생각했다.

실행하는데는 jar파일과 code deploy 실행시 필요한 appspec.yml, code deploy가 실행하는 script들과 secret 폴더에 있는 비속어 목록들 밖에 없었다. (애플리케이션 로드 시 필요)

 

[결과]

따라서 필요한 파일만 압축하니, 3MB정도의 공간이 줄었다.

비록 큰 차이는 아니지만, 불필요한 S3 용량과 EC2의 디스크 공간을 줄일 수 있었다.

code deploy시 사용하는 s3 bucket

 

 

  2-2. 배포 시 백엔드 서버가 중단되는 문제

[문제점]

위 CI/CD 파이프라인 구조를 보면 한 가지 문제가 있다.

이 방식대로 배포를 한다면, 배포 시 code deploy에서 기존 애플리케이션이 돌아가고 있는 프로세스를 kill하고 다시 스프링 애플리케이션을 띄우는 동안, 약 20초에서 30초 정도의 시간 동안 백엔드 서버가 아예 꺼지게 된다.

 

[기존 배포 스크립트]

if [ -z $CURRENT_PID ] # 기존 애플리케이션 있다면 종료
then
  echo "> 종료할 애플리케이션이 없습니다"
else
  echo "> 실행 중인 애플리케이션 종료 $CURRENT_PID"
  kill -15 $CURRENT_PID
  sleep 5
fi

echo "> 배포 - $JAR_PATH"

# 새로운 애플리케이션 배포 시작
chmod +x $JAR_PATH
sudo nohup java -jar $JAR_PATH --spring.profiles.active=develop --jasypt.encryptor.password=${encrypt} > /home/ubuntu/log/nohup_log.out 2> /home/ubuntu/log/nohup_error.out &

이렇게 되면, 프론트에서 애플리케이션 개발에 있어서 차질이 생길 수 밖에 없다.(실제로 이를 프론트에서 불편한 내색을 보인 경우도 있었다)

따라서, 나는 무중단 배포 방식으로 인프라 구조 변경을 진행하였다.

무중단 배포에도 여러 방식이 있지만, 하나의 EC2에서 가장 효율적으로 어떻게 하면 down time을 최소화 할 수 있을지를 고민하였다.

추가 EC2를 띄우기에는 비용문제가 발생했고, docker를 통한 무중단 배포를 진행하기에는 학습 비용도 발생할 뿐 아니라, 크게 장점을 현 프로젝트에서는 찾기 힘들었다.

 

[해결]

따라서, 내가 한 방식은 profile을 통한 blue green 무중단 배포이다.

1.blue, green 프로파일을 추가한다. 그리고 blue는 8081 포트로, green은 8082 포트로 설정한다.

2. health check를 위한 현재 profil을 확인 할 수 있는 api를 하나 만든다.

@RestController
@RequiredArgsConstructor
@RequestMapping("/api")
public class ProfileController {

    private static final List<String> BLUE_GREEN = List.of("blue", "green");

    private final Environment env;

    @GetMapping("/profile")
    public String profile() {
        return Arrays.stream(env.getActiveProfiles())
                .filter(BLUE_GREEN::contains)
                .findAny()
                .orElseGet(() -> " ");
    }

}

3. 위 api를 사용해 현재 8081포트, 즉 blue에 애플리케이션이 동작하고 있다면 8082 포트, green으로 애플리케이션을 배포한다. (반대도 마찬가지, 아무 애플리케이션 동작 안하고 있다면 blue에 배포)

4. health check를 통해 배포가 잘 완료되었는지 확인

5. nginx가 포트포워딩을 지금 배포한 애플리케이션으로 변경

6. 기존 실행중이던 애플리케이션 프로세스 kill ☠️

 

[변경된 배포 스크립트]

더보기
#!/bin/bash

source /home/ubuntu/action/scripts/properties.sh # jasypt 암호키 설정

PROJECT_NAME=space-club-backend
REPOSITORY=/home/ubuntu
PACKAGE=$REPOSITORY/action/build/libs/
JAR_NAME=$(ls -tr $PACKAGE | grep 'SNAPSHOT.jar' | tail -n 1) # snapshot jar 파일
JAR_PATH=$PACKAGE$JAR_NAME # action/build/libs/*SNAPSHOT.jar
echo "> build 파일명: $JAR_NAME"

# 금칙어 리스트 복사 (최신화)
PROFANITY_LIST_LOCATION_TO_STORE=$REPOSITORY/bad_word_list.txt
PROFANITY_LIST_LOCATION=$REPOSITORY/action/src/main/resources/secrets/bad_word_list.txt
echo "> 금칙어 list 복사" # 덮어 쓰기
cp -f $PROFANITY_LIST_LOCATION $PROFANITY_LIST_LOCATION_TO_STORE 

echo "> build 파일 복사" # 덮어 쓰기
DEPLOY_PATH=$REPOSITORY/jar/
cp -f $JAR_PATH $DEPLOY_PATH

echo "> 현재 구동중인 SET 확인"
PROFILE_BLUE=$(curl -s http://localhost:8081/api/profile)
PROFILE_GREEN=$(curl -s http://localhost:8082/api/profile)
echo "> BLUE : $PROFILE_BLUE GREEN : $PROFILE_GREEN"

# 쉬고 있는 set 찾기: blue가 사용중이면 green이 쉬고 있고, 반대면 blue가 쉬고 있음
if [ $PROFILE_BLUE == "blue" ]
then
  IDLE_PROFILE=green
  IDLE_PORT=8082
elif [ $PROFILE_GREEN == "green" ]
then
  IDLE_PROFILE=blue
  IDLE_PORT=8081
else
  echo "> 일치하는 Profile이 없습니다. Profile: $PROFILE_BLUE $PROFILE_GREEN"
  echo "> BLUE를 할당합니다. IDLE_PROFILE: blue"
  IDLE_PROFILE=blue
  IDLE_PORT=8081
fi

echo "> application.jar 교체"
IDLE_APPLICATION=$IDLE_PROFILE-SpaceClub.jar
IDLE_APPLICATION_PATH=$DEPLOY_PATH$IDLE_APPLICATION

echo "> jar 파일과 blue/green-SpaceClub.jar와 심볼릭 링크 생성"
ln -Tfs $DEPLOY_PATH$JAR_NAME $IDLE_APPLICATION_PATH

echo "> $IDLE_PROFILE 에서 구동중인 애플리케이션 pid 확인"
IDLE_PID=$(pgrep -f $IDLE_APPLICATION)
if [ -z $IDLE_PID ]
then
  echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
else
  echo "> kill -15 $IDLE_PID"
  sudo kill -15 $IDLE_PID
  sleep 5
fi

echo "> $IDLE_PROFILE 배포"
sudo nohup java -jar $IDLE_APPLICATION_PATH --spring.profiles.active=develop,$IDLE_PROFILE --jasypt.encryptor.password=${encrypt} --bad-word.path=$PROFANITY_LIST_LOCATION_TO_STORE > /home/ubuntu/log/nohup_log.out 2> /home/ubuntu/log/nohup_error.out &

echo "> $IDLE_PROFILE 10초 후 Health check 시작"
echo "> curl -s http://localhost:$IDLE_PORT/actuator/health "
sleep 10

for retry_count in {1..10}
do
  response=$(curl -s http://localhost:$IDLE_PORT/actuator/health)
  up_count=$(echo $response | grep 'UP' | wc -l)

  if [ $up_count -ge 1 ]
  then # $up_count >= 1 ("UP" 문자열이 있는지 검증)
      echo "> Health check 성공"
      break
  else
      echo "> Health check의 응답을 알 수 없거나 혹은 status가 UP이 아닙니다."
      echo "> Health check: ${response}"
  fi

  if [ $retry_count -eq 10 ]
  then
    echo "> Health check 실패. "
    echo "> Nginx에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

  echo "> Health check 연결 실패. 재시도..."
  sleep 10
done

echo "> nginx의 port를 변경합니다"
echo "> 현재 구동중인 Port 확인"

# blue가 사용중이면 green이 쉬고 있고, 반대면 blue가 쉬고 있음
if [ $IDLE_PROFILE == "blue" ]
then
  IDLE_PORT=8081
elif [ $IDLE_PROFILE == "green" ]
then
  IDLE_PORT=8082
else
  echo "> 일치하는 Profile이 없습니다. Profile: $PROFILE_BLUE $PROFILE_GREEN"
  echo "> 8081을 할당합니다."
  IDLE_PORT=8081
fi

echo "> 전환할 Port: $IDLE_PORT"
echo "> Port 전환"
echo "set \$service_url http://127.0.0.1:${IDLE_PORT};" |sudo tee /etc/nginx/conf.d/service-url.inc

echo "> Nginx Reload"
sudo service nginx reload

echo "> 배포 후 기존 실행하던 애플리케이션을 종료합니다."
echo "> $IDLE_PROFILE와 반대 애플리케이션을 종료합니다."
if [ $IDLE_PROFILE = "blue" ] # idle profile이 blue이면 green을 종료
then
  IDLE_PID=$(pgrep -f green-SpaceClub.jar)
  if [ -z "$IDLE_PID" ]
  then
    echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
  else
    echo "> blue가 실행되었고 green을 종료합니다.  kill -15 $IDLE_PID"
    sudo kill -15 $IDLE_PID
  fi
elif [ $IDLE_PROFILE == "green" ] # idle profile이 green이면 blue를 종료
then
  IDLE_PID=$(pgrep -f blue-SpaceClub.jar)
  if [ -z "$IDLE_PID" ]
  then
    echo "> 현재 구동중인 애플리케이션이 없으므로 종료하지 않습니다."
  else
    echo "> green이 실행되었고 blue를 종료합니다. kill -15 $IDLE_PID"
    sudo kill -15 $IDLE_PID
  fi
fi

[결과]

20~30초 정도의 downtime을 0.2초로 줄였다.

 

 

그래서 현재 무중단 배포 방식의 문제점은 없는가❓🤔

 

(1) 완벽한 무중단 배포가 아니다.

스프링 애플리케이션이 두 개가 켜져있을 때가 있어 EC2의 CPU 사용량이 튈때가 있지만, 그래도 중단시간을 최소화 할 수 있었다.

찾아보니, Nginx가 포트포워딩 8081에서 8082로 (혹은 반대로) 전환하고 reload 하면서 0.2초간의 down time이 발생한다.

따라서 완벽한 무중단 배포라고는 할 수 없을 것같다.

 

(2) stateful한 애플리케이션일 경우 적합하지 못하다

현재 스프링 애플리케이션은 JWT로 인증 처리를 하고 있어서 상관은 없지만, 세션을 통한 인증방식 같이 메모리에 데이터를 저장해 사용하는 stateful한 애플리케이션일 경우는, 현재의 방식이 적합하지 않다는 생각이 든다.

 

(3) 요청이 처리가 안될 수 있다.

물론 0.2초의 down time이 발생하기 때문에 당연하지만, 스프링 애플리케이션이 바뀌면서, 기존 처리하던 요청들이 nginx 포트 포워딩에 따라 응답을 받지 못할 수도 있다.

이것을 해결하는 방식으로 spring에서 지원하는 graceful shutdown을 사용하여 해결 할 수도 있을 것 같다.

출처 : 스프링 공식문서(https://docs.spring.io/spring-boot/reference/web/graceful-shutdown.html)

 

🔚 끝맺으며...

인프라 작업을 진행하면서 느낀건, 진짜 참을성이 전부였다.

수많은 예기치 못한 상황들을 해결하고, 끝내 해내는 것이 중요했다.

그렇다. 시간이 좀 걸렸지만 인프라, 이쯤이면 잘 해냈다고 스스로에게 생각하며 마친다. 이상.

 

인프라 적용 흔적들, 참고한 Reference 블로그

[노션링크]

 

실제 HTTPS 적용기 | Notion

위 블로그를 참고해서 space club 프로젝트의 HTTPS 설정을 진행했다.

kaput-trombone-343.notion.site

 

 

최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday