본문 바로가기
NestJS

Github Actions를 사용한 CI/CD - 롤백, 모니터링, 로그 정리

by Programmer.Junny 2025. 7. 2.

앞서 CI/CD를 구성하였고, Github Actions를 통해 NestJS 서버를 Docker Container에 재설치하는 것을 진행하였다.

이번엔 추가적으로 CI/CD가 실패했을 때 다시 이전 백업으로 롤백하는 기능과 시스템, Docker Container, API 등의 상태를 지속적으로 확인할 수 있는 모니터링, 주기적으로 로그들을 정리해주는 로그 정리 등의 기능을 구축해보도록 하겠다.

1. 롤백 스크립트 작성

이제 추가적으로 배포에 문제가 생겼을 때 롤백되는 스크립트를 작성하도록 해보자.

1.1. 프로젝트 경로 이동

cd /volume1/docker/NestJS_SNS

터미널에서 ssh로 NAS에 접속하여 프로젝트 경로로 이동한다.

1.2. 롤백 스크립트 작성

cat > rollback.sh << 'EOF'
#!/bin/bash

# 인수 확인 및 설정
if [ $# -lt 1 ]; then
    echo "사용법: $0 <sudo_password> [ddns_host]"
    echo "예시: $0 'your_password' 'xxx.synology.me'"
    echo "      $0 'your_password'  # 기본값: xxx.synology.me 사용"
    exit 1
fi

# 비밀번호를 첫 번째 인수로 받기 (deploy.sh와 일관성)
SUDO_PASSWORD="$1"

# DDNS 호스트를 두 번째 인수로 받기 (기본값: xxx.synology.me)
DDNS_HOST="${2:-xxx.synology.me}"

# 설정 변수
PROJECT_PATH="/volume1/docker/NestJS_SNS"
LOG_FILE="$PROJECT_PATH/logs/rollback.log"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')

# 역방향 프록시 DDNS 설정
API_URL="https://$DDNS_HOST"
DOCS_URL="https://$DDNS_HOST:444"

# 보존할 컨테이너들 (데이터베이스 컨테이너들)
PRESERVE_CONTAINERS="sns-dev-postgres sns-dev-redis"

# 로그 디렉토리 생성
mkdir -p "$PROJECT_PATH/logs"

echo "=====================================" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 🔄 롤백 프로세스 시작" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 🌍 DDNS 호스트: $DDNS_HOST" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 📡 API URL: $API_URL" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 📚 문서 URL: $DOCS_URL" | tee -a $LOG_FILE
echo "=====================================" | tee -a $LOG_FILE

# 프로젝트 디렉토리로 이동
cd $PROJECT_PATH || {
    echo "[$TIMESTAMP] ❌ 프로젝트 디렉토리 접근 실패: $PROJECT_PATH" | tee -a $LOG_FILE
    exit 1
}

# Docker 심볼릭 링크 생성 (deploy.sh와 동일)
echo "[$TIMESTAMP] 🔧 Docker 심볼릭 링크 확인" | tee -a $LOG_FILE
if [ ! -L "/usr/bin/docker" ]; then
    echo "$SUDO_PASSWORD" | sudo -S ln -sf /usr/local/bin/docker /usr/bin/docker
    echo "[$TIMESTAMP] ✅ Docker 심볼릭 링크 생성" | tee -a $LOG_FILE
fi

if [ ! -L "/usr/bin/docker-compose" ]; then
    echo "$SUDO_PASSWORD" | sudo -S ln -sf /usr/local/bin/docker-compose /usr/bin/docker-compose
    echo "[$TIMESTAMP] ✅ Docker Compose 심볼릭 링크 생성" | tee -a $LOG_FILE
fi

# Docker 명령어 경로 확인 (deploy.sh와 동일)
echo "[$TIMESTAMP] 🔧 Docker 명령어 경로 확인" | tee -a $LOG_FILE
echo "[$TIMESTAMP] Docker 경로: $(which docker)" | tee -a $LOG_FILE
echo "[$TIMESTAMP] Docker Compose 경로: $(which docker-compose)" | tee -a $LOG_FILE

# 현재 서비스 상태 확인
echo "[$TIMESTAMP] 📊 현재 서비스 상태 확인" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker-compose ps | tee -a $LOG_FILE

# 백업 이미지 목록 확인
echo "[$TIMESTAMP] 💾 사용 가능한 백업 이미지 확인" | tee -a $LOG_FILE
BACKUP_IMAGES=$(echo "$SUDO_PASSWORD" | sudo -S docker images | grep "sns-dev-app.*backup-" | head -5)

if [ -z "$BACKUP_IMAGES" ]; then
    echo "[$TIMESTAMP] ❌ 사용 가능한 백업 이미지가 없습니다." | tee -a $LOG_FILE
    echo "[$TIMESTAMP] 💡 Git 기반 롤백 옵션:" | tee -a $LOG_FILE
    
    # Git 커밋 히스토리 보여주기
    echo "[$TIMESTAMP] 📜 최근 커밋 히스토리:" | tee -a $LOG_FILE
    git log --oneline -10 | tee -a $LOG_FILE
    
    echo "[$TIMESTAMP] 💭 수동 Git 롤백 방법:" | tee -a $LOG_FILE
    echo "  1. git reset --hard <commit-hash>" | tee -a $LOG_FILE
    echo "  2. ./deploy.sh <password> <ddns_host>" | tee -a $LOG_FILE
    echo "[$TIMESTAMP] ⚠️ 백업 이미지 생성을 위해 deploy.sh를 다시 실행하세요." | tee -a $LOG_FILE
    
    exit 1
fi

echo "[$TIMESTAMP] 📋 백업 이미지 목록:" | tee -a $LOG_FILE
echo "$BACKUP_IMAGES" | tee -a $LOG_FILE

# 사용자에게 백업 선택 옵션 제공
echo "[$TIMESTAMP] 🎯 백업 이미지 선택 옵션:" | tee -a $LOG_FILE
echo "$BACKUP_IMAGES" | nl -w2 -s'. ' | tee -a $LOG_FILE

# 가장 최근 백업을 기본값으로 사용
LATEST_BACKUP=$(echo "$BACKUP_IMAGES" | head -1 | awk '{print $1":"$2}')
BACKUP_TAG=$(echo "$BACKUP_IMAGES" | head -1 | awk '{print $2}')
echo "[$TIMESTAMP] 🎯 선택된 백업: $LATEST_BACKUP" | tee -a $LOG_FILE

# 현재 상태 임시 백업 (롤백 전 안전장치)
echo "[$TIMESTAMP] 💾 현재 상태 임시 백업 (안전장치)" | tee -a $LOG_FILE
ROLLBACK_BACKUP="rollback-backup-$(date +%Y%m%d-%H%M%S)"
if echo "$SUDO_PASSWORD" | sudo -S docker tag sns-dev-app:latest sns-dev-app:$ROLLBACK_BACKUP 2>/dev/null; then
    echo "[$TIMESTAMP] ✅ 현재 이미지 백업 성공: $ROLLBACK_BACKUP" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ⚠️ 현재 이미지 백업 실패 (계속 진행)" | tee -a $LOG_FILE
fi

# 보존할 컨테이너 상태 확인
echo "[$TIMESTAMP] 🛡️ 보존할 컨테이너 상태 확인" | tee -a $LOG_FILE
for container in $PRESERVE_CONTAINERS; do
    if echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then
        CONTAINER_STATUS=$(echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "^${container}" | cut -f2)
        echo "[$TIMESTAMP] 🛡️ 보존된 컨테이너: $container - $CONTAINER_STATUS" | tee -a $LOG_FILE
    else
        echo "[$TIMESTAMP] ⚠️ 컨테이너 없음: $container" | tee -a $LOG_FILE
    fi
done

# 애플리케이션 컨테이너만 중지
echo "[$TIMESTAMP] 🛑 애플리케이션 컨테이너만 중지 (DB 보존)" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker-compose stop app monitor | tee -a $LOG_FILE

# 기존 애플리케이션 컨테이너 제거
CLEANUP_CONTAINERS="sns-dev-app sns-dev-monitor"
for container in $CLEANUP_CONTAINERS; do
    if echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "{{.Names}}" | grep -q "^${container}$"; then
        echo "[$TIMESTAMP] 🗑️ 기존 컨테이너 제거: $container" | tee -a $LOG_FILE
        echo "$SUDO_PASSWORD" | sudo -S docker rm -f "$container" | tee -a $LOG_FILE
    fi
done

# 백업 이미지로 복원
echo "[$TIMESTAMP] 🔄 백업 이미지로 복원: $BACKUP_TAG" | tee -a $LOG_FILE
if echo "$SUDO_PASSWORD" | sudo -S docker tag sns-dev-app:$BACKUP_TAG sns-dev-app:latest; then
    echo "[$TIMESTAMP] ✅ 이미지 태그 변경 성공" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ❌ 이미지 태그 변경 실패" | tee -a $LOG_FILE
    exit 1
fi

# 서비스 시작 (DB 컨테이너는 유지된 상태)
echo "[$TIMESTAMP] 🚀 서비스 시작" | tee -a $LOG_FILE
if echo "$SUDO_PASSWORD" | sudo -S docker-compose up -d; then
    echo "[$TIMESTAMP] ✅ 서비스 시작 성공" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ❌ 서비스 시작 실패" | tee -a $LOG_FILE
    exit 1
fi

# 서비스 시작 대기 (HTTPS 기반으로 변경)
echo "[$TIMESTAMP] ⏳ 서비스 시작 대기 중..." | tee -a $LOG_FILE
for i in {1..12}; do  # 최대 60초 대기 (5초씩 12번)
    # HTTPS 헬스체크로 변경 (SSL 검증 무시)
    if curl -k -f "$API_URL/health" > /dev/null 2>&1; then
        echo "[$TIMESTAMP] ✅ 서비스 시작 완료 ($((i*5))초 소요)" | tee -a $LOG_FILE
        break
    else
        echo "[$TIMESTAMP] ⏳ 서비스 시작 대기 중... ($((i*5))초)" | tee -a $LOG_FILE
        
        # 5번째 시도 후부터 컨테이너 로그 확인
        if [ $i -eq 5 ]; then
            echo "[$TIMESTAMP] 🔍 애플리케이션 컨테이너 로그 확인 (최근 10줄)" | tee -a $LOG_FILE
            echo "$SUDO_PASSWORD" | sudo -S docker logs --tail=10 sns-dev-app 2>&1 | tee -a $LOG_FILE || echo "앱 컨테이너 로그를 가져올 수 없습니다." | tee -a $LOG_FILE
        fi
        
        sleep 5
    fi
    
    if [ $i -eq 12 ]; then
        echo "[$TIMESTAMP] ⚠️ 서비스 시작 시간이 오래 걸리고 있습니다." | tee -a $LOG_FILE
        
        # 대체 헬스체크: 내부 네트워크로 시도
        echo "[$TIMESTAMP] 🔄 내부 네트워크 헬스체크 시도" | tee -a $LOG_FILE
        if curl -f http://localhost:3000/health > /dev/null 2>&1; then
            echo "[$TIMESTAMP] ✅ 내부 네트워크 헬스체크 통과 (역방향 프록시 설정 확인 필요)" | tee -a $LOG_FILE
            break
        else
            echo "[$TIMESTAMP] ❌ 내부 네트워크 헬스체크도 실패" | tee -a $LOG_FILE
            
            # 최종 디버깅 정보
            echo "[$TIMESTAMP] 🔍 최종 디버깅 정보" | tee -a $LOG_FILE
            echo "[$TIMESTAMP] 컨테이너 상세 상태:" | tee -a $LOG_FILE
            echo "$SUDO_PASSWORD" | sudo -S docker ps -a --format "table {{.Names}}\t{{.Status}}\t{{.Ports}}" | tee -a $LOG_FILE
            
            echo "[$TIMESTAMP] ❌ 롤백 후 서비스 시작 타임아웃" | tee -a $LOG_FILE
            exit 1
        fi
    fi
done

# 종합적인 헬스체크 수행
echo "[$TIMESTAMP] 🔍 롤백 후 종합 헬스체크" | tee -a $LOG_FILE

# HTTPS API 헬스체크
echo "[$TIMESTAMP] 🔍 NestJS API 헬스체크 (HTTPS)" | tee -a $LOG_FILE
if curl -k -f "$API_URL/health" > /dev/null 2>&1; then
    echo "[$TIMESTAMP] ✅ NestJS API HTTPS 헬스체크 통과" | tee -a $LOG_FILE
    
    # API 응답 내용도 확인
    API_RESPONSE=$(curl -k -s "$API_URL/health")
    echo "[$TIMESTAMP] 🧪 API 응답: $API_RESPONSE" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ❌ NestJS API HTTPS 헬스체크 실패" | tee -a $LOG_FILE
    
    # 대체 헬스체크: 내부 네트워크로 시도
    echo "[$TIMESTAMP] 🔄 내부 네트워크 헬스체크 시도" | tee -a $LOG_FILE
    if curl -f http://localhost:3000/health > /dev/null 2>&1; then
        echo "[$TIMESTAMP] ✅ 내부 네트워크 헬스체크 통과 (역방향 프록시 설정 확인 필요)" | tee -a $LOG_FILE
        API_RESPONSE=$(curl -s http://localhost:3000/health)
        echo "[$TIMESTAMP] 🧪 API 응답: $API_RESPONSE" | tee -a $LOG_FILE
    else
        echo "[$TIMESTAMP] ❌ 내부 네트워크 헬스체크도 실패" | tee -a $LOG_FILE
        exit 1
    fi
fi

# Swagger UI 접근 확인
echo "[$TIMESTAMP] 🔍 Swagger UI 접근 확인" | tee -a $LOG_FILE
SWAGGER_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$API_URL/api" 2>/dev/null)
if [ "$SWAGGER_STATUS" = "200" ] || [ "$SWAGGER_STATUS" = "401" ]; then
    echo "[$TIMESTAMP] ✅ Swagger UI 접근 가능 (HTTP $SWAGGER_STATUS)" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ⚠️ Swagger UI 접근 상태: HTTP $SWAGGER_STATUS" | tee -a $LOG_FILE
fi

# GraphQL 접근 확인
echo "[$TIMESTAMP] 🔍 GraphQL 접근 확인" | tee -a $LOG_FILE
GRAPHQL_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$API_URL/graphql" 2>/dev/null)
if [ "$GRAPHQL_STATUS" = "200" ] || [ "$GRAPHQL_STATUS" = "400" ] || [ "$GRAPHQL_STATUS" = "405" ]; then
    echo "[$TIMESTAMP] ✅ GraphQL 엔드포인트 접근 가능 (HTTP $GRAPHQL_STATUS)" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ⚠️ GraphQL 엔드포인트 상태: HTTP $GRAPHQL_STATUS" | tee -a $LOG_FILE
fi

# Compodoc 문서 접근 확인
echo "[$TIMESTAMP] 🔍 Compodoc 문서 접근 확인" | tee -a $LOG_FILE
DOCS_STATUS=$(curl -k -s -o /dev/null -w "%{http_code}" "$DOCS_URL/" 2>/dev/null)
if [ "$DOCS_STATUS" = "200" ]; then
    echo "[$TIMESTAMP] ✅ Compodoc 문서 접근 가능 (HTTP $DOCS_STATUS)" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ⚠️ Compodoc 문서 상태: HTTP $DOCS_STATUS" | tee -a $LOG_FILE
fi

# 데이터베이스 헬스체크
echo "[$TIMESTAMP] 🔍 데이터베이스 헬스체크" | tee -a $LOG_FILE
if echo "$SUDO_PASSWORD" | sudo -S docker exec sns-dev-postgres pg_isready -U postgres > /dev/null 2>&1; then
    echo "[$TIMESTAMP] ✅ PostgreSQL 헬스체크 통과" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ❌ PostgreSQL 헬스체크 실패" | tee -a $LOG_FILE
fi

# Redis 헬스체크
if echo "$SUDO_PASSWORD" | sudo -S docker exec sns-dev-redis redis-cli ping > /dev/null 2>&1; then
    echo "[$TIMESTAMP] ✅ Redis 헬스체크 통과" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ❌ Redis 헬스체크 실패" | tee -a $LOG_FILE
fi

# 모니터링 컨테이너 상태 확인
echo "[$TIMESTAMP] 🔍 모니터링 컨테이너 상태 확인" | tee -a $LOG_FILE
MONITOR_STATUS=$(echo "$SUDO_PASSWORD" | sudo -S docker ps --format "{{.Names}}\t{{.Status}}" | grep "sns-dev-monitor" || echo "없음")
if [ "$MONITOR_STATUS" != "없음" ]; then
    echo "[$TIMESTAMP] ✅ 모니터링 컨테이너: $MONITOR_STATUS" | tee -a $LOG_FILE
else
    echo "[$TIMESTAMP] ⚠️ 모니터링 컨테이너가 실행되지 않았습니다." | tee -a $LOG_FILE
fi

# 최종 컨테이너 상태 확인
echo "[$TIMESTAMP] 📋 롤백 후 최종 컨테이너 상태" | tee -a $LOG_FILE
echo "$SUDO_PASSWORD" | sudo -S docker-compose ps | tee -a $LOG_FILE

# 롤백 정보 출력 (HTTPS 우선)
echo "[$TIMESTAMP] 🌐 롤백된 서비스 접근 정보 (역방향 프록시)" | tee -a $LOG_FILE
echo "  🔒 HTTPS 접근 (권장):" | tee -a $LOG_FILE
echo "    📡 NestJS API: $API_URL" | tee -a $LOG_FILE
echo "    📋 Swagger UI: $API_URL/api" | tee -a $LOG_FILE
echo "    🚀 GraphQL: $API_URL/graphql" | tee -a $LOG_FILE
echo "    🔍 Health Check: $API_URL/health" | tee -a $LOG_FILE
echo "    📚 Compodoc: $DOCS_URL" | tee -a $LOG_FILE
echo "  📊 모니터링:" | tee -a $LOG_FILE
echo "    🔍 모니터링 로그: docker exec sns-dev-monitor tail -f /monitor/logs/monitor.log" | tee -a $LOG_FILE
echo "    📈 실시간 모니터링: docker exec sns-dev-monitor ./monitor.sh" | tee -a $LOG_FILE

# Git 상태 정보
echo "[$TIMESTAMP] 📊 현재 Git 상태" | tee -a $LOG_FILE
git log --oneline -3 | tee -a $LOG_FILE

echo "=====================================" | tee -a $LOG_FILE
echo "[$TIMESTAMP] 🎉 롤백 프로세스 완료" | tee -a $LOG_FILE
echo "=====================================" | tee -a $LOG_FILE

# 롤백 성공 알림
echo "[$TIMESTAMP] 📢 롤백 완료 알림" | tee -a $LOG_FILE
echo "  - 이전 백업으로 서비스가 복원되었습니다." | tee -a $LOG_FILE
echo "  - 사용된 백업 이미지: $BACKUP_TAG" | tee -a $LOG_FILE
echo "  - 안전장치 백업: $ROLLBACK_BACKUP" | tee -a $LOG_FILE
echo "  - 데이터베이스 컨테이너는 보존되었습니다." | tee -a $LOG_FILE
echo "  - DDNS 호스트: $DDNS_HOST" | tee -a $LOG_FILE
echo "  - 현재 서비스가 정상 동작하는지 확인하세요." | tee -a $LOG_FILE

# 명시적으로 성공 상태 반환
echo "[$TIMESTAMP] 📤 롤백 성공 상태 반환 (Exit Code: 0)" | tee -a $LOG_FILE
exit 0
EOF

이전 백업이 있는 경우 이전 백업으로 복원하게 된다.

1.3. 권한 설정

chmod +x rollback.sh

2. 모니터링 스크립트 작성

2.1. 프로젝트 경로 이동

cd /volume1/docker/NestJS_SNS

2.2. Dockerfile.monitor 작성

cat > Dockerfile.monitor << 'EOF'
FROM alpine:3.19

RUN apk add --no-cache \
    bash \
    curl \
    git \
    docker-cli \
    findutils \
    coreutils \
    gzip \
    procps

# 작업 디렉토리 설정
WORKDIR /monitor

# 스크립트 복사
COPY monitor.sh /monitor/monitor.sh
COPY log-rotate.sh /monitor/log-rotate.sh

# 실행 권한 부여
RUN chmod +x /monitor/monitor.sh /monitor/log-rotate.sh

# 기본 명령어
CMD ["sh", "-c", "while true; do ./monitor.sh; sleep ${MONITOR_INTERVAL:-60}; done"]
EOF

docker-compose.yaml이 실행되어 컨테이너에서 Dockerfile.monitor 파일이 실행된다.

Dockerfile.monitorrun_monitor.sh 파일을 실행한다.

2.3. run_monitor.sh 작성

cat > run_monitor.sh << 'EOF'
#!/bin/bash

echo "🔍 Docker 모니터링 컨테이너 시작"
echo "📅 시작 시간: $(date '+%Y-%m-%d %H:%M:%S')"
echo "⏰ 1분마다 모니터링 실행"
echo "🐳 컨테이너: $(hostname)"
echo "========================================="

# 초기 대기 (다른 서비스들이 시작될 시간)
echo "⏳ 초기 대기 중... (60초)"
sleep 60

# 초기 실행
echo "🔄 첫 번째 모니터링 실행"
/monitor/monitor.sh

# 1분마다 반복 실행
while true; do
    sleep 60
    echo ""
    echo "🔄 모니터링 갱신: $(date '+%Y-%m-%d %H:%M:%S')"
    echo "========================================="
    /monitor/monitor.sh
done
EOF
//권한 설정
chmod +x run_monitor.sh

run_monitor.sh 스크립트는 컨테이너에서 주기적으로 반복적으로 실행되며, monitor.sh 파일을 호출하여 상태를 보여준다.

2.4. monitor.sh 작성

cat > monitor.sh << 'EOF'
#!/bin/bash

# 환경변수에서 설정 가져오기 (DDNS 중심)
PROJECT_PATH="${PROJECT_PATH:-/project}"
DDNS_HOST="${DDNS_HOST:-xxx.synology.me}"
MONITOR_INTERVAL="${MONITOR_INTERVAL:-60}"
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-7}"
HEALTH_CHECK_TIMEOUT="${HEALTH_CHECK_TIMEOUT:-10}"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
LOG_FILE="/monitor/logs/monitor.log"

# DDNS 기반 URL 구성
API_URL="https://$DDNS_HOST"
DOCS_URL="https://$DDNS_HOST:444"

# 로그 디렉토리 생성
mkdir -p "/monitor/logs"

# Git 설정 초기화 (환경변수 기반)
setup_git_config() {
    local git_user_name="${GIT_USER_NAME:-CI/CD Monitor}"
    local git_user_email="${GIT_USER_EMAIL:-monitor@cicd.local}"
    local git_safe_dir="${GIT_SAFE_DIRECTORY:-/project}"
    
    # Git 설정이 없으면 초기화
    if ! git config --global user.name >/dev/null 2>&1; then
        git config --global user.name "$git_user_name"
        echo "🔧 Git user.name 설정: $git_user_name" >> $LOG_FILE
    fi
    
    if ! git config --global user.email >/dev/null 2>&1; then
        git config --global user.email "$git_user_email"
        echo "🔧 Git user.email 설정: $git_user_email" >> $LOG_FILE
    fi
    
    # Safe directory 설정
    if [ -d "$git_safe_dir/.git" ]; then
        git config --global --add safe.directory "$git_safe_dir" 2>/dev/null || true
        echo "🔧 Git safe directory 설정: $git_safe_dir" >> $LOG_FILE
    fi
}

# Git 설정 실행
setup_git_config

# 자동 로그 정리 (하루에 한 번, 새벽 3시경 또는 수동 실행시)
if [ $(date +%H:%M) = "03:00" ] || [ ! -f "/monitor/.last_cleanup" ] || [ $(find "/monitor/.last_cleanup" -mtime +1 2>/dev/null | wc -l) -gt 0 ]; then
    echo "🧹 자동 로그 정리 시작..."
    if [ -x "/monitor/log-rotate.sh" ]; then
        /monitor/log-rotate.sh
        touch "/monitor/.last_cleanup"
        echo "✅ 자동 로그 정리 완료"
    else
        echo "⚠️ log-rotate.sh 스크립트를 찾을 수 없습니다."
    fi
fi

# 헬퍼 함수들 (HEALTH_CHECK_TIMEOUT 사용)
check_https_service() {
    local service_name="$1"
    local url="$2"
    local timeout="${3:-$HEALTH_CHECK_TIMEOUT}"
    
    local status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
    if [ "$status" = "200" ]; then
        echo "✅ $service_name: 정상 (HTTPS $status)"
        return 0
    else
        echo "❌ $service_name: 오류 (HTTPS $status)"
        # 상세 디버깅 정보 추가
        if [ "$status" = "401" ]; then
            echo "     └─ 💡 401 Unauthorized: 인증이 필요하거나 경로가 잘못되었을 수 있습니다"
        elif [ "$status" = "404" ]; then
            echo "     └─ 💡 404 Not Found: 경로가 존재하지 않습니다"
        elif [ "$status" = "000" ]; then
            echo "     └─ 💡 연결 실패: 서버에 연결할 수 없습니다"
        fi
        return 1
    fi
}

# Swagger UI 전용 체크 함수 (401도 정상으로 처리)
check_swagger_ui() {
    local service_name="$1"
    local url="$2"
    local timeout="${3:-$HEALTH_CHECK_TIMEOUT}"
    
    local status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
    local response=$(curl -s --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
    
    # 200 또는 401을 정상적인 Swagger UI 응답으로 처리
    if [ "$status" = "200" ] || [ "$status" = "401" ]; then
        # HTML 응답에서 Swagger 관련 키워드 확인
        if echo "$response" | grep -qi "swagger\|openapi\|unauthorized\|api.*documentation"; then
            echo "✅ $service_name: 정상 (HTTPS $status) - Swagger UI 확인됨"
            if [ "$status" = "401" ]; then
                echo "     └─ 💡 인증이 필요한 Swagger UI입니다 (브라우저 접근 권장)"
            fi
            return 0
        elif [ "$status" = "200" ]; then
            echo "✅ $service_name: 정상 (HTTPS $status) - HTML 응답 확인"
            return 0
        else
            echo "⚠️ $service_name: 응답 확인 필요 (HTTPS $status)"
            echo "     └─ 💡 HTML 응답이 있지만 Swagger 키워드를 찾을 수 없습니다"
            return 0
        fi
    else
        echo "❌ $service_name: 오류 (HTTPS $status)"
        if [ "$status" = "404" ]; then
            echo "     └─ 💡 404 Not Found: 경로가 존재하지 않습니다"
        elif [ "$status" = "000" ]; then
            echo "     └─ 💡 연결 실패: 서버에 연결할 수 없습니다"
        fi
        return 1
    fi
}

# GraphQL 전용 체크 함수 (400, 405도 정상으로 처리)
check_graphql() {
    local service_name="$1"
    local url="$2"
    local timeout="${3:-$HEALTH_CHECK_TIMEOUT}"
    
    local status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
    local response=$(curl -s --connect-timeout $timeout --max-time $timeout -k "$url" 2>/dev/null)
    
    # 200, 400, 405를 정상적인 GraphQL 응답으로 처리
    if [ "$status" = "200" ] || [ "$status" = "400" ] || [ "$status" = "405" ]; then
        # 응답에서 GraphQL 관련 키워드 확인
        if echo "$response" | grep -qi "graphql\|playground\|query\|mutation\|subscription\|schema"; then
            echo "✅ $service_name: 정상 (HTTPS $status) - GraphQL 확인됨"
            if [ "$status" = "400" ]; then
                echo "     └─ 💡 400 Bad Request: GraphQL 쿼리가 필요합니다 (정상 응답)"
            elif [ "$status" = "405" ]; then
                echo "     └─ 💡 405 Method Not Allowed: POST 요청이 필요합니다 (정상 응답)"
            fi
            return 0
        elif echo "$response" | grep -qi "Cannot GET\|Method.*not.*allowed"; then
            echo "✅ $service_name: 정상 (HTTPS $status) - GraphQL 엔드포인트 확인됨"
            echo "     └─ 💡 GET 요청 불가 (GraphQL은 POST 필요)"
            return 0
        elif [ "$status" = "200" ]; then
            echo "✅ $service_name: 정상 (HTTPS $status) - 응답 확인"
            return 0
        else
            echo "⚠️ $service_name: 응답 확인 필요 (HTTPS $status)"
            echo "     └─ 💡 응답이 있지만 GraphQL 키워드를 찾을 수 없습니다"
            return 0
        fi
    else
        echo "❌ $service_name: 오류 (HTTPS $status)"
        if [ "$status" = "404" ]; then
            echo "     └─ 💡 404 Not Found: GraphQL 경로가 존재하지 않습니다"
        elif [ "$status" = "000" ]; then
            echo "     └─ 💡 연결 실패: 서버에 연결할 수 없습니다"
        fi
        return 1
    fi
}

# 컨테이너 내부 포트 체크 함수
check_container_port() {
    local port="$1"
    local service="$2"
    local container="$3"
    local timeout="${4:-$HEALTH_CHECK_TIMEOUT}"
    
    case $port in
        3000)
            if curl -s --connect-timeout $timeout "http://$container:3000/health" >/dev/null 2>&1; then
                echo "  ✅ 포트 $port ($service): 활성화"
            else
                echo "  ❌ 포트 $port ($service): 비활성화"
            fi
            ;;
        8080)
            if curl -s --connect-timeout $timeout "http://$container:8080/" >/dev/null 2>&1; then
                echo "  ✅ 포트 $port ($service): 활성화"
            else
                echo "  ❌ 포트 $port ($service): 비활성화"
            fi
            ;;
        5432)
            if docker exec "$container" pg_isready -U postgres >/dev/null 2>&1; then
                echo "  ✅ 포트 $port ($service): 활성화"
            else
                echo "  ❌ 포트 $port ($service): 비활성화"
            fi
            ;;
        6379)
            if docker exec "$container" redis-cli ping >/dev/null 2>&1; then
                echo "  ✅ 포트 $port ($service): 활성화"
            else
                echo "  ❌ 포트 $port ($service): 비활성화"
            fi
            ;;
        *)
            echo "  ❌ 포트 $port ($service): 비활성화"
            ;;
    esac
}

