문제&해결

Redis 도입하기 : CI/CD 파이프라인 수정기 (Blue/Green 무중단 배포)

mint* 2024. 10. 8. 20:07
728x90

서론

팀 프로젝트에 OAuth 기능을 도입하며 Redis를 추가할 상황이 생겼습니다.

⬇️

 

OAuth 소셜 로그인 안전하게 구현하기 (redirect_uri, redis, token)

서론OAuth의 기본 개념 소개OAuth의 핵심은 인증과 인가를 구분하는 것인증 : 사용자가 자신의 신원을 증명하는 것ex) id, pw로 로그인인가: 인증된 사용자에게 특정 리소스에 대한 접근 권한을 부여

shout-to-my-mae.tistory.com

 

이 과정에서 기존 인프라를 수정할 필요가 있었는데, redis 도입 과정에서 겪은 트러블슈팅을 정리했습니다.

 

기존 인프라 구성

    • 도커 기반의 컨테이너된 환경에서 운영되고 있습니다.
    • Blue/Green 무중단 배포 전략이 사용되었습니다.
    • CI/CD 파이프라인은 github action에서 jenkins로 이전한 상태였습니다.
기존 인프라는 다른 팀원이 구현하였기 때문에 인프라 수정시 먼저 작성된 인프라 파일을 분석할 필요가 있었습니다.
모든 파일은 github에 올라와있습니다.

 

참고) 도커 네트워크

  • 컨테이너끼리 서로 통신하게 해줍니다.
  • IP 주소가 아닌 컨테이너 이름으로 서비스에 접근합니다.
  • 명령어
# 도커 네트워크를 확인
docker network ls
docker network inspect [네트워크 이름]

# 한 컨테이너에서 다른 컨테이너로 통신 테스트
docker exec -it [컨테이너 이름] sh # 컨테이너 접속하기
ping redis # ping 보내기
nc -zv redis 6379

 

참고 ) Blue/Green 배포

  • 현재 서버와 똑같은 서버를 먼저 띄운 후, 앞단의 서버가 기존 서버에서 새로운 서버로 요청을 바꿔서 전달합니다.
    • 짧은 시간동안 두 서버(기존 서버와 새로운 서버)가 동시에 띄워져 있습니다.
  • 서로 다른 포트를 사용하여 동시에 두 버전을 운영합니다.
  • 문제가 생기면 기존 서버로 전환하여 롤백합니다.

 

redis 도입 순서

  • 1. docker 파일을 수정하여 redis 컨테이너를 추가합니다.
  • 2. 기존 애플리케이션 컨테이너와 redis 컨테이너를 연결할 네트워크를 생성합니다.
  • 3. 배포 스크립트를 수정하여 테스트합니다.
직접 ec2의 배포 환경에 접근하여 수정하지 않고, CI/CD 파이프라인을 통해서 수정을 진행할 예정입니다.
배포 환경에서 명령어를 바로 작동시키면 문제가 발생할 수 있기 때문입니다.

 

 

1. 도커 컴포즈 파일 수정하기

도커 컨테이너에 Redis 서비스를 추가하고 네트워크를 통해 기존 서비스와 연결했습니다.

 

✅ 네트워크 연결 문제

  • docker-compose.yml
version: "3"

services:
  backend:
    image: ${DOCKER_IMAGE}
    container_name: athens-backend
    ports:
      - 8080:8080
    env_file: .env
    volumes:
      - ./log:/log
    restart: always
    depends_on:
      - redis
    networks:
      - app-network

  redis:
    image: redis:latest
    container_name: athens-redis
    ports:
      - 6379:6379
    volumes:
      - redis-data:/data
    restart: always
    networks:
      - app-network

networks:
  app-network:
    driver: bridge

volumes:
  redis-data:

 

브리지 네트워크를 생성해서 컨테이너끼리 연결하려고 했는데 네트워크가 제대로 생성되지 않았습니다.

확인해보니, 생성된 네트워크 명이 지정된 `app-network`가 아닌 `ubuntu_app-network` 였습니다.

 

  • 명령어
# 각 컨테이너가 어떤 네트워크에 연결되었는지 확인
docker inspect -f '{{.Name}} - {{range $k, $v := .NetworkSettings.Networks}}{{$k}}{{end}}' $(docker ps -aq)

 

해결 방법

