조급하면 모래성이 될뿐

무중단 배포 적용하기 본문

TroubleShooting/데브코스

무중단 배포 적용하기

Pawer0223 2022. 8. 26. 03:50

Why?


  • 프로젝트를 하며 배포 과정에서 아래와 같이 이미 존재하는 포트를 띄우는 상황이 종종 발생했고, 그때마다 서버가 죽어버렸다.

  • 서버가 죽게 되면 프런트팀에서 슬랙을 통해 요청했고, 그때마다 서버에 접속해서 다시 애플리케이션을 구동시켰다.
  • 이런 경우를 최소화하고자 무중단 배포를 적용했다.

How?


  • AWS CodeDeploy를 사용했기 때문에 블루 그린 무중단 배포를 적용하는 방식
    • 이 경우는 ec2를 여러 개 생성해서 사용한다고 이해했다. 현재 프로젝트에서는 조금 오버스럽다고 느껴서 다른 방식을 선택했다.
  • Nginx를 통한 무중단 배포 - 이걸로 적용함
    • 2개의 애플리케이션을 실행한 후(8080, 8081), Nginx를 통해 포워딩한다.
      • 포워딩 규칙은 현재 Nginx가 8080 포트를 바라보고 있다면, 8081에 배포 후, 성공했으면 8081로 변경한다.
      • 실패했다면 그대로 둔다.
      • 반대의 경우도 동일하다. (8081이면 8080으로)

 

참조


 

이해하기


1. 2개의 application을 실행시키기 위해 profile을 분리한다.

  • profile을 dev, dev2로 두었다.
  • dev는 8080, dev2는 8081 포트로 동작하며, 나머지 설정은 동일하다.
  • application-dev.yml
server:
  port: 8080

spring:
  config:
    activate:
      on-profile: dev
      
... 나머지 설정은 동일함 !
  • application-dev2.yml
server:
  port: 8081

spring:
  config:
    activate:
      on-profile: dev2
      
... 나머지 설정은 동일함 !

 

2. 현재 Nginx가 바라보지 않는 애플리케이션을 찾는다. (active-profile, port)

  • 쉽게 Nginx의 proxy_pass설정 url이 무엇인지 확인한다.
    • 현재 8080 포트로 설정돼있으면 8081이 바라보지 않는 애플리케이션이다.

2-1) 현재 동작중인 profile을 확인하기 위한 end-point 추가

  • /profile 호출을 통해 현재 어떤 profile로 active 중인지 확인하기 위함이다.
package com.prgrms.tenwonmoa.domain.common.deploy;

import java.util.Arrays;
import java.util.List;

import org.springframework.core.env.Environment;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

import lombok.RequiredArgsConstructor;

@RestController
@RequiredArgsConstructor
public class ProfileController {
	private static final List<String> DEV_PROFILES = Arrays.asList("dev", "dev2");
	private static final String DEFAULT_PROFILE = "default";
	private final Environment env;

	@GetMapping("/profile")
	public String profile() {
		List<String> profiles = Arrays.asList(env.getActiveProfiles());
		String defaultProfile = profiles.isEmpty() ? DEFAULT_PROFILE : profiles.get(0);

		return profiles.stream().filter(DEV_PROFILES::contains).findAny().orElse(defaultProfile);
	}
}

 

2-2) Nginx 설정

  • 아래는 현재 배포서버의 Nginx설정 파일이다. (/etc/nginx/sites-available 경로에 존재)
    • proxy_pass가 변수로 설정돼있다.
    • 이 변수는 /etc/nginx/conf.d/service-url.inc에 정의되어있다.
server {
        listen 80;

        server_name {domain_name};
        include /etc/nginx/conf.d/service-url.inc; # 무중단 배포를 위해 추가
        location / { # location 블록
                include /etc/nginx/proxy_params;
                proxy_pass $service_url;
        }
        
...

 

  • 아래는 service-url.inc 파일이다.
    • 처음에 {ip_address} 부분을 localhost로 두었더니 제대로 동작하지 않았다.
      • 동작은 curl {domain_name}으로 확인했다. 서비스 주소로 입력했을 때 port를 적지 않아도 요청이 잘 가야 한다.
      • 그러나 no resolver defined to resolve localhost라는 에러와 함께 502 예외가 발생했다.
      • 예외는 /var/log/nginx/error.log 경로에서 확인할 수 있다.
      • 아래와 같이 직접 ec2 주소를 적어주었더니 해결되었다.
set $service_url http://{ip_address}:8080;

 

2-3) profile.sh에서 /profile을 호출하여, active-profile이 dev면 8080 포트를 dev2면 8081 포트를 반환한다.

  • 스크립트 내용은 참조링크에 자세하게 나와있어서 생략한다.
