문제&해결
Redis 도입하기 : CI/CD 파이프라인 수정기 (Blue/Green 무중단 배포)
mint*
2024. 10. 8. 20:07
728x90
서론
팀 프로젝트에 OAuth 기능을 도입하며 Redis를 추가할 상황이 생겼습니다.
⬇️
이 과정에서 기존 인프라를 수정할 필요가 있었는데, 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