컨테이너를 띄우기 전에 먼저 배포 스크립트에서 네트워크를 생성하고, 컨테이너들은 생성된 네트워크로 연결하도록 설정하였습니다.

  • 배포 스크립트 (deploy-prod.sh)에 네트워크 생성 코드를 추가했습니다.
if ! docker network inspect ubuntu_app-network &>/dev/null; then
  docker network create ubuntu_app-network
fi
  • docker-compose.yml 에서 외부 네트워크를 사용하도록 수정했습니다.
networks:
  ubuntu_app-network: # 네트워크 이름
    external: true # 외부 네트워크 사용

services:
  redis:
    # ...
    networks:
      - ubuntu_app-network # 네트워크 이름

 

최종 코드

  • docker-compose.blue.yml
services:  
  backend-blue:  
    image: ${DOCKER_IMAGE}  
    container_name: athens-blue  
    env_file: .env  
    volumes:  
      - ./log:/log  
    ports:  
      - 8081:8080  
    restart: always  
    depends_on:  
      - redis  
    networks:  
      - ubuntu_app-network  

networks:  
  ubuntu_app-network:  
    external: true
  • docker-compose.green.yml
services:  
  backend-green:  
    image: ${DOCKER_IMAGE}  
    container_name: athens-green  
    env_file: .env  
    volumes:  
      - ./log:/log  
    ports:  
      - 8082:8080  
    restart: always  
    depends_on:  
      - redis  
    networks:  
      - ubuntu_app-network  
  
networks:  
  ubuntu_app-network:  
    external: true
  • docker-compose.yml
services:  
  redis:  
    image: redis:latest  
    container_name: athens-redis  
    ports:  
      - 6379:6379  
    volumes:  
      - redis-data:/data  
    restart: always  
    networks:  
      - ubuntu_app-network  
  
networks:  
  ubuntu_app-network:  
    external: true  
  
volumes:  
  redis-data:

 

 환경변수가 제대로 로드되지 않은 문제

  • 문제 : .env 파일에서 환경 변수를 수정했음에도 컨테이너에 바로 반영되지 않는 문제가 있었습니다.
  • 이유 : 도커 파일을 확인해보니, 환경 변수는 도커 컨테이너가 시작될 때 주입되었습니다.
  • 해결 방법: 환경 변수 수정 후 변경 사항을 적용하려면 컨테이너를 재시작하면 됩니다.
  • .env
REDIS_HOST = redis // Docker Compose에서 정의한 서비스 이름
REDIS_PORT = 6379 // 기본 포트
  • application-prod.yml
redis:
  host: ${REDIS_HOST}
  port: ${REDIS_PORT}
  • docker-compose.blue.yml
services:  
  backend-blue:  
    image: ${DOCKER_IMAGE}  
    container_name: athens-blue  
    env_file: .env # 여기

 

관련 명령어

  • 컨테이너 재시작 방법
// 컨테이너 다시 재시작하기
docker-compose down && docker-compose up -d
// 또는, 환경변수를 확실히 적용하려면 컨테이너를 재생성하는 것도 좋다.
docker-compose up -d --force-recreate [서비스명]
  • 컨테이너에 적용된 환경변수 읽어오는 방법
docker exec -it [컨테이너이름] env | grep [환경변수명]
  • 컨테이너 로그 확인 방법
docker logs [컨테이너 이름]

 

2. CI/ CD 파이프라인 수정하기

  • 기존 배포 스크립트에 도커 네트워크 생성 로직을 추가했습니다.
  • 에러를 해결했습니다.
    • 발생 에러 : service "backend-blue" depends on undefined service "redis": invalid compose project
    • 발생 이유 : 배포 스크립트에서 환경별 파일(blue, green)뿐만 아니라 기본 환경 설정 파일(docker-compose.yml)도 포함시켜서 배포해야합니다.

 

기존 배포 스크립트를 파악하기

  • deploy.sh
#!/bin/bash

# 스크립트 불러오기
# mntdg(): 그린 버전의 Caddy 설정을 적용 
# mntbg(): 블루 버전의 Caddy 설정을 적용 
# mntms(): Caddy 서비스를 재시작
source /usr/share/bg/mnt.sh

# 환경 변수 설정하기
WAS_NAME="athens"  # WAS 이름
COMPOSE_BIN="/usr/local/bin/docker-compose"  # docker-compose 실행 파일 경로
MAX_RETRIES=10  # 최대 재시도 횟수

