Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions .github/workflows/ci-cd.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:` 상대 경로와 배포 스크립트가 맞습니다.
Expand Down Expand Up @@ -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 순서를 따릅니다.

## 관련 저장소

Expand Down
45 changes: 45 additions & 0 deletions db/README.md
Original file line number Diff line number Diff line change
@@ -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
```
4 changes: 4 additions & 0 deletions db/migrations/20260906_add_video_analysis_mode.sql
Original file line number Diff line number Diff line change
@@ -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'));
2 changes: 2 additions & 0 deletions db/rollback/20260906_drop_video_analysis_mode.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
ALTER TABLE videos
DROP COLUMN IF EXISTS analysis_mode;
3 changes: 3 additions & 0 deletions docs/CI-CD.md
Original file line number Diff line number Diff line change
Expand Up @@ -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. 현재 서버 규모의 트레이드오프

Expand Down
55 changes: 55 additions & 0 deletions pi/deploy.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down