# 자동 로그 정리 (하루에 한 번, 새벽 3시경)
if [ $(date +%H:%M) = "03:00" ] || [ ! -f "/monitor/.last_cleanup" ] || [ $(find "/monitor/.last_cleanup" -mtime +1 2>/dev/null | wc -l) -gt 0 ]; then
    cleanup_old_logs
    touch "/monitor/.last_cleanup"
fi

# 모니터링 시작 로그
echo "[$TIMESTAMP] 🔍 컨테이너 모니터링 시작 (간격: ${MONITOR_INTERVAL}초, 타임아웃: ${HEALTH_CHECK_TIMEOUT}초, 로그보관: ${LOG_RETENTION_DAYS}일)" | tee -a $LOG_FILE

echo "======================================"
echo "🔍 NAS CI/CD 모니터링 대시보드 (Container)"
echo "📅 시간: $TIMESTAMP"
echo "🌍 DDNS: $DDNS_HOST"
echo "🔒 API URL: $API_URL"
echo "📚 문서 URL: $DOCS_URL"
echo "🐳 모니터링 컨테이너: $(hostname)"
echo "⚙️  설정: 간격 ${MONITOR_INTERVAL}초 | 타임아웃 ${HEALTH_CHECK_TIMEOUT}초 | 로그보관 ${LOG_RETENTION_DAYS}일"
echo "======================================"