# 함수 정의하기
# 새로 배포된 WAS가 실행 중인지 확인하는 함수
check_execute_was() {
  docker ps --filter "name=$WAS_NAME-${NEW_COLOR}" --format "{{.Status}}" | grep -q "^Up"
}

# API 상태를 확인하는 함수
check_api_status() {
  local inspect=$(docker inspect $WAS_NAME-${NEW_COLOR} --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')
  local endpoint="http://$inspect:8080/api/v1/open/health-check"
  curl -s -o /dev/null -w "%{http_code}" "$endpoint" | grep -q '^200'
}

# 현재 실행 중인 WAS의 색상을 확인하기
CURRENT_COLOR=$(docker ps --filter "name=$WAS_NAME" --format "{{.Names}}" | cut -d'-' -f2)

# 새로 배포할 색상 결정하기
if [ -z "$CURRENT_COLOR" ]; then
  NEW_COLOR="blue"
else
  NEW_COLOR=$( [ "$CURRENT_COLOR" = "blue" ] && echo "green" || echo "blue" )
fi

# 새 버전 배포하기
$COMPOSE_BIN -f "docker-compose.${NEW_COLOR}.yml" pull && $COMPOSE_BIN -f "docker-compose.${NEW_COLOR}.yml" up -d

# 새 버전 상태 확인하기 (최대 10번 재시도)
attempts=0
while [ $attempts -lt $MAX_RETRIES ]; do
  if check_execute_was; then
   sleep 2
    if check_api_status; then
      break
    fi
  fi
  attempts=$((attempts + 1))
  sleep 2
done

# 배포 실패 시 롤백하기
if [ $attempts -ge $MAX_RETRIES ]; then
  $COMPOSE_BIN -f "docker-compose.${NEW_COLOR}.yml" down
  exit 1
fi

# Caddy 설정 변경 & 서비스 재시작
if [ "$NEW_COLOR" = "blue" ]; then
  echo "mb"
  mntbg  # 블루 버전의 Caddy 설정 적용
else
  echo "mg"
  mntdg  # 그린 버전의 Caddy 설정 적용
fi
mntms  # Caddy 서비스를 재시작하여 새 설정 적용하기

# 이전 버전 컨테이너 정리
if [ -n "$CURRENT_COLOR" ]; then
  $COMPOSE_BIN -f "docker-compose.${CURRENT_COLOR}.yml" down
fi
  • Blue/Green 배포 로직이 구현된 스크립트
    • 1. 현재 실행중인 색깔을 확인하고 반대색깔의 컨테이너를 실행합니다.
    • 2. 헬스 체크를 수행하여 정상 작동되는지 확인합니다. (10번까지 재시도)
      • 잘 실행되지 않으면 새 컨테이너를 중지하고 종료합니다.
    • 3. 새 버전이 잘 실행되면 트래픽을 새 버전으로 전환합니다. (Caddy 설정)
    • 4. 이전 버전의 컨테이너를 종료합니다.
Caddy는 api 서버 앞단의 웹서버입니다.

 

기존 배포 스크립트를 수정하기

  • 컨테이너들을 연결시킬 네트워크를 생성합니다.
if ! docker network inspect ubuntu_app-network >/dev/null 2>&1; then  
     docker network create ubuntu_app-network  
fi
  • redis 컨테이너가 색상에 포함되지 않도록 수정합니다.
CURRENT_COLOR=$(docker ps --filter "name=$WAS_NAME" --format "{{.Names}}" | grep -E 'blue|green' | cut -d'-' -f2)
  • 환경별 설정 파일 뿐 아니라 기본 환경 설정 파일도 포함해서 실행합니다.
$COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${NEW_COLOR}.yml" pull
$COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${NEW_COLOR}.yml" up -d
  • 배포 완료 메세지를 추가합니다.
`echo "Deployment completed successfully."`
  • 수정본
#!/bin/bash  
  
# 스크립트 불러오기  
# mntdg(): 그린 버전의 Caddy 설정을 적용  
# mntbg(): 블루 버전의 Caddy 설정을 적용  
# mntms(): Caddy 서비스를 재시작  
source /usr/share/bg/mnt.sh  
  
# 환경 변수 설정하기  
WAS_NAME="athens"  # WAS 이름  
COMPOSE_BIN="/usr/local/bin/docker-compose"  # docker-compose 실행 파일 경로  
MAX_RETRIES=10  # 최대 재시도 횟수  
PROJECT_NAME="athens"  
  
# 네트워크 생성 (없으면 생성, 있으면 무시)  
if ! docker network inspect ubuntu_app-network >/dev/null 2>&1; then  
  docker network create ubuntu_app-network  
fi 
  
# 함수 정의하기  
# 새로 배포된 WAS가 실행 중인지 확인하는 함수  
check_execute_was() {  
  docker ps --filter "name=$WAS_NAME-${NEW_COLOR}" --format "{{.Status}}" | grep -q "^Up"  
}  
  
# API 상태를 확인하는 함수  
check_api_status() {  
  local inspect=$(docker inspect $WAS_NAME-${NEW_COLOR} --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')  
  local endpoint="http://$inspect:8080/api/v1/open/health-check"  
  curl -s -o /dev/null -w "%{http_code}" "$endpoint" | grep -q '^200'  
}  
  
# 현재 실행 중인 WAS의 색상을 확인하기  
CURRENT_COLOR=$(docker ps --filter "name=$WAS_NAME" --format "{{.Names}}" | grep -E 'blue|green' | cut -d'-' -f2)
  
# 새로 배포할 색상 결정하기  
if [ -z "$CURRENT_COLOR" ]; then  
  NEW_COLOR="blue"  
else  
  NEW_COLOR=$( [ "$CURRENT_COLOR" = "blue" ] && echo "green" || echo "blue" )  
fi  
  
# 새 버전 배포하기  
echo "Deploying new version: $NEW_COLOR"  
$COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${NEW_COLOR}.yml" pull  
$COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${NEW_COLOR}.yml" up -d  
  
# 새 버전 상태 확인하기 (최대 10번 재시도)  
attempts=0  
while [ $attempts -lt $MAX_RETRIES ]; do  
  if check_execute_was; then  
    sleep 2  
    if check_api_status; then  
      echo "New version is up and healthy"  
      break  
    fi  
  fi  
  attempts=$((attempts + 1))  
  sleep 2  
done  
  
# 배포 실패 시 종료하기
if [ $attempts -ge $MAX_RETRIES ]; then  
  echo "Failed to deploy new version"  
  exit 1  
fi  
  
# Caddy 설정 변경 & 서비스 재시작  
if [ "$NEW_COLOR" = "blue" ]; then  
  echo "mb"  
  mntbg  # 블루 버전의 Caddy 설정 적용  
else  
  echo "mg"  
  mntdg # 그린 버전의 Caddy 설정 적용  
fi  
  
mntms # Caddy 서비스를 재시작하여 새 설정 적용하기  
  
# 이전 버전 컨테이너 정리하기  
if [ -n "$CURRENT_COLOR" ]; then  
  echo "Removing old version: $CURRENT_COLOR"  
  $COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${CURRENT_COLOR}.yml" down  
fi  
  
echo "Deployment completed successfully."

 

 이전 색상의 컨테이너가 종료 되지 않는 문제

  • 요구사항 : blue, green 배포시 새로운 색의 서버를 배포하고 기존 서버를 종료해야합니다.
  • 문제 : 컨테이너 종료 명령어가 제대로 수행되지 않아 그대로 남아있는 것을 발견하였습니다.
  • 이유 : 컨테이너에 연결된 네트워크가 있을 때, 연결된 네트워크로 인해 컨테이너가 제대로 잘 제거되지 않습니다.
  • 해결방법 : 먼저 컨테이너에서 네트워크 연결을 해제하고 컨테이너를 제거하면 됩니다.
# 이전 버전 컨테이너 정리하기  
if [ -n "$CURRENT_COLOR" ]; then  
  echo "Removing old version: $CURRENT_COLOR"  
  docker network disconnect ubuntu_app-network ${WAS_NAME}-${CURRENT_COLOR} || true  
  docker stop ${WAS_NAME}-${CURRENT_COLOR} || echo "Failed to stop container"  
  docker rm -f ${WAS_NAME}-${CURRENT_COLOR} || echo "Failed to remove container"  
fi

 

최종 배포 스크립트

#!/bin/bash  

# 스크립트 불러오기  
# mntdg(): 그린 버전의 Caddy 설정을 적용  
# mntbg(): 블루 버전의 Caddy 설정을 적용  
# mntms(): Caddy 서비스를 재시작  
source /usr/share/bg/mnt.sh  

# 환경 변수 설정하기  
WAS_NAME="athens"                           # WAS 이름  
COMPOSE_BIN="/usr/local/bin/docker-compose" # docker-compose 실행 파일 경로  
MAX_RETRIES=10                              # 최대 재시도 횟수  
PROJECT_NAME="athens"  

# 네트워크 생성 (없으면 생성, 있으면 무시)  
if ! docker network inspect ubuntu_app-network >/dev/null 2>&1; then  
  docker network create ubuntu_app-network  
fi  

# 함수 정의하기  
# 새로 배포된 WAS가 실행 중인지 확인하는 함수  
check_execute_was() {  
  docker ps --filter "name=$WAS_NAME-${NEW_COLOR}" --format "{{.Status}}" | grep -q "^Up"  
}  

# API 상태를 확인하는 함수  
check_api_status() {  
  local inspect=$(docker inspect $WAS_NAME-${NEW_COLOR} --format '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}')  
  local endpoint="http://$inspect:8080/api/v1/open/health-check"  
  curl -s -o /dev/null -w "%{http_code}" "$endpoint" | grep -q '^200'  
}  

# 현재 실행 중인 WAS의 색상을 확인하기  
CURRENT_COLOR=$(docker ps --filter "name=$WAS_NAME" --format "{{.Names}}" | grep -E 'blue|green' | cut -d'-' -f2)  

# 새로 배포할 색상 결정하기  
if [ -z "$CURRENT_COLOR" ]; then  
  NEW_COLOR="blue"  
else  
  NEW_COLOR=$([ "$CURRENT_COLOR" = "blue" ] && echo "green" || echo "blue")  
fi  

# 새 버전 배포하기  
echo "Deploying new version: $NEW_COLOR"  
$COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${NEW_COLOR}.yml" pull  
$COMPOSE_BIN -p $PROJECT_NAME -f docker-compose.yml -f "docker-compose.${NEW_COLOR}.yml" up -d  

# 새 버전 상태 확인하기 (최대 10번 재시도)  
attempts=0  
while [ $attempts -lt $MAX_RETRIES ]; do  
  if check_execute_was; then  
    sleep 2  
    if check_api_status; then  
      echo "New version is up and healthy"  
      break  
    fi  
  fi  
  attempts=$((attempts + 1))  
  sleep 2  
done  

# 배포 실패 시 종료하기  
if [ $attempts -ge $MAX_RETRIES ]; then  
  echo "Failed to deploy new version"  
  exit 1  
fi  

# Caddy 설정 변경 & 서비스 재시작  
if [ "$NEW_COLOR" = "blue" ]; then  
  echo "mb"  
  mntbg # 블루 버전의 Caddy 설정 적용  
else  
  echo "mg"  
  mntdg # 그린 버전의 Caddy 설정 적용  
fi  

mntms # Caddy 서비스를 재시작하여 새 설정 적용하기  

# 이전 버전 컨테이너 정리하기  
if [ -n "$CURRENT_COLOR" ]; then  
  echo "Removing old version: $CURRENT_COLOR"  
  docker network disconnect ubuntu_app-network ${WAS_NAME}-${CURRENT_COLOR} || true  
  docker stop ${WAS_NAME}-${CURRENT_COLOR} || echo "Failed to stop container"  
  docker rm -f ${WAS_NAME}-${CURRENT_COLOR} || echo "Failed to remove container"  
fi  

echo "Deployment completed successfully."

이렇게 하면 redis가 추가된 환경에서의 무중단 배포가 잘 작동합니다.

 

이후에 해결할 문제

  • Blue/Green 배포 중 서버를 전환할 때, 기존 웹소켓 연결이 끊어지는 문제가 보고되어서 해결책을 찾으면 좋겠다는 생각을 했습니다.
  • 인프라를 수정하면서 서버 500 에러가 터질때마다 다른 팀원에게 양해를 구하는 시간이 많았습니다.
    • 인프라 수정시에 배포 서버에서 테스트하는 것이 아닌 테스트 서버가 있으면 좋겠다는 생각을 했습니다.

 

결론

  • 인프라를 수정하면서 내 담당이 아니더라도 프로젝트의 인프라에 대한 이해가 필요하다는 생각이 들었습니다.
    • 개발자가 하는 것은 잘 작동하는 결과물 뿐 아니라 결과물이 동작할 환경이라는 것을 깨달았습니다.
  • ci, cd 파이프라인을 수정하면서 에러를 많이 만났지만 생각했던 것만큼 두렵지는 않았고 오히려 지식을 쌓아가는 과정에서 얻는 것이 많았습니다.
728x90