Compare commits
4 Commits
5c082f7c9d
...
feat/train
| Author | SHA1 | Date | |
|---|---|---|---|
| 265813e6f7 | |||
| 8190a6e9c8 | |||
| e9f8bb37fa | |||
| f3c822587f |
6
.gitignore
vendored
6
.gitignore
vendored
@@ -72,9 +72,3 @@ docker-compose.override.yml
|
|||||||
*.swo
|
*.swo
|
||||||
*~
|
*~
|
||||||
!/CLAUDE.md
|
!/CLAUDE.md
|
||||||
|
|
||||||
### SSL Certificates ###
|
|
||||||
nginx/ssl/
|
|
||||||
*.crt
|
|
||||||
*.key
|
|
||||||
*.pem
|
|
||||||
|
|||||||
415
DEPLOY.md
415
DEPLOY.md
@@ -1,415 +0,0 @@
|
|||||||
# KAMCO Training API 배포 가이드 (RedHat 9.6)
|
|
||||||
|
|
||||||
## 빠른 배포 (Quick Start)
|
|
||||||
|
|
||||||
이 문서는 RedHat 9.6 환경에서 HTTPS로 KAMCO Training API를 배포하는 방법을 설명합니다.
|
|
||||||
|
|
||||||
**접속 URL**:
|
|
||||||
- `https://api.train-kamco.com`
|
|
||||||
- `https://train-kamco.com`
|
|
||||||
|
|
||||||
## 사전 요구사항
|
|
||||||
|
|
||||||
- [x] Docker & Docker Compose 설치
|
|
||||||
- [x] Git 설치
|
|
||||||
- [x] sudo 권한
|
|
||||||
- [x] 포트 80, 443 사용 가능
|
|
||||||
|
|
||||||
## 1단계: /etc/hosts 설정
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# root 권한으로 도메인 추가
|
|
||||||
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
|
|
||||||
|
|
||||||
# 확인
|
|
||||||
cat /etc/hosts | grep train-kamco
|
|
||||||
```
|
|
||||||
|
|
||||||
**예상 결과**:
|
|
||||||
```
|
|
||||||
127.0.0.1 api.train-kamco.com train-kamco.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## 2단계: 방화벽 설정 (필요시)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 방화벽 상태 확인
|
|
||||||
sudo firewall-cmd --state
|
|
||||||
|
|
||||||
# HTTP/HTTPS 포트 개방
|
|
||||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=443/tcp
|
|
||||||
|
|
||||||
# 방화벽 재로드
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
|
|
||||||
# 확인
|
|
||||||
sudo firewall-cmd --list-ports
|
|
||||||
```
|
|
||||||
|
|
||||||
**예상 결과**: `80/tcp 443/tcp`
|
|
||||||
|
|
||||||
## 3단계: 프로젝트 디렉토리로 이동
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/kamco-train-api
|
|
||||||
|
|
||||||
# 현재 위치 확인
|
|
||||||
pwd
|
|
||||||
# 예상: /home/username/kamco-train-api
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4단계: 파일 구조 확인
|
|
||||||
|
|
||||||
배포 전 필수 파일이 모두 있는지 확인하세요:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# SSL 인증서 확인
|
|
||||||
ls -la nginx/ssl/
|
|
||||||
|
|
||||||
# 예상 결과:
|
|
||||||
# train-kamco.com.crt (인증서)
|
|
||||||
# train-kamco.com.key (개인 키)
|
|
||||||
# openssl.cnf (설정 파일)
|
|
||||||
```
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker Compose 파일 확인
|
|
||||||
ls -la docker-compose-prod.yml nginx/nginx.conf
|
|
||||||
|
|
||||||
# 예상: 두 파일 모두 존재
|
|
||||||
```
|
|
||||||
|
|
||||||
## 5단계: Docker 네트워크 생성 (최초 1회)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# kamco-cds 네트워크가 있는지 확인
|
|
||||||
docker network ls | grep kamco-cds
|
|
||||||
|
|
||||||
# 없으면 생성
|
|
||||||
docker network create kamco-cds
|
|
||||||
```
|
|
||||||
|
|
||||||
## 6단계: Docker Compose 배포
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 기존 컨테이너 중지 (있는 경우)
|
|
||||||
docker-compose -f docker-compose-prod.yml down
|
|
||||||
|
|
||||||
# 새로운 이미지 빌드 및 실행
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d --build
|
|
||||||
|
|
||||||
# 컨테이너 상태 확인
|
|
||||||
docker-compose -f docker-compose-prod.yml ps
|
|
||||||
```
|
|
||||||
|
|
||||||
**예상 결과**:
|
|
||||||
```
|
|
||||||
NAME STATUS
|
|
||||||
kamco-cd-nginx Up (healthy)
|
|
||||||
kamco-cd-training-api Up (healthy)
|
|
||||||
```
|
|
||||||
|
|
||||||
## 7단계: 배포 확인
|
|
||||||
|
|
||||||
### 컨테이너 로그 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Nginx 로그
|
|
||||||
docker logs kamco-cd-nginx --tail 50
|
|
||||||
|
|
||||||
# API 로그
|
|
||||||
docker logs kamco-cd-training-api --tail 50
|
|
||||||
|
|
||||||
# 실시간 로그 (Ctrl+C로 종료)
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTP → HTTPS 리다이렉트 테스트
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# HTTP 접속 시 HTTPS로 리다이렉트되는지 확인
|
|
||||||
curl -I http://api.train-kamco.com
|
|
||||||
curl -I http://train-kamco.com
|
|
||||||
|
|
||||||
# 예상 결과: 301 Moved Permanently
|
|
||||||
# Location: https://api.train-kamco.com/ 또는 https://train-kamco.com/
|
|
||||||
```
|
|
||||||
|
|
||||||
### HTTPS 헬스체크
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# -k 플래그: 사설 인증서 경고 무시
|
|
||||||
curl -k https://api.train-kamco.com/monitor/health
|
|
||||||
curl -k https://train-kamco.com/monitor/health
|
|
||||||
|
|
||||||
# 예상 결과: {"status":"UP","components":{...}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 브라우저 테스트
|
|
||||||
|
|
||||||
브라우저에서 다음 URL에 접속:
|
|
||||||
|
|
||||||
- `https://api.train-kamco.com/monitor/health`
|
|
||||||
- `https://train-kamco.com/monitor/health`
|
|
||||||
|
|
||||||
**사설 인증서 경고**:
|
|
||||||
- "안전하지 않음" 경고가 표시되면 **"고급"** → **"계속 진행"** 클릭
|
|
||||||
|
|
||||||
## 8단계: SSL 인증서 확인 (선택사항)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 인증서 정보 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | head -30
|
|
||||||
|
|
||||||
# 유효 기간 확인 (100년)
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -noout -dates
|
|
||||||
|
|
||||||
# SAN (멀티 도메인) 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | grep -A1 "Subject Alternative Name"
|
|
||||||
|
|
||||||
# 예상 결과:
|
|
||||||
# X509v3 Subject Alternative Name:
|
|
||||||
# DNS:api.train-kamco.com, DNS:train-kamco.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## 트러블슈팅
|
|
||||||
|
|
||||||
### 문제 1: "Connection refused"
|
|
||||||
|
|
||||||
**원인**: 컨테이너가 실행되지 않음
|
|
||||||
|
|
||||||
**해결**:
|
|
||||||
```bash
|
|
||||||
# 컨테이너 상태 확인
|
|
||||||
docker ps -a | grep kamco-cd
|
|
||||||
|
|
||||||
# 컨테이너 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart
|
|
||||||
|
|
||||||
# 로그 확인
|
|
||||||
docker logs kamco-cd-nginx
|
|
||||||
docker logs kamco-cd-training-api
|
|
||||||
```
|
|
||||||
|
|
||||||
### 문제 2: "502 Bad Gateway"
|
|
||||||
|
|
||||||
**원인**: Nginx는 실행 중이지만 API 컨테이너가 준비되지 않음
|
|
||||||
|
|
||||||
**해결**:
|
|
||||||
```bash
|
|
||||||
# API 컨테이너 상태 확인
|
|
||||||
docker logs kamco-cd-training-api
|
|
||||||
|
|
||||||
# API 헬스체크 (컨테이너 내부에서)
|
|
||||||
docker exec kamco-cd-nginx wget -qO- http://kamco-changedetection-api:8080/monitor/health
|
|
||||||
|
|
||||||
# API 컨테이너 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart kamco-changedetection-api
|
|
||||||
```
|
|
||||||
|
|
||||||
### 문제 3: "Name or service not known"
|
|
||||||
|
|
||||||
**원인**: /etc/hosts에 도메인이 설정되지 않음
|
|
||||||
|
|
||||||
**해결**:
|
|
||||||
```bash
|
|
||||||
# /etc/hosts 확인
|
|
||||||
cat /etc/hosts | grep train-kamco
|
|
||||||
|
|
||||||
# 없으면 추가
|
|
||||||
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
|
|
||||||
```
|
|
||||||
|
|
||||||
### 문제 4: 포트 80 또는 443이 이미 사용 중
|
|
||||||
|
|
||||||
**원인**: 다른 프로세스가 포트를 사용 중
|
|
||||||
|
|
||||||
**해결**:
|
|
||||||
```bash
|
|
||||||
# 포트 사용 확인
|
|
||||||
sudo lsof -i :80
|
|
||||||
sudo lsof -i :443
|
|
||||||
|
|
||||||
# 사용 중인 프로세스 종료 (예: httpd, nginx)
|
|
||||||
sudo systemctl stop httpd
|
|
||||||
sudo systemctl stop nginx
|
|
||||||
|
|
||||||
# Docker Compose 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### 문제 5: SELinux 권한 오류
|
|
||||||
|
|
||||||
**원인**: SELinux가 Docker 볼륨 마운트를 차단
|
|
||||||
|
|
||||||
**해결**:
|
|
||||||
```bash
|
|
||||||
# SELinux 상태 확인
|
|
||||||
getenforce
|
|
||||||
|
|
||||||
# Permissive 모드로 임시 변경 (재부팅 시 초기화됨)
|
|
||||||
sudo setenforce 0
|
|
||||||
|
|
||||||
# 영구 변경 (권장하지 않음)
|
|
||||||
sudo vi /etc/selinux/config
|
|
||||||
# SELINUX=permissive 또는 SELINUX=disabled로 변경
|
|
||||||
```
|
|
||||||
|
|
||||||
## 컨테이너 관리 명령어
|
|
||||||
|
|
||||||
### 시작/중지/재시작
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 시작
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d
|
|
||||||
|
|
||||||
# 중지
|
|
||||||
docker-compose -f docker-compose-prod.yml down
|
|
||||||
|
|
||||||
# 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart
|
|
||||||
|
|
||||||
# 특정 서비스만 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart nginx
|
|
||||||
docker-compose -f docker-compose-prod.yml restart kamco-changedetection-api
|
|
||||||
```
|
|
||||||
|
|
||||||
### 로그 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 전체 로그
|
|
||||||
docker-compose -f docker-compose-prod.yml logs
|
|
||||||
|
|
||||||
# 특정 서비스 로그
|
|
||||||
docker-compose -f docker-compose-prod.yml logs nginx
|
|
||||||
docker-compose -f docker-compose-prod.yml logs kamco-changedetection-api
|
|
||||||
|
|
||||||
# 실시간 로그
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f
|
|
||||||
|
|
||||||
# 마지막 N줄만 보기
|
|
||||||
docker logs kamco-cd-nginx --tail 100
|
|
||||||
```
|
|
||||||
|
|
||||||
### 컨테이너 상태 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 실행 중인 컨테이너
|
|
||||||
docker-compose -f docker-compose-prod.yml ps
|
|
||||||
|
|
||||||
# 상세 정보
|
|
||||||
docker inspect kamco-cd-nginx
|
|
||||||
docker inspect kamco-cd-training-api
|
|
||||||
|
|
||||||
# 리소스 사용량
|
|
||||||
docker stats kamco-cd-nginx kamco-cd-training-api
|
|
||||||
```
|
|
||||||
|
|
||||||
### 컨테이너 내부 접속
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Nginx 컨테이너 내부 접속
|
|
||||||
docker exec -it kamco-cd-nginx sh
|
|
||||||
|
|
||||||
# API 컨테이너 내부 접속
|
|
||||||
docker exec -it kamco-cd-training-api sh
|
|
||||||
|
|
||||||
# 내부에서 빠져나오기
|
|
||||||
exit
|
|
||||||
```
|
|
||||||
|
|
||||||
## 업데이트 및 재배포
|
|
||||||
|
|
||||||
### 코드 업데이트 후 재배포
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Git pull (코드 업데이트)
|
|
||||||
git pull origin develop
|
|
||||||
|
|
||||||
# 2. JAR 파일 빌드 (Jenkins에서 수행하는 경우 생략)
|
|
||||||
./gradlew clean build -x test
|
|
||||||
|
|
||||||
# 3. 컨테이너 재빌드 및 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml down
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d --build
|
|
||||||
|
|
||||||
# 4. 로그 확인
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### 설정 파일만 변경한 경우
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# nginx.conf 또는 docker-compose-prod.yml 변경 시
|
|
||||||
docker-compose -f docker-compose-prod.yml down
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d
|
|
||||||
|
|
||||||
# 또는
|
|
||||||
docker-compose -f docker-compose-prod.yml restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
## 모니터링
|
|
||||||
|
|
||||||
### 헬스체크 엔드포인트
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API 헬스체크
|
|
||||||
curl -k https://api.train-kamco.com/monitor/health
|
|
||||||
|
|
||||||
# 예상 결과:
|
|
||||||
# {
|
|
||||||
# "status": "UP",
|
|
||||||
# "components": {
|
|
||||||
# "db": {"status": "UP"},
|
|
||||||
# "diskSpace": {"status": "UP"}
|
|
||||||
# }
|
|
||||||
# }
|
|
||||||
```
|
|
||||||
|
|
||||||
### 시스템 리소스
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 디스크 사용량
|
|
||||||
df -h
|
|
||||||
|
|
||||||
# 메모리 사용량
|
|
||||||
free -h
|
|
||||||
|
|
||||||
# Docker 이미지 및 컨테이너 용량
|
|
||||||
docker system df
|
|
||||||
```
|
|
||||||
|
|
||||||
## 보안 권장 사항
|
|
||||||
|
|
||||||
1. **사설 인증서**: 현재 사설 인증서를 사용 중입니다. 프로덕션 환경에서는 **Let's Encrypt** 또는 **GlobalSign** 같은 공인 인증서 사용을 권장합니다.
|
|
||||||
|
|
||||||
2. **방화벽**: 필요한 포트(80, 443)만 개방하고, 불필요한 포트는 차단하세요.
|
|
||||||
|
|
||||||
3. **정기 업데이트**: Docker 이미지와 시스템 패키지를 정기적으로 업데이트하세요.
|
|
||||||
|
|
||||||
4. **로그 모니터링**: 정기적으로 로그를 확인하여 비정상적인 활동을 감지하세요.
|
|
||||||
|
|
||||||
5. **백업**: SSL 인증서 키 파일(`train-kamco.com.key`)과 데이터베이스를 정기적으로 백업하세요.
|
|
||||||
|
|
||||||
## 참고 문서
|
|
||||||
|
|
||||||
- **SSL 인증서 설정**: [nginx/SSL_SETUP.md](nginx/SSL_SETUP.md)
|
|
||||||
- **프로젝트 개요**: [README.md](README.md)
|
|
||||||
- **CLAUDE.md**: [CLAUDE.md](CLAUDE.md)
|
|
||||||
|
|
||||||
## 지원
|
|
||||||
|
|
||||||
문제가 발생하면 다음을 확인하세요:
|
|
||||||
1. 컨테이너 로그: `docker-compose -f docker-compose-prod.yml logs`
|
|
||||||
2. 컨테이너 상태: `docker-compose -f docker-compose-prod.yml ps`
|
|
||||||
3. /etc/hosts 설정: `cat /etc/hosts | grep train-kamco`
|
|
||||||
4. 방화벽 상태: `sudo firewall-cmd --list-ports`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**배포 완료!** 🎉
|
|
||||||
|
|
||||||
접속 URL:
|
|
||||||
- `https://api.train-kamco.com`
|
|
||||||
- `https://train-kamco.com`
|
|
||||||
443
DEPLOYMENT.md
443
DEPLOYMENT.md
@@ -1,443 +0,0 @@
|
|||||||
# KAMCO Train API - Production Deployment Guide
|
|
||||||
|
|
||||||
프로덕션 환경 배포 가이드
|
|
||||||
|
|
||||||
## 목차
|
|
||||||
- [사전 요구사항](#사전-요구사항)
|
|
||||||
- [초기 설정](#초기-설정)
|
|
||||||
- [배포 순서](#배포-순서)
|
|
||||||
- [개별 서비스 관리](#개별-서비스-관리)
|
|
||||||
- [롤백 절차](#롤백-절차)
|
|
||||||
- [모니터링 및 헬스체크](#모니터링-및-헬스체크)
|
|
||||||
- [트러블슈팅](#트러블슈팅)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 사전 요구사항
|
|
||||||
|
|
||||||
### 시스템 요구사항
|
|
||||||
- Docker Engine 20.10+
|
|
||||||
- Docker Compose 2.0+
|
|
||||||
- 최소 메모리: 4GB
|
|
||||||
- 디스크 공간: 20GB 이상
|
|
||||||
|
|
||||||
### 네트워크 요구사항
|
|
||||||
- 도메인 설정:
|
|
||||||
- `train-kamco.com` → 서버 IP (Web UI)
|
|
||||||
- `api.train-kamco.com` → 서버 IP (API)
|
|
||||||
- 포트:
|
|
||||||
- 80 (HTTP)
|
|
||||||
- 443 (HTTPS)
|
|
||||||
- 8080 (API - internal)
|
|
||||||
- 3002 (Web - internal)
|
|
||||||
|
|
||||||
### 필수 파일
|
|
||||||
- SSL 인증서:
|
|
||||||
- `nginx/ssl/train-kamco.com.crt`
|
|
||||||
- `nginx/ssl/train-kamco.com.key`
|
|
||||||
- 환경 설정:
|
|
||||||
- `application-prod.yml` (API 설정)
|
|
||||||
- `.env` 파일 (IMAGE_TAG 등)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 초기 설정
|
|
||||||
|
|
||||||
### 1. Docker 네트워크 생성
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# kamco-cds 네트워크 생성 (최초 1회만)
|
|
||||||
docker network create kamco-cds
|
|
||||||
|
|
||||||
# 네트워크 확인
|
|
||||||
docker network ls | grep kamco-cds
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. SSL 인증서 배치
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 인증서 디렉토리 생성
|
|
||||||
mkdir -p nginx/ssl
|
|
||||||
|
|
||||||
# 인증서 파일 복사
|
|
||||||
cp /path/to/train-kamco.com.crt nginx/ssl/
|
|
||||||
cp /path/to/train-kamco.com.key nginx/ssl/
|
|
||||||
|
|
||||||
# 권한 설정
|
|
||||||
chmod 600 nginx/ssl/train-kamco.com.key
|
|
||||||
chmod 644 nginx/ssl/train-kamco.com.crt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 환경 변수 설정
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# .env 파일 생성
|
|
||||||
cat > .env << EOF
|
|
||||||
IMAGE_TAG=latest
|
|
||||||
SPRING_PROFILES_ACTIVE=prod
|
|
||||||
TZ=Asia/Seoul
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 볼륨 디렉토리 생성
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 데이터 디렉토리 생성
|
|
||||||
mkdir -p ./app/model_output
|
|
||||||
mkdir -p ./app/train_dataset
|
|
||||||
|
|
||||||
# 권한 설정
|
|
||||||
chmod -R 755 ./app
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 배포 순서
|
|
||||||
|
|
||||||
### 전체 스택 초기 배포
|
|
||||||
|
|
||||||
**중요**: 반드시 아래 순서대로 실행해야 합니다.
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Nginx 시작
|
|
||||||
docker-compose -f docker-compose-nginx.yml up -d
|
|
||||||
|
|
||||||
# 2. Nginx 상태 확인
|
|
||||||
docker ps | grep kamco-train-nginx
|
|
||||||
|
|
||||||
# 3. API 서비스 시작
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d
|
|
||||||
|
|
||||||
# 4. API 헬스체크 대기 (최대 40초)
|
|
||||||
sleep 40
|
|
||||||
curl -f http://localhost:8080/monitor/health
|
|
||||||
|
|
||||||
# 5. Web 서비스 시작 (kamco-train-web 프로젝트에서)
|
|
||||||
# cd ../kamco-train-web
|
|
||||||
# docker-compose -f docker-compose-prod.yml up -d
|
|
||||||
|
|
||||||
# 6. 전체 상태 확인
|
|
||||||
docker ps -a | grep kamco
|
|
||||||
```
|
|
||||||
|
|
||||||
### 배포 검증
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 서비스별 헬스체크
|
|
||||||
curl -f http://localhost:8080/monitor/health # API (internal)
|
|
||||||
curl -kf https://api.train-kamco.com/monitor/health # API (external)
|
|
||||||
curl -kf https://train-kamco.com # Web (external)
|
|
||||||
|
|
||||||
# Nginx 설정 검증
|
|
||||||
docker exec kamco-train-nginx nginx -t
|
|
||||||
|
|
||||||
# 로그 확인
|
|
||||||
docker-compose -f docker-compose-nginx.yml logs --tail=50
|
|
||||||
docker-compose -f docker-compose-prod.yml logs --tail=50
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 개별 서비스 관리
|
|
||||||
|
|
||||||
### Nginx 관리
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 설정 변경 후 리로드 (다운타임 없음)
|
|
||||||
docker exec kamco-train-nginx nginx -s reload
|
|
||||||
|
|
||||||
# 재시작
|
|
||||||
docker-compose -f docker-compose-nginx.yml restart
|
|
||||||
|
|
||||||
# 로그 확인
|
|
||||||
docker-compose -f docker-compose-nginx.yml logs -f
|
|
||||||
|
|
||||||
# 컨테이너 내부 접근
|
|
||||||
docker exec -it kamco-train-nginx sh
|
|
||||||
```
|
|
||||||
|
|
||||||
### API 서비스 관리
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 재배포 (새 이미지 빌드)
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d --build
|
|
||||||
|
|
||||||
# 재시작 (이미지 변경 없이)
|
|
||||||
docker-compose -f docker-compose-prod.yml restart
|
|
||||||
|
|
||||||
# 중지
|
|
||||||
docker-compose -f docker-compose-prod.yml down
|
|
||||||
|
|
||||||
# 로그 확인
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f kamco-train-api
|
|
||||||
|
|
||||||
# 컨테이너 내부 접근
|
|
||||||
docker exec -it kamco-train-api bash
|
|
||||||
```
|
|
||||||
|
|
||||||
### Web 서비스 관리
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# kamco-train-web 프로젝트에서 실행
|
|
||||||
cd ../kamco-train-web
|
|
||||||
|
|
||||||
# 재배포
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d --build
|
|
||||||
|
|
||||||
# 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart
|
|
||||||
|
|
||||||
# 로그 확인
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 롤백 절차
|
|
||||||
|
|
||||||
### 이미지 기반 롤백
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 사용 가능한 이미지 확인
|
|
||||||
docker images | grep kamco-train-api
|
|
||||||
|
|
||||||
# 2. 이전 이미지 태그로 롤백
|
|
||||||
export IMAGE_TAG=previous-commit-hash
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d
|
|
||||||
|
|
||||||
# 3. 헬스체크 확인
|
|
||||||
curl -f http://localhost:8080/monitor/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### Git 기반 롤백
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. 이전 커밋으로 체크아웃
|
|
||||||
git log --oneline -10
|
|
||||||
git checkout <previous-commit-hash>
|
|
||||||
|
|
||||||
# 2. 재빌드 및 배포
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d --build
|
|
||||||
|
|
||||||
# 3. 검증 후 브랜치 업데이트 (필요시)
|
|
||||||
# git checkout develop
|
|
||||||
# git reset --hard <previous-commit-hash>
|
|
||||||
# git push -f origin develop
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 모니터링 및 헬스체크
|
|
||||||
|
|
||||||
### 헬스체크 엔드포인트
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API 헬스체크
|
|
||||||
curl http://localhost:8080/monitor/health
|
|
||||||
curl http://localhost:8080/monitor/health/readiness
|
|
||||||
curl http://localhost:8080/monitor/health/liveness
|
|
||||||
|
|
||||||
# Nginx를 통한 헬스체크
|
|
||||||
curl -k https://api.train-kamco.com/monitor/health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 컨테이너 상태 모니터링
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 모든 컨테이너 상태
|
|
||||||
docker ps -a | grep kamco
|
|
||||||
|
|
||||||
# 리소스 사용량 실시간 모니터링
|
|
||||||
docker stats kamco-train-nginx kamco-train-api
|
|
||||||
|
|
||||||
# 헬스체크 상태
|
|
||||||
docker inspect kamco-train-api | grep -A 10 Health
|
|
||||||
```
|
|
||||||
|
|
||||||
### 로그 모니터링
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 실시간 로그 (모든 서비스)
|
|
||||||
docker-compose -f docker-compose-nginx.yml logs -f &
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f &
|
|
||||||
|
|
||||||
# 에러 로그만 필터링
|
|
||||||
docker-compose -f docker-compose-prod.yml logs | grep -i error
|
|
||||||
|
|
||||||
# 최근 100줄
|
|
||||||
docker-compose -f docker-compose-prod.yml logs --tail=100
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 트러블슈팅
|
|
||||||
|
|
||||||
### 1. Nginx 502 Bad Gateway
|
|
||||||
|
|
||||||
**원인**: API 서비스가 준비되지 않음
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API 컨테이너 상태 확인
|
|
||||||
docker ps | grep kamco-train-api
|
|
||||||
|
|
||||||
# API 로그 확인
|
|
||||||
docker logs kamco-train-api --tail=100
|
|
||||||
|
|
||||||
# 네트워크 연결 확인
|
|
||||||
docker network inspect kamco-cds | grep kamco-train-api
|
|
||||||
|
|
||||||
# 해결: API 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. SSL 인증서 오류
|
|
||||||
|
|
||||||
**원인**: 인증서 파일 누락 또는 권한 문제
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 인증서 파일 확인
|
|
||||||
ls -la nginx/ssl/
|
|
||||||
|
|
||||||
# Nginx 설정 검증
|
|
||||||
docker exec kamco-train-nginx nginx -t
|
|
||||||
|
|
||||||
# 해결: 인증서 재배치 및 권한 설정
|
|
||||||
chmod 600 nginx/ssl/train-kamco.com.key
|
|
||||||
chmod 644 nginx/ssl/train-kamco.com.crt
|
|
||||||
docker-compose -f docker-compose-nginx.yml restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 컨테이너 시작 실패
|
|
||||||
|
|
||||||
**원인**: 포트 충돌, 볼륨 권한, 메모리 부족
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 포트 사용 확인
|
|
||||||
netstat -tulpn | grep -E '80|443|8080'
|
|
||||||
|
|
||||||
# 볼륨 권한 확인
|
|
||||||
ls -la ./app/
|
|
||||||
|
|
||||||
# 메모리 사용량 확인
|
|
||||||
free -h
|
|
||||||
docker system df
|
|
||||||
|
|
||||||
# 해결: 충돌 프로세스 종료 또는 포트 변경
|
|
||||||
# 메모리 정리
|
|
||||||
docker system prune -a
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 네트워크 연결 문제
|
|
||||||
|
|
||||||
**원인**: kamco-cds 네트워크 미생성 또는 컨테이너 미연결
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 네트워크 확인
|
|
||||||
docker network ls | grep kamco-cds
|
|
||||||
|
|
||||||
# 네트워크 상세 정보
|
|
||||||
docker network inspect kamco-cds
|
|
||||||
|
|
||||||
# 해결: 네트워크 생성
|
|
||||||
docker network create kamco-cds
|
|
||||||
|
|
||||||
# 컨테이너를 네트워크에 연결
|
|
||||||
docker network connect kamco-cds kamco-train-api
|
|
||||||
docker network connect kamco-cds kamco-train-nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 데이터베이스 연결 실패
|
|
||||||
|
|
||||||
**원인**: application-prod.yml의 DB 설정 오류
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# API 로그에서 DB 연결 에러 확인
|
|
||||||
docker logs kamco-train-api | grep -i "connection"
|
|
||||||
|
|
||||||
# DB 호스트 연결 테스트
|
|
||||||
docker exec kamco-train-api ping <db-host>
|
|
||||||
|
|
||||||
# 해결: application-prod.yml 수정 후 재배포
|
|
||||||
vim src/main/resources/application-prod.yml
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d --build
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Jenkins CI/CD 연동
|
|
||||||
|
|
||||||
현재 프로젝트는 Jenkins 파이프라인으로 자동 배포됩니다.
|
|
||||||
|
|
||||||
### Jenkinsfile-dev 주요 단계
|
|
||||||
|
|
||||||
1. **Checkout**: develop 브랜치 체크아웃
|
|
||||||
2. **Build**: `./gradlew clean build -x test`
|
|
||||||
3. **Extract Commit**: IMAGE_TAG로 사용
|
|
||||||
4. **Transfer**: 배포 서버로 파일 전송
|
|
||||||
5. **Deploy**: Docker Compose 빌드 및 배포
|
|
||||||
6. **Health Check**: 30초 대기 후 헬스체크
|
|
||||||
7. **Cleanup**: 오래된 이미지 정리 (최신 5개 유지)
|
|
||||||
|
|
||||||
### 배포 서버 정보
|
|
||||||
|
|
||||||
- **서버**: 192.168.2.109
|
|
||||||
- **사용자**: space
|
|
||||||
- **배포 경로**: `/home/space/kamco-training-api`
|
|
||||||
- **헬스체크**: `http://localhost:7200/monitor/health`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 백업 및 복구
|
|
||||||
|
|
||||||
### 데이터 백업
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 볼륨 데이터 백업
|
|
||||||
tar -czf backup-$(date +%Y%m%d).tar.gz ./app/model_output ./app/train_dataset
|
|
||||||
|
|
||||||
# 설정 파일 백업
|
|
||||||
tar -czf config-backup-$(date +%Y%m%d).tar.gz \
|
|
||||||
nginx/nginx.conf \
|
|
||||||
nginx/ssl/ \
|
|
||||||
src/main/resources/application-prod.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 이미지 백업
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 현재 이미지 저장
|
|
||||||
docker save kamco-train-api:latest | gzip > kamco-train-api-latest.tar.gz
|
|
||||||
|
|
||||||
# 이미지 복구
|
|
||||||
gunzip -c kamco-train-api-latest.tar.gz | docker load
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 보안 체크리스트
|
|
||||||
|
|
||||||
- [ ] SSL 인증서 유효기간 확인
|
|
||||||
- [ ] nginx/ssl/ 디렉토리 권한 600
|
|
||||||
- [ ] application-prod.yml에 DB 비밀번호 암호화
|
|
||||||
- [ ] JWT secret key 환경변수로 관리
|
|
||||||
- [ ] Docker 소켓 권한 최소화
|
|
||||||
- [ ] 방화벽 규칙 설정 (80, 443만 외부 노출)
|
|
||||||
- [ ] 정기 보안 업데이트 (docker images)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 참고 문서
|
|
||||||
|
|
||||||
- [CLAUDE.md](./CLAUDE.md) - 프로젝트 개발 가이드
|
|
||||||
- [README.md](./README.md) - 프로젝트 개요
|
|
||||||
- [Jenkinsfile-dev](./Jenkinsfile-dev) - CI/CD 파이프라인
|
|
||||||
- [nginx/nginx.conf](./nginx/nginx.conf) - Nginx 설정
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 연락처
|
|
||||||
|
|
||||||
문제 발생 시:
|
|
||||||
1. 로그 수집: `docker-compose logs` 출력
|
|
||||||
2. 시스템 정보: `docker ps -a`, `docker network ls`
|
|
||||||
3. 이슈 리포트: GitHub Issues 또는 내부 이슈 트래커
|
|
||||||
20
Dockerfile
20
Dockerfile
@@ -1,20 +0,0 @@
|
|||||||
# Stage 1: Build stage (gradle build는 Jenkins에서 이미 수행)
|
|
||||||
FROM eclipse-temurin:21-jre-jammy
|
|
||||||
|
|
||||||
# docker CLI 설치 (컨테이너에서 호스트 Docker 제어용) 260212 추가
|
|
||||||
RUN apt-get update && \
|
|
||||||
apt-get install -y --no-install-recommends docker.io ca-certificates && \
|
|
||||||
rm -rf /var/lib/apt/lists/*
|
|
||||||
|
|
||||||
# 작업 디렉토리 설정
|
|
||||||
WORKDIR /app
|
|
||||||
|
|
||||||
# JAR 파일 복사 (Jenkins에서 빌드된 ROOT.jar)
|
|
||||||
COPY build/libs/ROOT.jar app.jar
|
|
||||||
|
|
||||||
# 포트 노출
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
# 애플리케이션 실행
|
|
||||||
# dev 프로파일로 실행
|
|
||||||
ENTRYPOINT ["java", "-jar", "-Dspring.profiles.active=prod", "app.jar"]
|
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
services:
|
|
||||||
nginx:
|
|
||||||
image: nginx:alpine
|
|
||||||
container_name: kamco-train-nginx
|
|
||||||
ports:
|
|
||||||
- "80:80"
|
|
||||||
- "443:443"
|
|
||||||
volumes:
|
|
||||||
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
|
|
||||||
- ./nginx/ssl:/etc/nginx/ssl:ro
|
|
||||||
- nginx-logs:/var/log/nginx
|
|
||||||
networks:
|
|
||||||
- kamco-cds
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "--no-check-certificate", "https://localhost/monitor/health"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 10s
|
|
||||||
retries: 3
|
|
||||||
start_period: 10s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
kamco-cds:
|
|
||||||
external: true
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
nginx-logs:
|
|
||||||
driver: local
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
services:
|
|
||||||
kamco-train-api:
|
|
||||||
build:
|
|
||||||
context: .
|
|
||||||
dockerfile: Dockerfile
|
|
||||||
image: kamco-train-api:${IMAGE_TAG:-latest}
|
|
||||||
container_name: kamco-train-api
|
|
||||||
expose:
|
|
||||||
- "8080"
|
|
||||||
environment:
|
|
||||||
- SPRING_PROFILES_ACTIVE=prod
|
|
||||||
- TZ=Asia/Seoul
|
|
||||||
volumes:
|
|
||||||
- ./app/model_output:/app/model-outputs
|
|
||||||
- ./app/train_dataset:/app/train-dataset
|
|
||||||
- /var/run/docker.sock:/var/run/docker.sock
|
|
||||||
networks:
|
|
||||||
- kamco-cds
|
|
||||||
restart: unless-stopped
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "curl", "-f", "http://localhost:8080/monitor/health"]
|
|
||||||
interval: 10s
|
|
||||||
timeout: 5s
|
|
||||||
retries: 5
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
networks:
|
|
||||||
kamco-cds:
|
|
||||||
external: true
|
|
||||||
@@ -1,414 +0,0 @@
|
|||||||
# SSL 사설 인증서 설정 가이드 (RedHat 9.6)
|
|
||||||
|
|
||||||
## 개요
|
|
||||||
|
|
||||||
이 문서는 RedHat 9.6 환경에서 `https://api.train-kamco.com` 및 `https://train-kamco.com` 도메인을 위한 100년 유효한 사설 SSL 인증서 설정 방법을 설명합니다.
|
|
||||||
|
|
||||||
## 디렉토리 구조
|
|
||||||
|
|
||||||
```
|
|
||||||
nginx/
|
|
||||||
├── nginx.conf # Nginx 설정 파일
|
|
||||||
├── ssl/
|
|
||||||
│ ├── openssl.cnf # OpenSSL 설정 파일 (SAN 포함)
|
|
||||||
│ ├── train-kamco.com.crt # 사설 SSL 인증서 (멀티 도메인)
|
|
||||||
│ └── train-kamco.com.key # 개인 키 (비공개)
|
|
||||||
└── SSL_SETUP.md # 이 문서
|
|
||||||
```
|
|
||||||
|
|
||||||
## 인증서 정보
|
|
||||||
|
|
||||||
- **도메인**: api.train-kamco.com, train-kamco.com (멀티 도메인)
|
|
||||||
- **유효 기간**: 100년 (36500일)
|
|
||||||
- **알고리즘**: RSA 4096-bit
|
|
||||||
- **CN (Common Name)**: api.train-kamco.com
|
|
||||||
- **SAN (Subject Alternative Names)**: api.train-kamco.com, train-kamco.com
|
|
||||||
|
|
||||||
## 사설 SSL 인증서 생성 (이미 생성됨)
|
|
||||||
|
|
||||||
인증서가 이미 생성되어 있습니다. 재생성이 필요한 경우 아래 단계를 따르세요.
|
|
||||||
|
|
||||||
### 1. OpenSSL 설정 파일 생성
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/kamco-train-api
|
|
||||||
|
|
||||||
cat > nginx/ssl/openssl.cnf << 'EOF'
|
|
||||||
[req]
|
|
||||||
default_bits = 4096
|
|
||||||
prompt = no
|
|
||||||
default_md = sha256
|
|
||||||
distinguished_name = dn
|
|
||||||
req_extensions = v3_req
|
|
||||||
|
|
||||||
[dn]
|
|
||||||
C=KR
|
|
||||||
ST=Seoul
|
|
||||||
L=Seoul
|
|
||||||
O=KAMCO
|
|
||||||
OU=Training
|
|
||||||
CN=api.train-kamco.com
|
|
||||||
|
|
||||||
[v3_req]
|
|
||||||
subjectAltName = @alt_names
|
|
||||||
|
|
||||||
[alt_names]
|
|
||||||
DNS.1 = api.train-kamco.com
|
|
||||||
DNS.2 = train-kamco.com
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. SSL 인증서 및 개인 키 생성
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# nginx/ssl 디렉토리 생성 (없는 경우)
|
|
||||||
mkdir -p nginx/ssl
|
|
||||||
chmod 700 nginx/ssl
|
|
||||||
|
|
||||||
# 인증서 및 개인 키 생성 (100년 유효)
|
|
||||||
openssl req -new -x509 -newkey rsa:4096 -sha256 -nodes \
|
|
||||||
-keyout nginx/ssl/train-kamco.com.key \
|
|
||||||
-out nginx/ssl/train-kamco.com.crt \
|
|
||||||
-days 36500 \
|
|
||||||
-config nginx/ssl/openssl.cnf \
|
|
||||||
-extensions v3_req
|
|
||||||
|
|
||||||
# 파일 권한 설정
|
|
||||||
chmod 600 nginx/ssl/train-kamco.com.key
|
|
||||||
chmod 644 nginx/ssl/train-kamco.com.crt
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 인증서 검증
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 인증서 정보 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout
|
|
||||||
|
|
||||||
# 유효 기간 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | grep -A2 "Validity"
|
|
||||||
|
|
||||||
# SAN (멀티 도메인) 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -text -noout | grep -A1 "Subject Alternative Name"
|
|
||||||
|
|
||||||
# CN 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -noout -subject
|
|
||||||
|
|
||||||
# 개인 키 확인
|
|
||||||
openssl rsa -in nginx/ssl/train-kamco.com.key -check
|
|
||||||
```
|
|
||||||
|
|
||||||
**예상 결과**:
|
|
||||||
```
|
|
||||||
X509v3 Subject Alternative Name:
|
|
||||||
DNS:api.train-kamco.com, DNS:train-kamco.com
|
|
||||||
|
|
||||||
Validity
|
|
||||||
Not Before: Mar 2 23:39:XX 2026 GMT
|
|
||||||
Not After : Feb 6 23:39:XX 2126 GMT
|
|
||||||
```
|
|
||||||
|
|
||||||
## /etc/hosts 설정 (RedHat 9.6)
|
|
||||||
|
|
||||||
### 1. hosts 파일에 도메인 추가
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# root 권한으로 실행
|
|
||||||
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
|
|
||||||
|
|
||||||
# 확인
|
|
||||||
cat /etc/hosts | grep train-kamco
|
|
||||||
```
|
|
||||||
|
|
||||||
**예상 결과**:
|
|
||||||
```
|
|
||||||
127.0.0.1 api.train-kamco.com train-kamco.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 도메인 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# ping 테스트
|
|
||||||
ping -c 2 api.train-kamco.com
|
|
||||||
ping -c 2 train-kamco.com
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Compose 배포
|
|
||||||
|
|
||||||
### 1. 기존 컨테이너 중지 (실행 중인 경우)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/kamco-train-api
|
|
||||||
docker-compose -f docker-compose-prod.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Production 환경 실행
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# IMAGE_TAG 환경 변수 설정 (선택사항)
|
|
||||||
export IMAGE_TAG=latest
|
|
||||||
|
|
||||||
# Docker Compose 실행
|
|
||||||
docker-compose -f docker-compose-prod.yml up -d
|
|
||||||
|
|
||||||
# 컨테이너 상태 확인
|
|
||||||
docker-compose -f docker-compose-prod.yml ps
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 로그 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Nginx 로그
|
|
||||||
docker logs kamco-cd-nginx
|
|
||||||
|
|
||||||
# API 로그
|
|
||||||
docker logs kamco-cd-training-api
|
|
||||||
|
|
||||||
# 실시간 로그 확인
|
|
||||||
docker-compose -f docker-compose-prod.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## HTTPS 접속 테스트
|
|
||||||
|
|
||||||
### 1. HTTP → HTTPS 리다이렉트 테스트
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# api.train-kamco.com
|
|
||||||
curl -I http://api.train-kamco.com
|
|
||||||
|
|
||||||
# train-kamco.com
|
|
||||||
curl -I http://train-kamco.com
|
|
||||||
|
|
||||||
# 예상 결과: 301 Moved Permanently
|
|
||||||
# Location: https://api.train-kamco.com/ 또는 https://train-kamco.com/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. HTTPS 헬스체크 (-k: 사설 인증서 경고 무시)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# api.train-kamco.com
|
|
||||||
curl -k https://api.train-kamco.com/monitor/health
|
|
||||||
|
|
||||||
# train-kamco.com
|
|
||||||
curl -k https://train-kamco.com/monitor/health
|
|
||||||
|
|
||||||
# 예상 결과: {"status":"UP","components":{...}}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. SSL 인증서 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# api.train-kamco.com
|
|
||||||
openssl s_client -connect api.train-kamco.com:443 -showcerts
|
|
||||||
|
|
||||||
# train-kamco.com
|
|
||||||
openssl s_client -connect train-kamco.com:443 -showcerts
|
|
||||||
|
|
||||||
# CN 및 SAN 확인
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 브라우저 테스트
|
|
||||||
|
|
||||||
브라우저에서 다음 URL에 접속:
|
|
||||||
|
|
||||||
- `https://api.train-kamco.com/monitor/health`
|
|
||||||
- `https://train-kamco.com/monitor/health`
|
|
||||||
|
|
||||||
**주의**: 사설 인증서이므로 "안전하지 않음" 경고가 표시됩니다.
|
|
||||||
- **Chrome/Edge**: "고급" → "계속 진행" 클릭
|
|
||||||
- **Firefox**: "위험 감수 및 계속" 클릭
|
|
||||||
|
|
||||||
## 브라우저에서 사설 인증서 신뢰 설정 (선택사항)
|
|
||||||
|
|
||||||
사설 인증서를 브라우저에 등록하면 경고 없이 접속 가능합니다.
|
|
||||||
|
|
||||||
### Chrome/Edge (RedHat Desktop)
|
|
||||||
|
|
||||||
1. `chrome://settings/certificates` 접속
|
|
||||||
2. **Authorities** 탭 선택
|
|
||||||
3. **Import** 클릭
|
|
||||||
4. `nginx/ssl/train-kamco.com.crt` 선택
|
|
||||||
5. **Trust this certificate for identifying websites** 체크
|
|
||||||
6. **OK** 클릭
|
|
||||||
|
|
||||||
### Firefox
|
|
||||||
|
|
||||||
1. `about:preferences#privacy` 접속
|
|
||||||
2. **Certificates** → **View Certificates** 클릭
|
|
||||||
3. **Authorities** 탭 선택
|
|
||||||
4. **Import** 클릭
|
|
||||||
5. `nginx/ssl/train-kamco.com.crt` 선택
|
|
||||||
6. **Trust this CA to identify websites** 체크
|
|
||||||
7. **OK** 클릭
|
|
||||||
|
|
||||||
## 방화벽 설정 (RedHat 9.6)
|
|
||||||
|
|
||||||
### 1. 방화벽 상태 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
sudo firewall-cmd --state
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. HTTP (80) 및 HTTPS (443) 포트 개방
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# HTTP 포트 개방
|
|
||||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
|
|
||||||
# HTTPS 포트 개방
|
|
||||||
sudo firewall-cmd --permanent --add-port=443/tcp
|
|
||||||
|
|
||||||
# 방화벽 재로드
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
|
|
||||||
# 확인
|
|
||||||
sudo firewall-cmd --list-ports
|
|
||||||
```
|
|
||||||
|
|
||||||
**예상 결과**:
|
|
||||||
```
|
|
||||||
80/tcp 443/tcp
|
|
||||||
```
|
|
||||||
|
|
||||||
## 보안 체크리스트
|
|
||||||
|
|
||||||
- [x] `train-kamco.com.key` 파일 권한이 600으로 설정됨
|
|
||||||
- [x] ssl 디렉토리가 버전 관리에서 제외됨 (.gitignore 확인)
|
|
||||||
- [x] 두 도메인(api.train-kamco.com, train-kamco.com) 모두 SAN에 포함됨
|
|
||||||
- [ ] 방화벽에서 80, 443 포트 개방 확인
|
|
||||||
- [x] HSTS 헤더 활성화 확인
|
|
||||||
- [x] TLS 1.2 이상만 허용 확인
|
|
||||||
- [ ] /etc/hosts에 도메인 매핑 확인
|
|
||||||
|
|
||||||
## 트러블슈팅
|
|
||||||
|
|
||||||
### 인증서 관련 오류
|
|
||||||
|
|
||||||
**"certificate verify failed"**
|
|
||||||
```bash
|
|
||||||
# 해결: -k 플래그 사용 (사설 인증서 경고 무시)
|
|
||||||
curl -k https://api.train-kamco.com/monitor/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**"NET::ERR_CERT_AUTHORITY_INVALID" (브라우저)**
|
|
||||||
- 정상 동작: 사설 인증서이므로 브라우저 경고는 예상된 동작입니다
|
|
||||||
- 해결: 브라우저에 인증서 등록 (위 "브라우저에서 사설 인증서 신뢰 설정" 참조)
|
|
||||||
|
|
||||||
### 연결 오류
|
|
||||||
|
|
||||||
**"Connection refused"**
|
|
||||||
```bash
|
|
||||||
# 컨테이너 상태 확인
|
|
||||||
docker ps | grep kamco-cd
|
|
||||||
|
|
||||||
# 포트 바인딩 확인
|
|
||||||
docker port kamco-cd-nginx
|
|
||||||
|
|
||||||
# 예상 결과:
|
|
||||||
# 80/tcp -> 0.0.0.0:80
|
|
||||||
# 443/tcp -> 0.0.0.0:443
|
|
||||||
```
|
|
||||||
|
|
||||||
**"502 Bad Gateway"**
|
|
||||||
```bash
|
|
||||||
# API 컨테이너 상태 확인
|
|
||||||
docker logs kamco-cd-training-api
|
|
||||||
|
|
||||||
# nginx → API 연결 확인
|
|
||||||
docker exec kamco-cd-nginx wget -qO- http://kamco-changedetection-api:8080/monitor/health
|
|
||||||
```
|
|
||||||
|
|
||||||
**"Name or service not known" (도메인 해석 실패)**
|
|
||||||
```bash
|
|
||||||
# /etc/hosts 확인
|
|
||||||
cat /etc/hosts | grep train-kamco
|
|
||||||
|
|
||||||
# 없으면 추가
|
|
||||||
echo "127.0.0.1 api.train-kamco.com train-kamco.com" | sudo tee -a /etc/hosts
|
|
||||||
```
|
|
||||||
|
|
||||||
### 방화벽 관련 오류
|
|
||||||
|
|
||||||
**외부에서 접속 안 됨**
|
|
||||||
```bash
|
|
||||||
# 방화벽 확인
|
|
||||||
sudo firewall-cmd --list-ports
|
|
||||||
|
|
||||||
# 포트 개방
|
|
||||||
sudo firewall-cmd --permanent --add-port=80/tcp
|
|
||||||
sudo firewall-cmd --permanent --add-port=443/tcp
|
|
||||||
sudo firewall-cmd --reload
|
|
||||||
```
|
|
||||||
|
|
||||||
## 인증서 만료 및 갱신
|
|
||||||
|
|
||||||
### 만료 확인
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 인증서 만료일 확인
|
|
||||||
openssl x509 -in nginx/ssl/train-kamco.com.crt -noout -enddate
|
|
||||||
|
|
||||||
# 예상 결과: notAfter=Feb 6 23:39:XX 2126 GMT (100년 후)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 갱신 방법 (필요시)
|
|
||||||
|
|
||||||
100년 유효한 인증서이므로 갱신이 필요하지 않지만, 재생성이 필요한 경우:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 기존 인증서 백업
|
|
||||||
cp nginx/ssl/train-kamco.com.crt nginx/ssl/train-kamco.com.crt.bak
|
|
||||||
cp nginx/ssl/train-kamco.com.key nginx/ssl/train-kamco.com.key.bak
|
|
||||||
|
|
||||||
# 위의 "사설 SSL 인증서 생성" 단계 재실행
|
|
||||||
|
|
||||||
# nginx 재시작
|
|
||||||
docker-compose -f docker-compose-prod.yml restart nginx
|
|
||||||
```
|
|
||||||
|
|
||||||
## 주의사항
|
|
||||||
|
|
||||||
1. **사설 인증서 경고**: 브라우저에서 "안전하지 않음" 경고가 표시됩니다. 프로덕션 환경에서는 **공인 인증서(Let's Encrypt, GlobalSign 등) 사용을 권장**합니다.
|
|
||||||
|
|
||||||
2. **포트 80/443**: Docker가 자동으로 처리하지만, 이미 사용 중인 프로세스가 있으면 충돌할 수 있습니다.
|
|
||||||
```bash
|
|
||||||
# 포트 사용 확인
|
|
||||||
sudo lsof -i :80
|
|
||||||
sudo lsof -i :443
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **대용량 파일 업로드**: `client_max_body_size`를 10GB로 설정했으므로, 서버 메모리 및 디스크 용량을 충분히 확보하세요.
|
|
||||||
|
|
||||||
4. **인증서 백업**: `train-kamco.com.key` 파일은 매우 중요합니다. 안전한 곳에 백업하세요.
|
|
||||||
|
|
||||||
5. **SELinux**: RedHat 9.6에서 SELinux가 활성화된 경우, Docker 볼륨 마운트 권한 문제가 발생할 수 있습니다.
|
|
||||||
```bash
|
|
||||||
# SELinux 상태 확인
|
|
||||||
getenforce
|
|
||||||
|
|
||||||
# 필요시 permissive 모드로 변경
|
|
||||||
sudo setenforce 0
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
### 2단계: 시스템 신뢰 폴더로 복사
|
|
||||||
터미널을 열고 관리자 권한(sudo)을 사용해 인증서를 시스템 폴더로 복사합니다.
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo cp mycert.crt /etc/pki/ca-trust/source/anchors/
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3단계: 시스템 신뢰 목록 업데이트
|
|
||||||
아래 명령어를 입력해 추가한 인증서를 시스템에 갱신시킵니다.
|
|
||||||
|
|
||||||
```
|
|
||||||
sudo update-ca-trust
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 참고 자료
|
|
||||||
|
|
||||||
- [OpenSSL Documentation](https://www.openssl.org/docs/)
|
|
||||||
- [Nginx SSL Configuration](https://nginx.org/en/docs/http/configuring_https_servers.html)
|
|
||||||
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
|
||||||
- [Let's Encrypt (공인 인증서)](https://letsencrypt.org/)
|
|
||||||
179
nginx/nginx.conf
179
nginx/nginx.conf
@@ -1,179 +0,0 @@
|
|||||||
events {
|
|
||||||
worker_connections 1024;
|
|
||||||
}
|
|
||||||
|
|
||||||
http {
|
|
||||||
include /etc/nginx/mime.types;
|
|
||||||
default_type application/octet-stream;
|
|
||||||
|
|
||||||
# 로그 설정
|
|
||||||
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
|
|
||||||
'$status $body_bytes_sent "$http_referer" '
|
|
||||||
'"$http_user_agent" "$http_x_forwarded_for"';
|
|
||||||
|
|
||||||
access_log /var/log/nginx/access.log main;
|
|
||||||
error_log /var/log/nginx/error.log warn;
|
|
||||||
|
|
||||||
sendfile on;
|
|
||||||
keepalive_timeout 65;
|
|
||||||
|
|
||||||
# 업로드 파일 크기 제한 (10GB)
|
|
||||||
client_max_body_size 10G;
|
|
||||||
|
|
||||||
# Upstream 설정
|
|
||||||
upstream api_backend {
|
|
||||||
server kamco-train-api:8080;
|
|
||||||
}
|
|
||||||
|
|
||||||
upstream web_backend {
|
|
||||||
server kamco-train-web:3002;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTP → HTTPS 리다이렉트 서버
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name api.train-kamco.com train-kamco.com;
|
|
||||||
|
|
||||||
# 모든 HTTP 요청을 HTTPS로 리다이렉트
|
|
||||||
return 301 https://$server_name$request_uri;
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS 서버 설정
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name api.train-kamco.com;
|
|
||||||
|
|
||||||
# SSL 인증서 설정 (사설 인증서 - 멀티 도메인)
|
|
||||||
ssl_certificate /etc/nginx/ssl/train-kamco.com.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/train-kamco.com.key;
|
|
||||||
|
|
||||||
# SSL 프로토콜 및 암호화 설정
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
# SSL 세션 캐시
|
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
ssl_session_timeout 10m;
|
|
||||||
|
|
||||||
# HSTS (HTTP Strict Transport Security)
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
|
|
||||||
# 보안 헤더
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
|
|
||||||
# 프록시 설정
|
|
||||||
location / {
|
|
||||||
proxy_pass http://api_backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# 프록시 헤더 설정
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $server_name;
|
|
||||||
|
|
||||||
# 인증 헤더 및 쿠키 전달 (JWT 토큰 전달 보장)
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_set_header Cookie $http_cookie;
|
|
||||||
proxy_set_header Authorization $http_authorization;
|
|
||||||
|
|
||||||
# 타임아웃 설정 (대용량 파일 업로드 지원)
|
|
||||||
proxy_connect_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
|
|
||||||
# 버퍼 설정
|
|
||||||
proxy_buffering on;
|
|
||||||
proxy_buffer_size 4k;
|
|
||||||
proxy_buffers 8 4k;
|
|
||||||
proxy_busy_buffers_size 8k;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 헬스체크 엔드포인트
|
|
||||||
location /monitor/health {
|
|
||||||
proxy_pass http://api_backend/monitor/health;
|
|
||||||
access_log off;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# HTTPS 서버 설정 - Next.js Web Application
|
|
||||||
server {
|
|
||||||
listen 443 ssl http2;
|
|
||||||
server_name train-kamco.com;
|
|
||||||
|
|
||||||
# SSL 인증서 설정 (사설 인증서 - 멀티 도메인)
|
|
||||||
ssl_certificate /etc/nginx/ssl/train-kamco.com.crt;
|
|
||||||
ssl_certificate_key /etc/nginx/ssl/train-kamco.com.key;
|
|
||||||
|
|
||||||
# SSL 프로토콜 및 암호화 설정
|
|
||||||
ssl_protocols TLSv1.2 TLSv1.3;
|
|
||||||
ssl_ciphers 'ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384';
|
|
||||||
ssl_prefer_server_ciphers off;
|
|
||||||
|
|
||||||
# SSL 세션 캐시
|
|
||||||
ssl_session_cache shared:SSL:10m;
|
|
||||||
ssl_session_timeout 10m;
|
|
||||||
|
|
||||||
# HSTS (HTTP Strict Transport Security)
|
|
||||||
add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
|
|
||||||
|
|
||||||
# 보안 헤더
|
|
||||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
|
||||||
add_header X-Content-Type-Options "nosniff" always;
|
|
||||||
add_header X-XSS-Protection "1; mode=block" always;
|
|
||||||
|
|
||||||
# API 프록시 설정 (Web에서 API 호출 시)
|
|
||||||
location /api/ {
|
|
||||||
proxy_pass http://api_backend/api/;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# 프록시 헤더 설정
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $server_name;
|
|
||||||
|
|
||||||
# 인증 헤더 및 쿠키 전달
|
|
||||||
proxy_pass_request_headers on;
|
|
||||||
proxy_set_header Cookie $http_cookie;
|
|
||||||
|
|
||||||
# 타임아웃 설정
|
|
||||||
proxy_connect_timeout 300s;
|
|
||||||
proxy_send_timeout 300s;
|
|
||||||
proxy_read_timeout 300s;
|
|
||||||
}
|
|
||||||
|
|
||||||
# 프록시 설정
|
|
||||||
location / {
|
|
||||||
proxy_pass http://web_backend;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
|
|
||||||
# 프록시 헤더 설정
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_set_header X-Real-IP $remote_addr;
|
|
||||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
||||||
proxy_set_header X-Forwarded-Proto $scheme;
|
|
||||||
proxy_set_header X-Forwarded-Host $server_name;
|
|
||||||
|
|
||||||
# Next.js WebSocket 지원을 위한 Upgrade 헤더
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection "upgrade";
|
|
||||||
|
|
||||||
# 타임아웃 설정
|
|
||||||
proxy_connect_timeout 600s;
|
|
||||||
proxy_send_timeout 600s;
|
|
||||||
proxy_read_timeout 600s;
|
|
||||||
|
|
||||||
# 버퍼 설정
|
|
||||||
proxy_buffering on;
|
|
||||||
proxy_buffer_size 4k;
|
|
||||||
proxy_buffers 8 4k;
|
|
||||||
proxy_busy_buffers_size 8k;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -719,18 +719,26 @@ public class FIleChecker {
|
|||||||
public static void unzip(String fileName, String destDirectory) throws IOException {
|
public static void unzip(String fileName, String destDirectory) throws IOException {
|
||||||
String zipFilePath = destDirectory + File.separator + fileName;
|
String zipFilePath = destDirectory + File.separator + fileName;
|
||||||
|
|
||||||
|
log.info("fileName : {}", fileName);
|
||||||
|
log.info("destDirectory : {}", destDirectory);
|
||||||
|
log.info("zipFilePath : {}", zipFilePath);
|
||||||
// zip 이름으로 폴더 생성 (확장자 제거)
|
// zip 이름으로 폴더 생성 (확장자 제거)
|
||||||
String folderName =
|
String folderName =
|
||||||
fileName.endsWith(".zip") ? fileName.substring(0, fileName.length() - 4) : fileName;
|
fileName.endsWith(".zip") ? fileName.substring(0, fileName.length() - 4) : fileName;
|
||||||
|
log.info("folderName : {}", folderName);
|
||||||
|
|
||||||
File destDir = new File(destDirectory, folderName);
|
File destDir = new File(destDirectory, folderName);
|
||||||
|
log.info("destDir : {}", destDir);
|
||||||
|
|
||||||
// 동일 폴더가 이미 있으면 삭제
|
// 동일 폴더가 이미 있으면 삭제
|
||||||
|
log.info("111 destDir.exists() : {}", destDir.exists());
|
||||||
if (destDir.exists()) {
|
if (destDir.exists()) {
|
||||||
deleteDirectoryRecursively(destDir.toPath());
|
deleteDirectoryRecursively(destDir.toPath());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.info("222 destDir.exists() : {}", destDir.exists());
|
||||||
if (!destDir.exists()) {
|
if (!destDir.exists()) {
|
||||||
|
log.info("mkdirs : {}", destDir.exists());
|
||||||
destDir.mkdirs();
|
destDir.mkdirs();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package com.kamco.cd.training.config;
|
|||||||
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
|
import com.kamco.cd.training.auth.CustomAuthenticationProvider;
|
||||||
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
|
import com.kamco.cd.training.auth.JwtAuthenticationFilter;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
@@ -26,9 +25,6 @@ import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
|
|||||||
@EnableWebSecurity
|
@EnableWebSecurity
|
||||||
public class SecurityConfig {
|
public class SecurityConfig {
|
||||||
|
|
||||||
@Value("${cors.allowed-origins}")
|
|
||||||
private List<String> allowedOrigins;
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public SecurityFilterChain securityFilterChain(
|
public SecurityFilterChain securityFilterChain(
|
||||||
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
|
org.springframework.security.config.annotation.web.builders.HttpSecurity http,
|
||||||
@@ -108,19 +104,15 @@ public class SecurityConfig {
|
|||||||
return new BCryptPasswordEncoder();
|
return new BCryptPasswordEncoder();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** CORS 설정 - application.yml에서 환경별로 관리 */
|
/** CORS 설정 */
|
||||||
@Bean
|
@Bean
|
||||||
public CorsConfigurationSource corsConfigurationSource() {
|
public CorsConfigurationSource corsConfigurationSource() {
|
||||||
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
|
CorsConfiguration config = new CorsConfiguration(); // CORS 객체 생성
|
||||||
|
config.setAllowedOriginPatterns(List.of("*")); // 도메인 허용
|
||||||
// application.yml에서 환경별로 설정된 도메인 사용
|
|
||||||
config.setAllowedOriginPatterns(allowedOrigins);
|
|
||||||
|
|
||||||
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
config.setAllowedMethods(List.of("GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"));
|
||||||
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
|
config.setAllowedHeaders(List.of("*")); // 헤더요청 Authorization, Content-Type, X-Custom-Header
|
||||||
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
|
config.setAllowCredentials(true); // 쿠키, Authorization 헤더, Bearer Token 등 자격증명 포함 요청을 허용할지 설정
|
||||||
config.setExposedHeaders(List.of("Content-Disposition", "Authorization"));
|
config.setExposedHeaders(List.of("Content-Disposition"));
|
||||||
config.setMaxAge(3600L); // Preflight 요청 캐시 (1시간)
|
|
||||||
|
|
||||||
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
|
||||||
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
|
/** "/**" → 모든 API 경로에 대해 이 CORS 규칙을 적용 /api/** 같이 특정 경로만 지정 가능. */
|
||||||
|
|||||||
@@ -398,6 +398,24 @@ public class DatasetService {
|
|||||||
return datasetCoreService.getFilePathByUUIDPathType(uuid, pathType);
|
return datasetCoreService.getFilePathByUUIDPathType(uuid, pathType);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String readRemoteFileAsString(String remoteFilePath) {
|
||||||
|
|
||||||
|
String command = "cat " + escape(remoteFilePath);
|
||||||
|
|
||||||
|
List<String> lines = FIleChecker.execCommandAndReadLines(command);
|
||||||
|
|
||||||
|
return String.join("\n", lines);
|
||||||
|
}
|
||||||
|
|
||||||
|
private JsonNode parseJson(String json) {
|
||||||
|
try {
|
||||||
|
ObjectMapper mapper = new ObjectMapper();
|
||||||
|
return mapper.readTree(json);
|
||||||
|
} catch (IOException e) {
|
||||||
|
throw new RuntimeException("JSON 파싱 실패", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private String escape(String path) {
|
private String escape(String path) {
|
||||||
// 쉘 커맨드에서 안전하게 사용할 수 있도록 문자열을 작은따옴표로 감싸면서, 내부의 작은따옴표를 이스케이프 처리
|
// 쉘 커맨드에서 안전하게 사용할 수 있도록 문자열을 작은따옴표로 감싸면서, 내부의 작은따옴표를 이스케이프 처리
|
||||||
return "'" + path.replace("'", "'\"'\"'") + "'";
|
return "'" + path.replace("'", "'\"'\"'") + "'";
|
||||||
|
|||||||
@@ -430,44 +430,6 @@ public class ModelTrainMngCoreService {
|
|||||||
master.setUpdatedDttm(ZonedDateTime.now());
|
master.setUpdatedDttm(ZonedDateTime.now());
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* step1 정지 처리
|
|
||||||
*
|
|
||||||
* @param modelId
|
|
||||||
* @param errorMessage
|
|
||||||
*/
|
|
||||||
public void markStep1Stop(Long modelId, String errorMessage) {
|
|
||||||
ModelMasterEntity master =
|
|
||||||
modelMngRepository
|
|
||||||
.findById(modelId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Model not found: " + modelId));
|
|
||||||
|
|
||||||
master.setStatusCd(TrainStatusType.STOPPED.getId());
|
|
||||||
master.setStep1State(TrainStatusType.STOPPED.getId());
|
|
||||||
master.setLastError(errorMessage);
|
|
||||||
master.setUpdatedUid(userUtil.getId());
|
|
||||||
master.setUpdatedDttm(ZonedDateTime.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* step2 정지 처리
|
|
||||||
*
|
|
||||||
* @param modelId
|
|
||||||
* @param errorMessage
|
|
||||||
*/
|
|
||||||
public void markStep2Stop(Long modelId, String errorMessage) {
|
|
||||||
ModelMasterEntity master =
|
|
||||||
modelMngRepository
|
|
||||||
.findById(modelId)
|
|
||||||
.orElseThrow(() -> new IllegalArgumentException("Model not found: " + modelId));
|
|
||||||
|
|
||||||
master.setStatusCd(TrainStatusType.STOPPED.getId());
|
|
||||||
master.setStep2State(TrainStatusType.STOPPED.getId());
|
|
||||||
master.setLastError(errorMessage);
|
|
||||||
master.setUpdatedUid(userUtil.getId());
|
|
||||||
master.setUpdatedDttm(ZonedDateTime.now());
|
|
||||||
}
|
|
||||||
|
|
||||||
@Transactional
|
@Transactional
|
||||||
public void markSuccess(Long modelId) {
|
public void markSuccess(Long modelId) {
|
||||||
ModelMasterEntity master =
|
ModelMasterEntity master =
|
||||||
|
|||||||
@@ -222,9 +222,7 @@ public class DataSetCountersService {
|
|||||||
log.info("missing = {}", missing.size());
|
log.info("missing = {}", missing.size());
|
||||||
log.info("extra = {}", extra.size());
|
log.info("extra = {}", extra.size());
|
||||||
|
|
||||||
log.info("[MISSING] total = {}", missing.size());
|
|
||||||
missing.stream().sorted().limit(50).forEach(f -> log.warn("[MISSING] {}", f));
|
missing.stream().sorted().limit(50).forEach(f -> log.warn("[MISSING] {}", f));
|
||||||
log.info("[EXTRA] total = {}", extra.size());
|
|
||||||
extra.stream().sorted().limit(50).forEach(f -> log.warn("[EXTRA] {}", f));
|
extra.stream().sorted().limit(50).forEach(f -> log.warn("[EXTRA] {}", f));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,9 +20,7 @@ import java.util.stream.Stream;
|
|||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.extern.log4j.Log4j2;
|
import lombok.extern.log4j.Log4j2;
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
import org.springframework.boot.context.event.ApplicationReadyEvent;
|
|
||||||
import org.springframework.context.annotation.Profile;
|
import org.springframework.context.annotation.Profile;
|
||||||
import org.springframework.context.event.EventListener;
|
|
||||||
import org.springframework.stereotype.Component;
|
import org.springframework.stereotype.Component;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
@@ -43,7 +41,6 @@ public class JobRecoveryOnStartupService {
|
|||||||
|
|
||||||
private final ModelTrainJobCoreService modelTrainJobCoreService;
|
private final ModelTrainJobCoreService modelTrainJobCoreService;
|
||||||
private final ModelTrainMngCoreService modelTrainMngCoreService;
|
private final ModelTrainMngCoreService modelTrainMngCoreService;
|
||||||
private final ModelTrainMetricsJobService modelTrainMetricsJobService;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Docker 컨테이너가 쓰는 response(산출물) 디렉토리의 "호스트 측" 베이스 경로. 예) /data/train/response
|
* Docker 컨테이너가 쓰는 response(산출물) 디렉토리의 "호스트 측" 베이스 경로. 예) /data/train/response
|
||||||
@@ -59,7 +56,7 @@ public class JobRecoveryOnStartupService {
|
|||||||
* <p>@Transactional: - recover() 메서드 전체가 하나의 트랜잭션으로 감싸집니다. - Job 하나씩 처리하다가 예외가 발생하면 전체 롤백이 될 수
|
* <p>@Transactional: - recover() 메서드 전체가 하나의 트랜잭션으로 감싸집니다. - Job 하나씩 처리하다가 예외가 발생하면 전체 롤백이 될 수
|
||||||
* 있으므로 "잡 단위로 확실히 커밋"이 필요하면 (권장) 잡 단위로 분리 트랜잭션(REQUIRES_NEW) 고려하세요.
|
* 있으므로 "잡 단위로 확실히 커밋"이 필요하면 (권장) 잡 단위로 분리 트랜잭션(REQUIRES_NEW) 고려하세요.
|
||||||
*/
|
*/
|
||||||
@EventListener(ApplicationReadyEvent.class)
|
// @EventListener(ApplicationReadyEvent.class)
|
||||||
@Transactional
|
@Transactional
|
||||||
public void recover() {
|
public void recover() {
|
||||||
|
|
||||||
@@ -94,92 +91,57 @@ public class JobRecoveryOnStartupService {
|
|||||||
if (out.completed()) {
|
if (out.completed()) {
|
||||||
log.info("[RECOVERY] outputs look completed. mark SUCCESS. jobId={}", job.getId());
|
log.info("[RECOVERY] outputs look completed. mark SUCCESS. jobId={}", job.getId());
|
||||||
modelTrainJobCoreService.markSuccess(job.getId(), 0);
|
modelTrainJobCoreService.markSuccess(job.getId(), 0);
|
||||||
// model 상태 변경
|
|
||||||
markStepSuccessByJobType(job);
|
markStepSuccessByJobType(job);
|
||||||
// 결과 csv 파일 정보 등록
|
|
||||||
modelTrainMetricsJobService.findTrainValidMetricCsvFiles();
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
// 3-3) 산출물이 부족하면 실패 처리(운영 정책에 따라 "유예"도 가능)
|
||||||
// 3-3) 산출물이 부족하면 중단처리
|
|
||||||
// 산출물이 부족하면 "중단/보류"로 처리
|
|
||||||
// 운영자가 재시작 할 수 있게 한다.
|
|
||||||
log.warn(
|
log.warn(
|
||||||
"[RECOVERY] outputs incomplete. mark PAUSED/STOP for restart. jobId={} reason={}",
|
"[RECOVERY] outputs incomplete. mark FAILED. jobId={} reason={}",
|
||||||
job.getId(),
|
job.getId(),
|
||||||
out.reason());
|
out.reason());
|
||||||
|
|
||||||
Integer modelId = job.getModelId() == null ? null : Math.toIntExact(job.getModelId());
|
modelTrainJobCoreService.markFailed(
|
||||||
|
job.getId(), -1, "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE");
|
||||||
|
|
||||||
// PAUSED/STOP
|
markStepErrorByJobType(job, out.reason());
|
||||||
modelTrainJobCoreService.markPaused(
|
|
||||||
job.getId(), modelId, "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE");
|
|
||||||
|
|
||||||
// 모델도 에러가 아니라 STOP으로
|
|
||||||
markStepStopByJobType(
|
|
||||||
job, "SERVER_RESTART_CONTAINER_MISSING_OUTPUT_INCOMPLETE: " + out.reason());
|
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) 컨테이너는 존재하고, 아직 running=true
|
// 4) 컨테이너는 존재하고, 아직 running=true
|
||||||
// - 서버만 재기동됐고 컨테이너는 그대로 살아있는 케이스
|
// - 서버만 재기동됐고 컨테이너는 그대로 살아있는 케이스
|
||||||
// - 실행중 docker 를 kill 하고 이어하기를 한다,
|
// - 이 경우 DB를 건드리면 오히려 꼬일 수 있으니 RUNNING 유지
|
||||||
if (state.running()) {
|
if (state.running()) {
|
||||||
log.warn("[RECOVERY] container still running. force kill. container={}", containerName);
|
log.info("[RECOVERY] container still running. container={}", containerName);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// ============================================================
|
ProcessBuilder pb = new ProcessBuilder("docker", "stop", "-t", "20", containerName);
|
||||||
// 1) docker kill (SIGKILL) 바로 전송
|
|
||||||
// - kill은 즉시 종료
|
|
||||||
// ============================================================
|
|
||||||
ProcessBuilder pb = new ProcessBuilder("docker", "kill", containerName);
|
|
||||||
pb.redirectErrorStream(true);
|
pb.redirectErrorStream(true);
|
||||||
|
|
||||||
Process p = pb.start();
|
Process p = pb.start();
|
||||||
|
|
||||||
boolean finished = p.waitFor(20, TimeUnit.SECONDS);
|
boolean finished = p.waitFor(30, TimeUnit.SECONDS);
|
||||||
if (!finished) {
|
if (!finished) {
|
||||||
p.destroyForcibly();
|
p.destroyForcibly();
|
||||||
throw new IOException("docker kill timeout");
|
throw new IOException("docker stop timeout");
|
||||||
}
|
}
|
||||||
|
|
||||||
int code = p.exitValue();
|
int code = p.exitValue();
|
||||||
if (code != 0) {
|
if (code != 0) {
|
||||||
throw new IOException("docker kill failed. exit=" + code);
|
throw new IOException("docker stop failed. exit=" + code);
|
||||||
}
|
}
|
||||||
|
|
||||||
// ============================================================
|
log.info(
|
||||||
// 2) kill 후 실제로 죽었는지 확인
|
"[RECOVERY] container stopped (will be auto removed by --rm). container={}",
|
||||||
// ============================================================
|
containerName);
|
||||||
DockerInspectState after = inspectContainer(containerName);
|
|
||||||
if (after.exists() && after.running()) {
|
|
||||||
throw new IOException("docker kill returned 0 but container still running");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.info("[RECOVERY] container killed successfully. container={}", containerName);
|
// 여기서 상태를 PAUSED로 바꿔도 되고
|
||||||
|
modelTrainJobCoreService.markPaused(job.getId(), -1, "AUTO_STOP_FAILED_ON_RESTART");
|
||||||
// ============================================================
|
|
||||||
// 3) job 상태를 PAUSED로 변경 (서버 재기동으로 강제 중단)
|
|
||||||
// ============================================================
|
|
||||||
Integer modelId = job.getModelId() == null ? null : Math.toIntExact(job.getModelId());
|
|
||||||
|
|
||||||
modelTrainJobCoreService.markPaused(
|
|
||||||
job.getId(), modelId, "AUTO_KILLED_ON_SERVER_RESTART");
|
|
||||||
|
|
||||||
log.info("job = {}", job);
|
|
||||||
markStepStopByJobType(job, "AUTO_KILLED_ON_SERVER_RESTART");
|
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
log.error("[RECOVERY] docker stop failed. container={}", containerName, e);
|
||||||
|
|
||||||
log.error("[RECOVERY] docker kill failed. container={}", containerName, e);
|
modelTrainJobCoreService.markFailed(job.getId(), -1, "AUTO_STOP_FAILED_ON_RESTART");
|
||||||
|
|
||||||
modelTrainJobCoreService.markFailed(
|
|
||||||
job.getId(), -1, "AUTO_KILL_FAILED_ON_SERVER_RESTART");
|
|
||||||
|
|
||||||
markStepErrorByJobType(job, "AUTO_KILL_FAILED_ON_SERVER_RESTART");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -192,10 +154,7 @@ public class JobRecoveryOnStartupService {
|
|||||||
if (exitCode != null && exitCode == 0) {
|
if (exitCode != null && exitCode == 0) {
|
||||||
log.info("[RECOVERY] container exited(0). mark SUCCESS. container={}", containerName);
|
log.info("[RECOVERY] container exited(0). mark SUCCESS. container={}", containerName);
|
||||||
modelTrainJobCoreService.markSuccess(job.getId(), 0);
|
modelTrainJobCoreService.markSuccess(job.getId(), 0);
|
||||||
// model 상태 변경
|
|
||||||
markStepSuccessByJobType(job);
|
markStepSuccessByJobType(job);
|
||||||
// 결과 csv 파일 정보 등록
|
|
||||||
modelTrainMetricsJobService.findTrainValidMetricCsvFiles();
|
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
// 5-2) exitCode != 0 이거나 null이면 실패로 간주 → FAILED 처리
|
// 5-2) exitCode != 0 이거나 null이면 실패로 간주 → FAILED 처리
|
||||||
@@ -207,7 +166,7 @@ public class JobRecoveryOnStartupService {
|
|||||||
|
|
||||||
modelTrainJobCoreService.markFailed(
|
modelTrainJobCoreService.markFailed(
|
||||||
job.getId(), exitCode, "SERVER_RESTART_CONTAINER_EXIT_NONZERO");
|
job.getId(), exitCode, "SERVER_RESTART_CONTAINER_EXIT_NONZERO");
|
||||||
// model 상태 변경
|
|
||||||
markStepErrorByJobType(job, "exit=" + exitCode + " status=" + status);
|
markStepErrorByJobType(job, "exit=" + exitCode + " status=" + status);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -219,7 +178,7 @@ public class JobRecoveryOnStartupService {
|
|||||||
|
|
||||||
modelTrainJobCoreService.markFailed(
|
modelTrainJobCoreService.markFailed(
|
||||||
job.getId(), -1, "SERVER_RESTART_CONTAINER_INSPECT_ERROR");
|
job.getId(), -1, "SERVER_RESTART_CONTAINER_INSPECT_ERROR");
|
||||||
// model 상태 변경
|
|
||||||
markStepErrorByJobType(job, "inspect-error");
|
markStepErrorByJobType(job, "inspect-error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,16 +204,6 @@ public class JobRecoveryOnStartupService {
|
|||||||
*
|
*
|
||||||
* <p>예: - jobType == "EVAL" → step2(평가 단계) 에러 - 그 외 → step1 혹은 전체 에러
|
* <p>예: - jobType == "EVAL" → step2(평가 단계) 에러 - 그 외 → step1 혹은 전체 에러
|
||||||
*/
|
*/
|
||||||
private void markStepStopByJobType(ModelTrainJobDto job, String msg) {
|
|
||||||
Map<String, Object> params = job.getParamsJson();
|
|
||||||
boolean isEval = params != null && "EVAL".equals(String.valueOf(params.get("jobType")));
|
|
||||||
if (isEval) {
|
|
||||||
modelTrainMngCoreService.markStep2Stop(job.getModelId(), msg);
|
|
||||||
} else {
|
|
||||||
modelTrainMngCoreService.markStep1Stop(job.getModelId(), msg);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void markStepErrorByJobType(ModelTrainJobDto job, String msg) {
|
private void markStepErrorByJobType(ModelTrainJobDto job, String msg) {
|
||||||
Map<String, Object> params = job.getParamsJson();
|
Map<String, Object> params = job.getParamsJson();
|
||||||
boolean isEval = params != null && "EVAL".equals(String.valueOf(params.get("jobType")));
|
boolean isEval = params != null && "EVAL".equals(String.valueOf(params.get("jobType")));
|
||||||
@@ -361,66 +310,33 @@ public class JobRecoveryOnStartupService {
|
|||||||
*/
|
*/
|
||||||
private OutputResult probeOutputs(ModelTrainJobDto job) {
|
private OutputResult probeOutputs(ModelTrainJobDto job) {
|
||||||
try {
|
try {
|
||||||
|
|
||||||
log.info(
|
|
||||||
"[RECOVERY] probeOutputs start. jobId={}, modelId={}", job.getId(), job.getModelId());
|
|
||||||
|
|
||||||
// 1) 출력 디렉토리 확인
|
|
||||||
Path outDir = resolveOutputDir(job);
|
Path outDir = resolveOutputDir(job);
|
||||||
if (outDir == null || !Files.isDirectory(outDir)) {
|
if (outDir == null || !Files.isDirectory(outDir)) {
|
||||||
log.warn("[RECOVERY] output directory missing. jobId={}, path={}", job.getId(), outDir);
|
|
||||||
return new OutputResult(false, "output-dir-missing");
|
return new OutputResult(false, "output-dir-missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[RECOVERY] output directory found. jobId={}, path={}", job.getId(), outDir);
|
|
||||||
|
|
||||||
// 2) totalEpoch 확인
|
|
||||||
Integer totalEpoch = extractTotalEpoch(job).orElse(null);
|
Integer totalEpoch = extractTotalEpoch(job).orElse(null);
|
||||||
if (totalEpoch == null || totalEpoch <= 0) {
|
if (totalEpoch == null || totalEpoch <= 0) {
|
||||||
log.warn(
|
|
||||||
"[RECOVERY] totalEpoch missing or invalid. jobId={}, totalEpoch={}",
|
|
||||||
job.getId(),
|
|
||||||
totalEpoch);
|
|
||||||
return new OutputResult(false, "total-epoch-missing");
|
return new OutputResult(false, "total-epoch-missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.info("[RECOVERY] totalEpoch={}. jobId={}", totalEpoch, job.getId());
|
|
||||||
|
|
||||||
// 3) val.csv 존재 확인
|
|
||||||
Path valCsv = outDir.resolve("val.csv");
|
Path valCsv = outDir.resolve("val.csv");
|
||||||
if (!Files.exists(valCsv)) {
|
if (!Files.exists(valCsv)) {
|
||||||
log.warn("[RECOVERY] val.csv missing. jobId={}, path={}", job.getId(), valCsv);
|
|
||||||
return new OutputResult(false, "val.csv-missing");
|
return new OutputResult(false, "val.csv-missing");
|
||||||
}
|
}
|
||||||
|
|
||||||
// 4) val.csv 라인 수 확인
|
|
||||||
long lines = countNonHeaderLines(valCsv);
|
long lines = countNonHeaderLines(valCsv);
|
||||||
|
|
||||||
log.info(
|
// “같아야 완료” 정책
|
||||||
"[RECOVERY] val.csv lines counted. jobId={}, lines={}, expected={}",
|
|
||||||
job.getId(),
|
|
||||||
lines,
|
|
||||||
totalEpoch);
|
|
||||||
|
|
||||||
// 5) 완료 판정
|
|
||||||
if (lines == totalEpoch) {
|
if (lines == totalEpoch) {
|
||||||
log.info("[RECOVERY] outputs look COMPLETE. jobId={}", job.getId());
|
|
||||||
return new OutputResult(true, "ok");
|
return new OutputResult(true, "ok");
|
||||||
}
|
}
|
||||||
|
|
||||||
log.warn(
|
|
||||||
"[RECOVERY] val.csv line mismatch. jobId={}, lines={}, expected={}",
|
|
||||||
job.getId(),
|
|
||||||
lines,
|
|
||||||
totalEpoch);
|
|
||||||
|
|
||||||
return new OutputResult(
|
return new OutputResult(
|
||||||
false, "val.csv-lines-mismatch lines=" + lines + " expected=" + totalEpoch);
|
false, "val.csv-lines-mismatch lines=" + lines + " expected=" + totalEpoch);
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
|
||||||
log.error("[RECOVERY] probeOutputs error. jobId={}", job.getId(), e);
|
log.error("[RECOVERY] probeOutputs error. jobId={}", job.getId(), e);
|
||||||
|
|
||||||
return new OutputResult(false, "probe-error");
|
return new OutputResult(false, "probe-error");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,19 +268,13 @@ public class TrainJobService {
|
|||||||
try {
|
try {
|
||||||
// 데이터셋 심볼링크 생성
|
// 데이터셋 심볼링크 생성
|
||||||
// String pathUid = tmpDatasetService.buildTmpDatasetSymlink(raw, uids);
|
// String pathUid = tmpDatasetService.buildTmpDatasetSymlink(raw, uids);
|
||||||
// train path 모델 클래스별 조회
|
// train path
|
||||||
List<ModelTrainLinkDto> trainList = modelTrainMngCoreService.findDatasetTrainPath(modelId);
|
List<ModelTrainLinkDto> trainList = modelTrainMngCoreService.findDatasetTrainPath(modelId);
|
||||||
// validation path 모델 클래스별 조회
|
// validation path
|
||||||
List<ModelTrainLinkDto> valList = modelTrainMngCoreService.findDatasetValPath(modelId);
|
List<ModelTrainLinkDto> valList = modelTrainMngCoreService.findDatasetValPath(modelId);
|
||||||
// test path 모델 클래스별 조회
|
// test path
|
||||||
List<ModelTrainLinkDto> testList = modelTrainMngCoreService.findDatasetTestPath(modelId);
|
List<ModelTrainLinkDto> testList = modelTrainMngCoreService.findDatasetTestPath(modelId);
|
||||||
|
|
||||||
log.info(
|
|
||||||
"createTmpFile class list trainList = {} valList = {} testList = {}",
|
|
||||||
trainList.size(),
|
|
||||||
valList.size(),
|
|
||||||
testList.size());
|
|
||||||
|
|
||||||
// train 데이터셋 심볼링크 생성
|
// train 데이터셋 심볼링크 생성
|
||||||
tmpDatasetService.buildTmpDatasetHardlink(raw, "train", trainList);
|
tmpDatasetService.buildTmpDatasetHardlink(raw, "train", trainList);
|
||||||
// val 데이터셋 심볼링크 생성
|
// val 데이터셋 심볼링크 생성
|
||||||
|
|||||||
@@ -108,10 +108,6 @@ public class TrainJobWorker {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 0 정상 종료 SUCCESS 1~125 학습 코드 에러 FAILED 137 OOMKill FAILED 143 SIGTERM (stop) STOP -1 우리 내부
|
|
||||||
* 강제 중단 STOP
|
|
||||||
*/
|
|
||||||
if (result.getExitCode() == 0) {
|
if (result.getExitCode() == 0) {
|
||||||
// 성공 처리
|
// 성공 처리
|
||||||
modelTrainJobCoreService.markSuccess(jobId, result.getExitCode());
|
modelTrainJobCoreService.markSuccess(jobId, result.getExitCode());
|
||||||
@@ -128,34 +124,17 @@ public class TrainJobWorker {
|
|||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
String failMsg = result.getStatus() + "\n" + result.getLogs();
|
String failMsg = result.getStatus() + "\n" + result.getLogs();
|
||||||
log.info("training fail exitCode={} Msg ={}", result.getExitCode(), failMsg);
|
// 실패 처리
|
||||||
|
modelTrainJobCoreService.markFailed(
|
||||||
|
jobId, result.getExitCode(), result.getStatus() + "\n" + result.getLogs());
|
||||||
|
|
||||||
if (result.getExitCode() == -1 || result.getExitCode() == 143) {
|
if (isEval) {
|
||||||
// 실패 처리
|
// 오류 정보 등록
|
||||||
modelTrainJobCoreService.markPaused(
|
modelTrainMngCoreService.markStep2Error(modelId, "exit=" + result.getExitCode());
|
||||||
jobId, result.getExitCode(), result.getStatus() + "\n" + result.getLogs());
|
|
||||||
|
|
||||||
if (isEval) {
|
|
||||||
// 오류 정보 등록
|
|
||||||
modelTrainMngCoreService.markStep2Stop(modelId, "exit=" + result.getExitCode());
|
|
||||||
} else {
|
|
||||||
// 오류 정보 등록
|
|
||||||
modelTrainMngCoreService.markStep1Stop(modelId, "exit=" + result.getExitCode());
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
// 실패 처리
|
// 오류 정보 등록
|
||||||
modelTrainJobCoreService.markFailed(
|
modelTrainMngCoreService.markError(modelId, "exit=" + result.getExitCode());
|
||||||
jobId, result.getExitCode(), result.getStatus() + "\n" + result.getLogs());
|
|
||||||
|
|
||||||
if (isEval) {
|
|
||||||
// 오류 정보 등록
|
|
||||||
modelTrainMngCoreService.markStep2Error(modelId, "exit=" + result.getExitCode());
|
|
||||||
} else {
|
|
||||||
// 오류 정보 등록
|
|
||||||
modelTrainMngCoreService.markError(modelId, "exit=" + result.getExitCode());
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -70,11 +70,3 @@ train:
|
|||||||
containerPrefix: kamco-cd-train
|
containerPrefix: kamco-cd-train
|
||||||
shmSize: 16g
|
shmSize: 16g
|
||||||
ipcHost: true
|
ipcHost: true
|
||||||
|
|
||||||
# CORS 설정 (개발 환경)
|
|
||||||
cors:
|
|
||||||
allowed-origins:
|
|
||||||
- https://kamco.training-dev.gs.dabeeo.com
|
|
||||||
- http://localhost:3002
|
|
||||||
- http://192.168.2.109:3002
|
|
||||||
- http://192.168.2.109:7100
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ spring:
|
|||||||
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
|
format_sql: true # ⚠️ 선택 - SQL 포맷팅 (가독성)
|
||||||
|
|
||||||
datasource:
|
datasource:
|
||||||
url: jdbc:postgresql://kamco-cd-train-db:5432/kamco_training_db
|
url: jdbc:postgresql://127.0.01:15432/kamco_training_db
|
||||||
# url: jdbc:postgresql://localhost:15432/kamco_training_db
|
# url: jdbc:postgresql://localhost:15432/kamco_training_db
|
||||||
username: kamco_training_user
|
username: kamco_training_user
|
||||||
password: kamco_training_user_2025_!@#
|
password: kamco_training_user_2025_!@#
|
||||||
@@ -70,8 +70,3 @@ train:
|
|||||||
shmSize: 16g
|
shmSize: 16g
|
||||||
ipcHost: true
|
ipcHost: true
|
||||||
|
|
||||||
# CORS 설정 (운영 환경)
|
|
||||||
cors:
|
|
||||||
allowed-origins:
|
|
||||||
- https://train-kamco.com
|
|
||||||
|
|
||||||
|
|||||||
@@ -54,12 +54,6 @@ logging:
|
|||||||
web: INFO
|
web: INFO
|
||||||
security: INFO
|
security: INFO
|
||||||
root: INFO
|
root: INFO
|
||||||
|
|
||||||
# CORS 설정
|
|
||||||
cors:
|
|
||||||
allowed-origins:
|
|
||||||
- http://localhost:3000
|
|
||||||
- http://localhost:3002
|
|
||||||
# actuator
|
# actuator
|
||||||
management:
|
management:
|
||||||
health:
|
health:
|
||||||
|
|||||||
Reference in New Issue
Block a user