# 컨테이너 시스템 정보
echo ""
echo "🖥️  컨테이너 시스템 정보:"
echo "  - 컨테이너 ID: $(hostname)"
echo "  - 현재 사용자: $(whoami)"
echo "  - 시스템 시간: $(date '+%Y-%m-%d %H:%M:%S %Z')"

# Docker 컨테이너 상태 확인
echo ""
echo "🐳 Docker 컨테이너 상태:"

# 모든 SNS 관련 컨테이너 확인
SNS_CONTAINERS=$(docker ps -a --format "{{.Names}}\t{{.Status}}" | grep "sns-dev" 2>/dev/null)

if [ -n "$SNS_CONTAINERS" ]; then
    echo "$SNS_CONTAINERS" | while IFS=$'\t' read -r name status; do
        if echo "$status" | grep -q "Up"; then
            echo "  ✅ $name: $status"
        else
            echo "  ❌ $name: $status"
        fi
    done
else
    echo "  ⚠️ SNS 관련 컨테이너를 찾을 수 없습니다."
    echo "  💡 전체 컨테이너 확인:"
    docker ps -a --format "  {{.Names}}\t{{.Status}}" 2>/dev/null | head -5
fi

# 컨테이너 네트워크 내부 서비스 체크
echo ""
echo "🔍 서비스 헬스체크 (컨테이너 네트워크 내부):"

