diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd.yml index d831222..1a2f821 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd.yml @@ -18,6 +18,19 @@ jobs: name: Validate deployment configuration runs-on: ubuntu-latest timeout-minutes: 10 + services: + mariadb: + image: mariadb:11.4 + env: + MARIADB_ROOT_PASSWORD: test-root-password + MARIADB_DATABASE: rallytrack + ports: + - 3306:3306 + options: >- + --health-cmd="healthcheck.sh --connect --innodb_initialized" + --health-interval=5s + --health-timeout=5s + --health-retries=12 steps: - uses: actions/checkout@v6 - name: Validate shell syntax @@ -26,6 +39,29 @@ jobs: run: docker compose -f docker-compose.yml config --quiet - name: Validate Pi Compose model run: docker compose -f docker-compose.pi.yml --env-file .env.example config --quiet + - name: Validate analysis-mode migration and rollback + env: + DB_CONTAINER: ${{ job.services.mariadb.id }} + run: | + docker exec "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack -e \ + "CREATE TABLE videos (video_id BIGINT PRIMARY KEY, title VARCHAR(255)); INSERT INTO videos VALUES (1, 'legacy');" + docker exec -i "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack \ + < db/migrations/20260906_add_video_analysis_mode.sql + test "$(docker exec "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack -Nse \ + "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='rallytrack' AND TABLE_NAME='videos' AND COLUMN_NAME='analysis_mode'")" = "1" + test "$(docker exec "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack -Nse \ + "SELECT COUNT(*) FROM videos WHERE video_id=1 AND analysis_mode IS NULL")" = "1" + docker exec "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack -e \ + "INSERT INTO videos VALUES (2, 'pro', 'pro'), (3, 'amateur', 'amateur');" + if docker exec "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack -e \ + "INSERT INTO videos VALUES (4, 'invalid', 'invalid')"; then + echo "analysis_mode CHECK constraint accepted an invalid value" + exit 1 + fi + docker exec -i "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack \ + < db/rollback/20260906_drop_video_analysis_mode.sql + test "$(docker exec "$DB_CONTAINER" mariadb -uroot -ptest-root-password rallytrack -Nse \ + "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='rallytrack' AND TABLE_NAME='videos' AND COLUMN_NAME='analysis_mode'")" = "0" deploy: name: Deploy Pi configuration diff --git a/README.md b/README.md index 91362b8..2fe1b84 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ RallyTrack 서비스의 **배포 구성 저장소**입니다. 애플리케이션 | `ml-server/deploy-ai.sh` | AI 서버 배포 스크립트 | | `ml-server/rallytrack-ai.service` | AI 분석 서버 systemd 유닛 템플릿 | | `docs/CI-CD.md` | CI 선택 근거, GitHub 설정, 배포·롤백, 서버 규모 트레이드오프 | +| `db/` | 운영 DB forward/rollback migration과 적용 절차 | `backend` / `frontend` / `aiAnalysis-server` 를 이 저장소와 같은 상위 폴더에 두고 clone해야 compose의 `build:` 상대 경로와 배포 스크립트가 맞습니다. @@ -66,6 +67,7 @@ GitHub Actions 기반 CI/CD 구조와 최초 runner 설정은 [`docs/CI-CD.md`]( 모든 시크릿과 호스트 주소는 `.env`로 주입하며 저장소에 커밋하지 않습니다. 필요한 키와 설명은 [`.env.example`](.env.example)을 참고하세요. 스키마 최초 생성 시에만 `DDL_AUTO=update`, 이후에는 `validate`로 되돌립니다. +수동 스키마 변경은 [`db/README.md`](db/README.md)의 백업 및 migration 순서를 따릅니다. ## 관련 저장소 diff --git a/db/README.md b/db/README.md new file mode 100644 index 0000000..cb6e45c --- /dev/null +++ b/db/README.md @@ -0,0 +1,45 @@ +# Database migrations + +RallyTrack는 아직 Flyway를 사용하지 않습니다. Pi 배포 스크립트는 이 nullable 컬럼이 없을 때 +`backups/db/`에 `videos` 테이블을 먼저 백업하고 forward migration을 한 번 적용합니다. +운영자가 앱 배포 전에 직접 적용하려면 아래 절차를 사용합니다. +기존 영상의 분석 모드는 근거가 없어 `NULL`로 유지되고, 배포 이후 업로드부터 `pro` 또는 +`amateur`가 저장됩니다. + +## 적용 + +```bash +cd /home/junmin/RallyTrack/devops + +# 먼저 백업 +docker compose -p rallytrack -f docker-compose.pi.yml --env-file .env \ + exec -T db sh -c 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" videos' \ + > videos-before-analysis-mode.sql + +# forward migration +docker compose -p rallytrack -f docker-compose.pi.yml --env-file .env \ + exec -T db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' \ + < db/migrations/20260906_add_video_analysis_mode.sql +``` + +## 확인 + +```bash +docker compose -p rallytrack -f docker-compose.pi.yml --env-file .env \ + exec -T db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" -e \ + "SELECT video_id, title, analysis_mode, video_status, upload_date FROM videos ORDER BY video_id DESC LIMIT 20"' +``` + +`analysis_mode IS NULL`은 migration 이전 영상이라는 의미입니다. `NULL`을 `pro`나 `amateur`로 +일괄 보정하면 실제 선택값처럼 오해되므로 근거 없이 업데이트하지 않습니다. + +## 롤백 + +먼저 이 필드를 사용하지 않는 백엔드 버전으로 롤백한 뒤 아래 SQL을 적용합니다. +앱 롤백 시 새 nullable 컬럼을 남겨둬도 구버전과 호환되므로 자동 롤백은 컬럼을 삭제하지 않습니다. + +```bash +docker compose -p rallytrack -f docker-compose.pi.yml --env-file .env \ + exec -T db sh -c 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' \ + < db/rollback/20260906_drop_video_analysis_mode.sql +``` diff --git a/db/migrations/20260906_add_video_analysis_mode.sql b/db/migrations/20260906_add_video_analysis_mode.sql new file mode 100644 index 0000000..8f9a67a --- /dev/null +++ b/db/migrations/20260906_add_video_analysis_mode.sql @@ -0,0 +1,4 @@ +-- Existing rows remain NULL because their original upload mode cannot be reconstructed. +ALTER TABLE videos + ADD COLUMN IF NOT EXISTS analysis_mode VARCHAR(16) NULL + CHECK (analysis_mode IN ('pro', 'amateur')); diff --git a/db/rollback/20260906_drop_video_analysis_mode.sql b/db/rollback/20260906_drop_video_analysis_mode.sql new file mode 100644 index 0000000..8f27526 --- /dev/null +++ b/db/rollback/20260906_drop_video_analysis_mode.sql @@ -0,0 +1,2 @@ +ALTER TABLE videos + DROP COLUMN IF EXISTS analysis_mode; diff --git a/docs/CI-CD.md b/docs/CI-CD.md index f72f89f..045372e 100644 --- a/docs/CI-CD.md +++ b/docs/CI-CD.md @@ -79,6 +79,9 @@ FastAPI import를 확인한 뒤 systemd를 재시작합니다. 헬스 체크 실 자동 롤백은 Git 코드와 애플리케이션 이미지에만 적용됩니다. MariaDB/MinIO 볼륨은 배포 중 삭제하지 않지만, 호환되지 않는 DB 스키마 변경은 자동 복구하지 않습니다. 운영 중에는 `DDL_AUTO=validate`를 유지하고 스키마 변경은 백업, 호환 가능한 선행 migration, 앱 배포, 구버전 제거 순서로 수행해야 합니다. +`videos.analysis_mode`는 nullable additive migration이라, 컬럼이 없으면 배포 스크립트가 `videos` +테이블을 백업하고 migration을 적용한 뒤 검증합니다. 실패하면 앱 빌드 전에 중단됩니다. +구체적인 forward/rollback 절차는 [`../db/README.md`](../db/README.md)에 있습니다. ## 5. 현재 서버 규모의 트레이드오프 diff --git a/pi/deploy.sh b/pi/deploy.sh index 0cb96ab..76ba05b 100755 --- a/pi/deploy.sh +++ b/pi/deploy.sh @@ -110,6 +110,56 @@ wait_for_http() { fail "$name health check failed: $url" } +wait_for_database() { + local attempts="${1:-24}" + local attempt + + "${COMPOSE[@]}" up -d --no-build db + for ((attempt = 1; attempt <= attempts; attempt++)); do + if "${COMPOSE[@]}" exec -T db sh -c \ + 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" -e "SELECT 1"' \ + >/dev/null 2>&1; then + log "database readiness check passed" + return 0 + fi + sleep 5 + done + fail "database readiness check failed" +} + +ensure_analysis_mode_schema() { + local column_count + column_count="$("${COMPOSE[@]}" exec -T db sh -c \ + 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" -Nse \ + "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '\''videos'\'' AND COLUMN_NAME = '\''analysis_mode'\''"')" + + if [[ "$column_count" == "0" ]]; then + local migration="$DEVOPS_DIR/db/migrations/20260906_add_video_analysis_mode.sql" + local backup_dir="${RALLYTRACK_DB_BACKUP_DIR:-$DEPLOY_ROOT/backups/db}" + local backup_file="$backup_dir/videos-$(date -u +%Y%m%dT%H%M%SZ)-before-analysis-mode.sql" + [[ -f "$migration" ]] || fail "analysis-mode migration file is missing" + mkdir -p "$backup_dir" + chmod 700 "$backup_dir" + + log "backing up videos table before the additive migration" + "${COMPOSE[@]}" exec -T db sh -c \ + 'mariadb-dump -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" videos' \ + > "$backup_file" + chmod 600 "$backup_file" + + log "adding nullable videos.analysis_mode column" + "${COMPOSE[@]}" exec -T db sh -c \ + 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE"' \ + < "$migration" + column_count="$("${COMPOSE[@]}" exec -T db sh -c \ + 'mariadb -uroot -p"$MARIADB_ROOT_PASSWORD" "$MARIADB_DATABASE" -Nse \ + "SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = '\''videos'\'' AND COLUMN_NAME = '\''analysis_mode'\''"')" + fi + + [[ "$column_count" == "1" ]] || fail \ + "videos.analysis_mode migration did not complete" +} + rollback() { local exit_code="$1" trap - ERR @@ -160,6 +210,11 @@ ROLLBACK_REQUIRED=true COMPOSE=(docker compose -p rallytrack -f "$DEVOPS_DIR/docker-compose.pi.yml" --env-file "$ENV_FILE") "${COMPOSE[@]}" config --quiet +if [[ "$COMPONENT" != "frontend" ]]; then + wait_for_database + ensure_analysis_mode_schema +fi + case "$COMPONENT" in backend) capture_image backend