#!/usr/bin/env bash

# 쉬고있는 profile 찾기
function find_idle_profile()
{
	...
}


# 쉬고 있는 profile 의 port 찾기
function find_idle_port()
{
	...
}

예시로 현재 Nginx가 바라보고 있지 않은 애플리케이션이 dev2(8081)이라고 가정하고 설명.

 

3. 2를 통해 찾은 현재 Nginx가 바라보고 있지 않은 애플리케이션을 종료한다.

  • stop.sh이 수행된다. 이 또한 생략

 

4. 배포를 수행한다.

  • start.sh이 수행된다. 이 또한 생략
  • 수행될 때 역시 바라보지 않는 애플리케이션을 찾은 후, 띄우게 된다.
...

IDLE_PROFILE=$(find_idle_profile)

echo "> $JAR_NAME 을 profile=$IDLE_PROFILE 로 실행."

nohup java -jar -Dspring.profiles.active=${IDLE_PROFILE} \
$JAR_NAME > $REPOSITORY/nohup.out 2>&1 &

...

 

5. Nginx가 바라보고 있지 않은 애플리케이션이 정상적으로 재구동 되었는지 확인한 후, Nginx의 설정을 바꾼다.

  • health.sh이 수행된다.
    • 보면 IDLE_PORT를 찾은 후 /profile요청을 포트와 함께 보낸다.
    • 그리고 active profile로 dev를 포함하는지 grep으로 잡고 있다.
    • 서버가 구동되는 시간이 걸릴 수 있기 때문에 10초씩에 한 번씩 총 10번을 보낸다. 그동안 서버 구동이 확인되지 않는다면 Nginx설정은 바꾸지 않는다. 만약 올바른 응답을 받았다면 Nginx설정을 바꾼다.
      • Nginx설정을 바꾸기 위해 switch.sh이 수행된다. 
      • switch.sh에서도 변경될 주소를 localhost가 아닌 {ip_address}로 설정해주어야 정상적으로 switch 가 된다.
        • "set \$service_url http://{ip_address}:${IDLE_PORT};"... 
...

for RETRY_COUNT in {1..10}
do
  RESPONSE=$(curl -s http://localhost:${IDLE_PORT}/profile)
  UP_COUNT=$(echo $RESPONSE} | grep 'dev' | wc -l)

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

  if [ ${RETRY_COUNT} -eq 10 ]
  then
    echo "> Health check 실패."
    echo "> 엔진엑스에 연결하지 않고 배포를 종료합니다."
    exit 1
  fi

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

...

 

6. 작업의 순서는 appspec.yml에 의해 제어된다.

  • appspec.yml
hooks:
  AfterInstall:
    - location: stop.sh # Nginx 와 연결되지 않은 부트 종료
      timeout: 60
      runas: ubuntu
  ApplicationStart:
    - location: start.sh # Nginx 와 연결되어 있지 않은 Port로 새 버전의 부트 시작
      timeout: 60
      runas: ubuntu
  ValidateService:
    - location: health.sh # 새 스프링 부트가 정상적으로 실행됐는지 확인
      timeout: 60
      runas: ubuntu

 

 

결과 확인


  • 2번 이상 배포가 성공했다면, 서버에는 2개의 애플리케이션이 수행되어야 한다.
    • ps -ef | grep java

 

  • Nginx가 8080을 보고 있을 때 한번 더 배포가 성공했다면
    • /etc/nginx/conf.d/service-url.inc 에서 $service_url의 포트가 8081로 변경되어야 한다.
    • vi /etc/nginx/conf.d/service-url.inc 해서 확인하면 변경된 걸 확인할 수 있다.

반응형