# API 서비스 체크 (컨테이너명으로 접근)
API_STATUS_INTERNAL=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT "http://sns-dev-app:3000/health" 2>/dev/null)
if [ "$API_STATUS_INTERNAL" = "200" ]; then
    API_RESPONSE=$(curl -s --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT "http://sns-dev-app:3000/health" 2>/dev/null)
    echo "  ✅ API 서버 (컨테이너 내부): 정상 (HTTP $API_STATUS_INTERNAL)"
    echo "     └─ 응답: $API_RESPONSE"
else
    echo "  ❌ API 서버 (컨테이너 내부): 오류 (HTTP $API_STATUS_INTERNAL)"
fi

# Compodoc 문서 서비스 체크
DOCS_STATUS_INTERNAL=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT "http://sns-dev-app:8080/" 2>/dev/null)
if [ "$DOCS_STATUS_INTERNAL" = "200" ]; then
    echo "  ✅ Compodoc 문서 (컨테이너 내부): 정상 (HTTP $DOCS_STATUS_INTERNAL)"
else
    echo "  ❌ Compodoc 문서 (컨테이너 내부): 오류 (HTTP $DOCS_STATUS_INTERNAL)"
fi

# HTTPS 접근 체크 (역방향 프록시를 통한 외부 접근)
echo ""
echo "🔒 HTTPS 접근 상태 (역방향 프록시):"

# HTTPS API 헬스체크
if check_https_service "HTTPS API 헬스체크" "$API_URL/health"; then
    echo "     └─ 주소: $API_URL/health"
fi

# HTTPS Swagger UI 체크 (401도 정상으로 처리)
echo ""
echo "🔍 Swagger UI 경로 체크 (401 허용):"
SWAGGER_PATHS=("/api" "/api-docs" "/swagger" "/docs")
SWAGGER_FOUND=false

for path in "${SWAGGER_PATHS[@]}"; do
    if check_swagger_ui "HTTPS Swagger UI ($path)" "$API_URL$path"; then
        echo "     └─ 주소: $API_URL$path"
        SWAGGER_FOUND=true
        SWAGGER_WORKING_PATH="$path"
        break
    fi
done

if [ "$SWAGGER_FOUND" = false ]; then
    echo "  ⚠️ 모든 Swagger 경로에서 접근 실패. 설정을 확인해주세요."
    # 대체 경로들의 상태코드 표시
    echo "  📋 경로별 상태코드:"
    for path in "${SWAGGER_PATHS[@]}"; do
        status=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL$path" 2>/dev/null)
        echo "     - $path: HTTP $status"
    done
fi

# HTTPS GraphQL 체크 (400, 405도 정상으로 처리)
echo ""
if check_graphql "HTTPS GraphQL" "$API_URL/graphql"; then
    echo "     └─ 주소: $API_URL/graphql"
fi

# HTTPS Compodoc 문서 체크 (444 포트)
if check_https_service "HTTPS Compodoc 문서" "$DOCS_URL/"; then
    echo "     └─ 주소: $DOCS_URL"
fi

# 데이터베이스 상태 확인
echo ""
echo "🗄️  데이터베이스 상태:"

# PostgreSQL 체크
if docker exec sns-dev-postgres pg_isready -U postgres >/dev/null 2>&1; then
    echo "  ✅ PostgreSQL: 정상"
    # 연결 수 확인
    DB_CONNECTIONS=$(docker exec sns-dev-postgres psql -U postgres -t -c "SELECT count(*) FROM pg_stat_activity;" 2>/dev/null | xargs || echo "확인불가")
    echo "     └─ 활성 연결: $DB_CONNECTIONS개"
    
    # 데이터베이스 크기 확인
    DB_SIZE=$(docker exec sns-dev-postgres psql -U postgres -t -c "SELECT pg_size_pretty(pg_database_size('postgres'));" 2>/dev/null | xargs || echo "확인불가")
    echo "     └─ DB 크기: $DB_SIZE"
else
    echo "  ❌ PostgreSQL: 오류 또는 연결 불가"
fi

# Redis 체크
if docker exec sns-dev-redis redis-cli ping >/dev/null 2>&1; then
    echo "  ✅ Redis: 정상"
    # Redis 메모리 사용량
    REDIS_MEMORY=$(docker exec sns-dev-redis redis-cli info memory 2>/dev/null | grep "used_memory_human" | cut -d: -f2 | tr -d '\r' || echo "확인불가")
    echo "     └─ 메모리 사용: $REDIS_MEMORY"
    
    # Redis 키 개수
    REDIS_KEYS=$(docker exec sns-dev-redis redis-cli dbsize 2>/dev/null || echo "확인불가")
    echo "     └─ 저장된 키: $REDIS_KEYS개"
else
    echo "  ❌ Redis: 오류 또는 연결 불가"
fi

# 컨테이너별 포트 확인 (HEALTH_CHECK_TIMEOUT 사용)
echo ""
echo "🔌 컨테이너별 서비스 포트:"
check_container_port 3000 "API" "sns-dev-app" $HEALTH_CHECK_TIMEOUT
check_container_port 8080 "문서" "sns-dev-app" $HEALTH_CHECK_TIMEOUT
check_container_port 5432 "PostgreSQL" "sns-dev-postgres"
check_container_port 6379 "Redis" "sns-dev-redis"

# Git 상태 (프로젝트 디렉토리에서)
echo ""
echo "📊 Git 저장소 상태:"

if [ -d "$PROJECT_PATH" ]; then
    cd "$PROJECT_PATH" 2>/dev/null || {
        echo "  ❌ 프로젝트 디렉토리 접근 실패: $PROJECT_PATH"
        return
    }
    
    if [ -d ".git" ]; then
        echo "  ✅ Git 저장소 확인됨"
        
        # Git 명령어 실행 - safe directory 설정이 적용된 상태
        CURRENT_BRANCH=$(git branch --show-current 2>/dev/null || echo "확인 불가")
        echo "  - 현재 브랜치: $CURRENT_BRANCH"
        
        LATEST_COMMIT=$(git log --oneline -1 2>/dev/null || echo "확인 불가")
        echo "  - 최근 커밋: $LATEST_COMMIT"
        
        UNCOMMITTED_CHANGES=$(git status --porcelain 2>/dev/null | wc -l)
        echo "  - 미커밋 변경사항: ${UNCOMMITTED_CHANGES}개"
        
        # 원격 저장소와의 차이 확인
        if git remote >/dev/null 2>&1; then
            REMOTE_URL=$(git remote get-url origin 2>/dev/null || echo "확인 불가")
            echo "  - 원격 저장소: $REMOTE_URL"
            
            # fetch 시도
            if git fetch >/dev/null 2>&1; then
                BEHIND_COUNT=$(git rev-list --count HEAD..origin/main 2>/dev/null || echo "0")
                AHEAD_COUNT=$(git rev-list --count origin/main..HEAD 2>/dev/null || echo "0")
                echo "  - 원격 대비: $BEHIND_COUNT개 뒤처짐, $AHEAD_COUNT개 앞섬"
            else
                echo "  - 원격 저장소 fetch 실패"
            fi
        else
            echo "  - 원격 저장소: 설정되지 않음"
        fi
    else
        echo "  ❌ Git 저장소가 아닙니다"
    fi
else
    echo "  ❌ 프로젝트 디렉토리가 존재하지 않습니다: $PROJECT_PATH"
fi

# 최근 로그 파일들
echo ""
echo "📝 최근 로그 상태:"

# 배포 로그
if [ -f "$PROJECT_PATH/logs/deploy.log" ]; then
    DEPLOY_LOG_SIZE=$(du -h "$PROJECT_PATH/logs/deploy.log" | cut -f1)
    DEPLOY_LOG_LINES=$(wc -l < "$PROJECT_PATH/logs/deploy.log")
    echo "  - 배포 로그: ${DEPLOY_LOG_SIZE} (${DEPLOY_LOG_LINES}줄)"
    echo "    └─ 마지막 5줄:"
    tail -5 "$PROJECT_PATH/logs/deploy.log" | sed 's/^/      /'
else
    echo "  - 배포 로그: 없음"
fi

# 롤백 로그
if [ -f "$PROJECT_PATH/logs/rollback.log" ]; then
    ROLLBACK_LOG_SIZE=$(du -h "$PROJECT_PATH/logs/rollback.log" | cut -f1)
    echo "  - 롤백 로그: $ROLLBACK_LOG_SIZE"
else
    echo "  - 롤백 로그: 없음"
fi

# 모니터링 로그 상태
if [ -f "$LOG_FILE" ]; then
    MONITOR_LOG_SIZE=$(du -h "$LOG_FILE" | cut -f1)
    MONITOR_LOG_LINES=$(wc -l < "$LOG_FILE")
    echo "  - 모니터링 로그: ${MONITOR_LOG_SIZE} (${MONITOR_LOG_LINES}줄)"
else
    echo "  - 모니터링 로그: 생성 중..."
fi

# 애플리케이션 에러 로그
echo ""
echo "🚨 최근 에러 로그:"
ERROR_LOGS=$(docker logs sns-dev-app --tail=20 2>/dev/null | grep -i "error\|exception\|fatal" | tail -3)
if [ -n "$ERROR_LOGS" ]; then
    echo "$ERROR_LOGS" | sed 's/^/  /'
else
    echo "  ✅ 최근 에러가 없습니다."
fi

# 리소스 사용량 상세
echo ""
echo "💾 저장공간 사용량:"
PROJECT_SIZE=$(du -sh "$PROJECT_PATH" 2>/dev/null | awk '{print $1}' || echo "확인 불가")
echo "  - 프로젝트 디렉토리: $PROJECT_SIZE ($PROJECT_PATH)"

# Docker 이미지 및 볼륨 정보
DOCKER_IMAGES_COUNT=$(docker images --format '{{.Repository}}' | grep -E '(sns|2_cf_sns)' | wc -l 2>/dev/null || echo "0")
DOCKER_VOLUMES_COUNT=$(docker volume ls | grep -E '(pnpm|redis)' | wc -l 2>/dev/null || echo "0")
echo "  - Docker 이미지: ${DOCKER_IMAGES_COUNT}개"
echo "  - Docker 볼륨: ${DOCKER_VOLUMES_COUNT}개"

# 서비스 접근 링크 (DDNS 기반)
echo ""
echo "🌐 서비스 접근 링크 (DDNS 기반):"
echo ""
echo "  🔒 HTTPS 접근 (역방향 프록시):"
echo "     📡 API 헬스체크:   $API_URL/health"
if [ "$SWAGGER_FOUND" = true ]; then
    echo "     📋 Swagger UI:     $API_URL$SWAGGER_WORKING_PATH ✅"
else
    echo "     📋 Swagger UI:     $API_URL/api (확인 필요)"
fi
echo "     🚀 GraphQL:        $API_URL/graphql"
echo "     📚 Compodoc 문서:  $DOCS_URL"

# 역방향 프록시 상태 요약
echo ""
echo "🔀 역방향 프록시 구성:"
echo "  - HTTPS → API (3000):    $API_URL/* → http://sns-dev-app:3000/*"
echo "  - HTTPS → 문서 (444):    $DOCS_URL/* → http://sns-dev-app:8080/*"
echo "  - SSL 인증서:           Synology DSM 관리"
echo "  - Swagger UI 인증:      401 상태는 정상 (브라우저 접근 권장)"
echo "  - GraphQL 응답:         400/405 상태는 정상 (POST 요청 필요)"

# 상태 요약
echo ""
echo "📊 상태 요약:"
OVERALL_STATUS="✅ 정상"

# HTTPS 접근 상태 확인 (HEALTH_CHECK_TIMEOUT 사용)
HTTPS_API_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL/health" 2>/dev/null)
HTTPS_SWAGGER_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL/api" 2>/dev/null)
HTTPS_GRAPHQL_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$API_URL/graphql" 2>/dev/null)
HTTPS_DOCS_STATUS=$(curl -s -o /dev/null -w "%{http_code}" --connect-timeout $HEALTH_CHECK_TIMEOUT --max-time $HEALTH_CHECK_TIMEOUT -k "$DOCS_URL/" 2>/dev/null)

if [ "$API_STATUS_INTERNAL" != "200" ]; then
    OVERALL_STATUS="⚠️ API 서버 문제"
elif [ "$DOCS_STATUS_INTERNAL" != "200" ]; then
    OVERALL_STATUS="⚠️ 문서 서버 문제"
elif [ "$HTTPS_API_STATUS" != "200" ]; then
    OVERALL_STATUS="⚠️ HTTPS API 프록시 문제"
elif [ "$HTTPS_DOCS_STATUS" != "200" ]; then
    OVERALL_STATUS="⚠️ HTTPS 문서 프록시 문제"
fi

echo "  - 전체 상태: $OVERALL_STATUS"
echo "  - API 상태 (내부): $([ "$API_STATUS_INTERNAL" = "200" ] && echo "정상" || echo "문제")"
echo "  - 문서 상태 (내부): $([ "$DOCS_STATUS_INTERNAL" = "200" ] && echo "정상" || echo "문제")"
echo "  - HTTPS API 상태: $([ "$HTTPS_API_STATUS" = "200" ] && echo "정상" || echo "문제")"
echo "  - HTTPS Swagger 상태: $([ "$HTTPS_SWAGGER_STATUS" = "200" ] || [ "$HTTPS_SWAGGER_STATUS" = "401" ] && echo "정상" || echo "문제")"
echo "  - HTTPS GraphQL 상태: $([ "$HTTPS_GRAPHQL_STATUS" = "200" ] || [ "$HTTPS_GRAPHQL_STATUS" = "400" ] || [ "$HTTPS_GRAPHQL_STATUS" = "405" ] && echo "정상" || echo "문제")"
echo "  - HTTPS 문서 상태: $([ "$HTTPS_DOCS_STATUS" = "200" ] && echo "정상" || echo "문제")"
echo "  - 마지막 체크: $TIMESTAMP"
echo "  - 다음 체크: $(date -d "+${MONITOR_INTERVAL} seconds" '+%Y-%m-%d %H:%M:%S' 2>/dev/null || date '+%Y-%m-%d %H:%M:%S')"

echo ""
echo "======================================"
echo "✅ 모니터링 완료: $TIMESTAMP"
echo "======================================"

# 모니터링 완료 로그
echo "[$TIMESTAMP] 🔍 컨테이너 모니터링 완료 - 상태: $OVERALL_STATUS" | tee -a $LOG_FILE

exit 0
EOF
// 권한 설정
chmod +x monitor.sh

monitor.sh 스크립트를 실행하면, 컨테이너, 각종 API 등의 상태를 확인 후 보여준다.

완료되면 모니터링 로그를 로그 파일에 저장한다.

2.5. docker-compose.yaml 수정

cat > docker-compose.yaml << 'EOF'
version: '3.8'
 
services:
  app:
    build: .
    container_name: sns-dev-app
    restart: unless-stopped
    ports:
      - "3000:3000"
      - "8080:8080"
    environment:
      - NODE_ENV=dev
    env_file:
      - src/configs/env/.dev.env
    volumes:
      - ./src:/app/src
      - ./logs:/app/logs
      - ./uploads:/app/uploads
      - pnpm-store:/root/.pnpm-store
      - /app/node_modules
    dns:
      - 168.126.63.1
      - 8.8.8.8
      - 8.8.4.4
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    networks:
      - dev-network
    healthcheck:
      test: ["CMD", "sh", "-c", "wget --quiet --tries=1 --spider http://localhost:3000/health && wget --quiet --tries=1 --spider http://localhost:8080"]
      interval: 30s
      timeout: 15s
      retries: 3
      start_period: 90s
 
  postgres:
    image: postgres:15-alpine
    container_name: sns-dev-postgres
    restart: always
    ports:
      - "5433:5432"
    environment:
      POSTGRES_USER: postgres
      POSTGRES_PASSWORD: postgres
      POSTGRES_DB: postgres
      POSTGRES_INITDB_ARGS: "--encoding=UTF-8 --locale=C"
    volumes:
      - ./postgres-data:/var/lib/postgresql/data
    networks:
      - dev-network
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U postgres"]
      interval: 30s
      timeout: 10s
      retries: 5
      start_period: 30s
 
  redis:
    image: redis:7.4-alpine
    container_name: sns-dev-redis
    restart: always
    ports:
      - "6379:6379"
    volumes:
      - redis-data:/data
    networks:
      - dev-network
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 30s
      timeout: 5s
      retries: 3

  # 모니터링 서비스 (Git 설정 마운트 제거)
  monitor:
    build:
      context: .
      dockerfile: Dockerfile.monitor
    container_name: sns-dev-monitor
    restart: unless-stopped
    volumes:
      # Docker 소켓 마운트 (호스트 Docker 접근용)
      - /var/run/docker.sock:/var/run/docker.sock:ro
      # 프로젝트 디렉토리 마운트 (읽기 전용)
      - .:/project:ro
      # 모니터링 로그 저장용
      - ./logs:/monitor/logs
      # Git 설정 마운트 제거 (컨테이너 내부에서 직접 설정)
    environment:
      # 프로젝트 경로
      - PROJECT_PATH=/project
      # DDNS 호스트
      - DDNS_HOST=${DDNS_HOST:-xxx.synology.me}
      # 모니터링 설정
      - MONITOR_INTERVAL=60
      - LOG_RETENTION_DAYS=7
      - HEALTH_CHECK_TIMEOUT=10
      # Git 설정 (환경변수로 전달)
      - GIT_SAFE_DIRECTORY=/project
      - GIT_USER_NAME=${GIT_USER_NAME:-CI/CD Bot}
      - GIT_USER_EMAIL=${GIT_USER_EMAIL:-email@example.com}
    networks:
      - dev-network
    depends_on:
      app:
        condition: service_healthy
      postgres:
        condition: service_healthy
      redis:
        condition: service_started
    healthcheck:
      test: ["CMD", "sh", "-c", "test -f /monitor/logs/monitor.log && test -x /monitor/monitor.sh"]
      interval: 60s
      timeout: 10s
      retries: 3
      start_period: 120s
    # 보안 설정
    security_opt:
      - no-new-privileges:true
    # 메모리 제한 (GitHub Actions 호환)
    mem_limit: 256m
    memswap_limit: 256m
 
volumes:
  pnpm-store:
    driver: local
  redis-data:
    driver: local
 
networks:
  dev-network:
    driver: bridge
    ipam:
      config:
        - subnet: 172.20.0.0/16
      driver: default
    labels:
      - "project=nestjs-sns"
      - "environment=development"
      - "ddns=${DDNS_HOST:-xxx.synology.me}"
EOF

기존의 docker-compose.yaml을 수정한다. 

기존 스크립트에서 monitor 서비스 부분만 추가되었다.

2.6. 결과 확인

monitor가 정상적으로 컨테이너에 올라갔으면 위와 같이 보여진다.

2.6.1. 로그 확인 방법들

방법1: NAS에서 직접 확인하기

# NAS에 SSH 접속 후
cd /volume1/docker/NestJS_SNS

# 모니터링 로그 파일 확인
sudo ls -la logs/

# 실시간 로그 확인
sudo tail -f logs/monitor.log

# 마지막 20줄 확인
sudo tail -20 logs/monitor.log

# 특정 시간대 로그 검색
sudo grep "2025-06-30 15:" logs/monitor.log

만약 실시간 로그 확인을 한다면 위 스크린샷처럼 현재 monitor 컨테이너의 로그를 보여준다.

방법2: Docker 명령어로 확인

# 컨테이너 내부 로그 파일 확인
sudo docker exec sns-dev-monitor ls -la /monitor/logs/

# 컨테이너 내부에서 로그 내용 확인
sudo docker exec sns-dev-monitor cat /monitor/logs/monitor.log

# 컨테이너 내부에서 실시간 로그 확인
sudo docker exec sns-dev-monitor tail -f /monitor/logs/monitor.log

마찬가지로 Docker 명령어로도 동일하게 볼 수 있다.

방법3: Docker Compose 로그 확인

# 모니터링 컨테이너의 실시간 로그 (stdout)
sudo docker-compose logs -f monitor

# 모니터링 컨테이너의 최근 로그
sudo docker-compose logs --tail=50 monitor

# 컨테이너 내부 로그 파일 짧게 확인
sudo docker-compose exec monitor tail -f /monitor/logs/monitor.log

결국 어떤 명령어로 볼 것인가는 취향차이라고 볼 수 있겠다.

 

3. 로그정리 스크립트 작성

monitor.sh 에서 정의된 log-rotate.sh는 UTC 시간 새벽3시 (한국시간 정오 12:00) 에 실행된다.

log-rotate.sh가 실행되면 기존에 쌓여있던 로그들을 정리하거나 삭제한다.

3.1. 프로젝트 경로 이동

cd /volume1/docker/NestJS_SNS

3.2. 로그정리 스크립트 작성

cat > log-rotate.sh << 'EOF'
#!/bin/bash

# 환경변수에서 설정 가져오기 (monitor.sh와 동일)
PROJECT_PATH="${PROJECT_PATH:-/project}"
LOG_RETENTION_DAYS="${LOG_RETENTION_DAYS:-7}"
LOGS_DIR="/monitor/logs"
PROJECT_LOGS_DIR="$PROJECT_PATH/logs"
BACKUP_DIR="$LOGS_DIR/archive"
TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')
LOG_FILE="$LOGS_DIR/log-rotate.log"

# 디렉토리 생성
mkdir -p "$BACKUP_DIR"
mkdir -p "$PROJECT_LOGS_DIR"

# 함수: 로그 메시지 출력
log_message() {
    local message="$1"
    echo "[$TIMESTAMP] $message" | tee -a $LOG_FILE
}

# 함수: 파일 크기를 사람이 읽기 쉬운 형태로 변환
human_size() {
    local size_bytes="$1"
    if [ "$size_bytes" -gt 1073741824 ]; then
        echo "$(( size_bytes / 1073741824 ))GB"
    elif [ "$size_bytes" -gt 1048576 ]; then
        echo "$(( size_bytes / 1048576 ))MB"
    elif [ "$size_bytes" -gt 1024 ]; then
        echo "$(( size_bytes / 1024 ))KB"
    else
        echo "${size_bytes}B"
    fi
}

echo "=====================================" | tee -a $LOG_FILE
log_message "🧹 로그 로테이션 시작 (보관기간: ${LOG_RETENTION_DAYS}일)"
echo "=====================================" | tee -a $LOG_FILE

# 현재 로그 상태 확인
log_message "📊 현재 로그 상태:"
echo "  - 모니터링 로그 디렉토리: $LOGS_DIR" | tee -a $LOG_FILE
echo "  - 프로젝트 로그 디렉토리: $PROJECT_LOGS_DIR" | tee -a $LOG_FILE

# 모니터링 로그 파일 수 계산
MONITOR_LOG_COUNT=$(find "$LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
PROJECT_LOG_COUNT=$(find "$PROJECT_LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)

echo "  - 모니터링 로그 파일: ${MONITOR_LOG_COUNT}개" | tee -a $LOG_FILE
echo "  - 프로젝트 로그 파일: ${PROJECT_LOG_COUNT}개" | tee -a $LOG_FILE

if [ -d "$LOGS_DIR" ]; then
    TOTAL_SIZE=$(du -sb "$LOGS_DIR" 2>/dev/null | awk '{print $1}' || echo "0")
    echo "  - 총 로그 크기: $(human_size $TOTAL_SIZE)" | tee -a $LOG_FILE
fi

# 1. 모니터링 로그 로테이션 (monitor.log가 1000줄 초과시)
log_message "🔄 모니터링 로그 로테이션 확인"
MONITOR_LOG_MAIN="$LOGS_DIR/monitor.log"

if [ -f "$MONITOR_LOG_MAIN" ]; then
    LINES=$(wc -l < "$MONITOR_LOG_MAIN" 2>/dev/null || echo "0")
    if [ "$LINES" -gt 1000 ]; then
        ROTATED_NAME="monitor.log.$(date +%Y%m%d-%H%M%S)"
        mv "$MONITOR_LOG_MAIN" "$LOGS_DIR/$ROTATED_NAME"
        touch "$MONITOR_LOG_MAIN"
        log_message "  ✅ 로테이션: monitor.log → $ROTATED_NAME ($LINES줄)"
    else
        log_message "  ℹ️  monitor.log 로테이션 불필요 ($LINES줄)"
    fi
else
    log_message "  ℹ️  monitor.log 파일 없음"
fi

# 2. LOG_RETENTION_DAYS 이상된 로그 파일 백업
log_message "📦 ${LOG_RETENTION_DAYS}일 이상된 로그 파일 백업"

# 모니터링 로그 백업
OLD_MONITOR_LOGS=$(find "$LOGS_DIR" -maxdepth 1 -name "*.log.*" -mtime +$LOG_RETENTION_DAYS 2>/dev/null)
BACKUP_COUNT=0

if [ -n "$OLD_MONITOR_LOGS" ]; then
    echo "$OLD_MONITOR_LOGS" | while read logfile; do
        if [ -f "$logfile" ]; then
            filename=$(basename "$logfile")
            mv "$logfile" "$BACKUP_DIR/$filename"
            log_message "  ✅ 백업: $filename"
            BACKUP_COUNT=$((BACKUP_COUNT + 1))
        fi
    done
else
    log_message "  ℹ️  백업할 오래된 모니터링 로그가 없습니다."
fi

# 프로젝트 로그 백업
if [ -d "$PROJECT_LOGS_DIR" ]; then
    OLD_PROJECT_LOGS=$(find "$PROJECT_LOGS_DIR" -name "*.log.*" -mtime +$LOG_RETENTION_DAYS 2>/dev/null)
    
    if [ -n "$OLD_PROJECT_LOGS" ]; then
        echo "$OLD_PROJECT_LOGS" | while read logfile; do
            if [ -f "$logfile" ]; then
                filename=$(basename "$logfile")
                backup_name="project_${filename}"
                mv "$logfile" "$BACKUP_DIR/$backup_name"
                log_message "  ✅ 백업: $filename → $backup_name"
            fi
        done
    else
        log_message "  ℹ️  백업할 오래된 프로젝트 로그가 없습니다."
    fi
fi

# 3. 30일 이상된 백업 파일 압축
log_message "🗜️  30일 이상된 백업 파일 압축"
OLD_BACKUPS=$(find "$BACKUP_DIR" -name "*.log*" -not -name "*.gz" -mtime +30 2>/dev/null)

if [ -n "$OLD_BACKUPS" ]; then
    echo "$OLD_BACKUPS" | while read backupfile; do
        if [ -f "$backupfile" ]; then
            gzip "$backupfile"
            log_message "  ✅ 압축: $(basename "$backupfile") → $(basename "$backupfile").gz"
        fi
    done
else
    log_message "  ℹ️  압축할 백업 파일이 없습니다."
fi

# 4. 90일 이상된 압축 파일 삭제
log_message "🗑️  90일 이상된 압축 파일 삭제"
VERY_OLD_BACKUPS=$(find "$BACKUP_DIR" -name "*.gz" -mtime +90 2>/dev/null)

if [ -n "$VERY_OLD_BACKUPS" ]; then
    echo "$VERY_OLD_BACKUPS" | while read oldfile; do
        if [ -f "$oldfile" ]; then
            rm "$oldfile"
            log_message "  ✅ 삭제: $(basename "$oldfile")"
        fi
    done
else
    log_message "  ℹ️  삭제할 오래된 압축 파일이 없습니다."
fi

# 5. Docker 로그 정리 (컨테이너 내부에서 실행시)
log_message "🐳 Docker 로그 정리"
if command -v docker >/dev/null 2>&1; then
    # 중지된 컨테이너 정리 (SNS 관련 제외)
    STOPPED_CONTAINERS=$(docker ps -a -q -f status=exited --filter "name!=sns-dev" 2>/dev/null)
    if [ -n "$STOPPED_CONTAINERS" ]; then
        docker rm $STOPPED_CONTAINERS >/dev/null 2>&1
        log_message "  ✅ 중지된 컨테이너 정리 완료"
    else
        log_message "  ℹ️  정리할 중지된 컨테이너가 없습니다."
    fi
    
    # 사용하지 않는 이미지 정리 (dangling 이미지만)
    PRUNED=$(docker image prune -f 2>/dev/null | grep "Total reclaimed space" || echo "0B")
    log_message "  ✅ 사용하지 않는 이미지 정리 완료: $PRUNED"
    
    # 컨테이너 로그 크기 확인
    SNS_CONTAINERS=$(docker ps --format "{{.Names}}" | grep "sns-dev" 2>/dev/null)
    if [ -n "$SNS_CONTAINERS" ]; then
        echo "$SNS_CONTAINERS" | while read container; do
            LOG_SIZE=$(docker logs --details "$container" 2>/dev/null | wc -c)
            log_message "  📊 $container 로그 크기: $(human_size $LOG_SIZE)"
        done
    fi
else
    log_message "  ⚠️  Docker 명령어 접근 불가 (호스트에서 실행 시 정상)"
fi

# 6. 로테이션 후 상태 요약
log_message "📊 로테이션 후 상태:"

# 파일 개수 재계산
FINAL_MONITOR_LOGS=$(find "$LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
FINAL_PROJECT_LOGS=$(find "$PROJECT_LOGS_DIR" -maxdepth 1 -name "*.log" -type f 2>/dev/null | wc -l)
BACKUP_FILES=$(find "$BACKUP_DIR" -name "*.log*" -not -name "*.gz" 2>/dev/null | wc -l)
COMPRESSED_FILES=$(find "$BACKUP_DIR" -name "*.gz" 2>/dev/null | wc -l)

echo "  - 활성 모니터링 로그: ${FINAL_MONITOR_LOGS}개" | tee -a $LOG_FILE
echo "  - 활성 프로젝트 로그: ${FINAL_PROJECT_LOGS}개" | tee -a $LOG_FILE
echo "  - 백업 파일: ${BACKUP_FILES}개" | tee -a $LOG_FILE
echo "  - 압축 파일: ${COMPRESSED_FILES}개" | tee -a $LOG_FILE

if [ -d "$LOGS_DIR" ]; then
    FINAL_SIZE=$(du -sb "$LOGS_DIR" 2>/dev/null | awk '{print $1}' || echo "0")
    echo "  - 총 크기: $(human_size $FINAL_SIZE)" | tee -a $LOG_FILE
fi

# 7. 디스크 공간 확인
log_message "💾 디스크 공간 상태:"
DISK_USAGE=$(df -h /monitor 2>/dev/null | tail -1 | awk '{print $5 " (" $4 " 사용가능)"}' || echo "확인불가")
echo "  - 디스크 사용률: $DISK_USAGE" | tee -a $LOG_FILE

echo "=====================================" | tee -a $LOG_FILE
log_message "🎉 로그 로테이션 완료"
echo "=====================================" | tee -a $LOG_FILE

# 실행 결과 반환 (성공: 0, 실패: 1)
exit 0
EOF

3.3. 권한 설정

chmod +x log-rotate.sh

3.4. 수동 실행 가이드

# 1. 일반적인 수동 실행
docker exec sns-dev-monitor /monitor/log-rotate.sh

# 2. 로그가 너무 많이 쌓였을 때 (3일 이상 된 것만 정리)
docker exec -e LOG_RETENTION_DAYS=3 sns-dev-monitor /monitor/log-rotate.sh

# 3. 실행 과정을 실시간으로 확인
docker exec -it sns-dev-monitor /monitor/log-rotate.sh

# 4. 실행 후 결과 확인
docker exec sns-dev-monitor tail -20 /monitor/logs/log-rotate.log

 

4. 문제해결 가이드

# API 응답 체크
curl -v http://호스트:3000/health

# 컨테이너 상태 확인
docker-compose ps
docker-compose logs app

# 롤백 실행
./rollback.sh

# 완전 재시작
docker-compose down
docker-compose up -d --build

 

최근댓글

최근글

skin by © 2024 ttuttak