업그레이드
GenD 플랫폼의 버전 업그레이드 절차와 주의 사항입니다.
개요
- 제품 버전은 SemVer(
vX.Y.Z) 단일 버전으로 릴리스됩니다 (ADR-0036, v1.0.0+). 설치본의 현재 버전은 UI 하단 StatusBar 또는GET /version으로 확인합니다. - 릴리스마다 GitHub Release와 정확 태그 이미지
4종(
gend-api/gend-ui/gend-pipelines/gend-relay, 태그vX.Y.Z)이 발행됩니다 (v1.1.0 릴리스부터). 변경 사항은 릴리스 노트를 참조하세요. - 업그레이드 = ① DB 스키마 마이그레이션 → ② 컨테이너 이미지 교체 → ③ 검증 순서입니다.
업그레이드 전 체크리스트
- 현재 버전 확인:
curl -fsS https://<도메인>/version - 대상 버전의 릴리스 노트 검토 (스키마 변경·비호환 항목)
- 데이터베이스 백업 완료 (
pg_dump또는velero backup create— 백업/복원) - 롤백 계획 수립 (직전 버전 이미지 태그 기록)
표준 업그레이드 절차 (온프렘, v1.1.0+)
1. DB 스키마 마이그레이션 (alembic upgrade Job)
alembic 이력이 정상인 설치본(신규 설치 이후 본 절차로만 업그레이드한 설치본)은 동봉된 Job 매니페스트로 마이그레이션을 실행합니다:
# 0) 적용 대상 파일의 리소스 종류를 눈으로 확인 (아래 danger 박스 참고)
grep -n '^kind:' infra/onprem/alembic-upgrade-job.yaml
# 1) infra/onprem/alembic-upgrade-job.yaml 의 image :placeholder 를
# 대상 릴리스 태그로 치환 (예: <registry>/gend-api:v1.1.0)
# 2) 실행
kubectl apply -f infra/onprem/alembic-upgrade-job.yaml
kubectl logs -n gend job/gend-alembic-upgrade -f # "Running upgrade ..." 확인
kubectl wait -n gend --for=condition=complete job/gend-alembic-upgrade --timeout=300s
# 3) 완료 후 삭제 (Job spec 은 immutable — 재실행 시 삭제 후 재 apply)
kubectl delete job -n gend gend-alembic-upgrade
:::danger apply 대상 파일의 kind: 를 매번 눈으로 확인하세요 — 파일명을 믿지 마세요
이미지 :placeholder 치환을 잊으면 즉시 ImagePullBackOff 로 드러나지만,
kind: Secret 문서가 다른 리소스와 한 파일에 섞여 있는 경우는 apply 가 조용히
성공합니다 — 실패로 드러나지 않는 만큼 더 위험합니다. 2026-08-06 prod 장애가
이 패턴이었습니다: infra/mlflow/configmap.yaml 에 kind: ConfigMap 과 kind: Secret 이 함께 있었고, 파일명만 보고 "ConfigMap 만 바뀐다"고 가정한 채
런북대로 apply 했다가 라이브 DB 비밀번호가 레포의 dev placeholder 값으로
덮어써져 CrashLoop 이 났습니다(상세 사고 경위·복구 절차는
MLflow artifact 프록시 운영
문서 참조). 이 alembic Job 매니페스트 자체는 kind: Job 하나만 담고 있지만
(위 0단계로 확인), 온프렘/AKS 를 막론하고 infra/ 하위 어떤 매니페스트를
apply 하기 전에도 같은 습관을 들이세요 — 파일명(configmap.yaml,
deployment.yaml 등)은 그 파일이 담은 kind: 를 보증하지 않습니다.
scripts/test_manifest_secret_isolation.py (CI, #2991) 가 kind: Secret
문서가 다른 kind 와 같은 파일에 커밋되는 것은 막지만, 로컬 워킹카피의
임시 수정이나 오래된 브랜치/커밋을 체크아웃해 적용하는 경우까지는 지켜주지
않습니다 — 운영자의 apply 직전 grep -n '^kind:' <파일> 확인이 마지막 방어선입니다.
:::
TLS 가 강제된 PostgreSQL 은 Job 의 GEND_DB_URL 에 ?sslmode=verify-full 등
사이트 정책 파라미터를 덧붙이고 CA 볼륨을 마운트하세요 (helm deployment.yaml
의 postgresql-tls-ca 패턴 참조).
:::info AKS prod 이력 정합화 완료 — Job 절차를 그대로 씁니다 (v1.2+)
기존 prod(aks-genos-prod)는 init_db(auto_create_tables) + 수동 ALTER 로
관리되어 alembic_version 이 코드 head 와 불일치(동결) 상태였고, 그 상태에서는
alembic upgrade 가 이미 존재하는 객체와 충돌해 이 Job 의 실행이 금지되어
있었습니다.
2026-08-05 정합화를 마쳤습니다 — stamp 로 이력을 재정렬한 뒤 정합 리비전
r2869conform → s2869jsonb 를 적용해 구조 드리프트를 133 → 13 으로 줄였습니다.
alembic_version 이 코드 head 와 일치합니다.
Job 절차는 prod 에서 실제로 검증했습니다 — 같은 날 위 Job 매니페스트로
s2869jsonb 를 적용했고(TLS verify-full + PGOPTIONS 포함) 성공했습니다.
단, 아래 NetworkPolicy 전제를 먼저 확인하세요.
:::
:::tip 잔여 드리프트 0 — 예외 목록이 필요 없습니다
2026-08-07 prod 적용 완료. compare_metadata 결과가 0건입니다
(alembic 1.19.0, prod 파드에서 측정 · env.py 의 include_object 적용).
alembic head : ('y2989video',)
드리프트 : 0
:::caution "0건" 은 도구 버전과 함께 적어야 합니다
같은 DB 를 alembic 1.18.x 로 재면 CHECK 제약을 비교하지 않아 다른 답이 나옵니다.
실제로 2026-08-06 에 "0건" 으로 보고했던 것이 1.19.0 재측정에서 4건으로 드러났고
(uv.lock 도입 #2941 로 prod 이미지가 1.19.0 을 받으면서), 그 4건이 아래 u2869ck 로
해소된 것입니다. 재측정은 prod 가 실제로 쓰는 바이너리로 하세요.
또한 필터 없이 compare_metadata 를 부르면 6건이 나옵니다 — 전부 intel_patents
(수동 DDL 고아, 테이블 1 + 인덱스 5)입니다. env.py 의 include_object 를 직접
넘겨야 정본 수치(0)가 나옵니다.
2026-08-07 이전에는 이 수치가 26건이었습니다 — _p4b_bak_* 백필 스냅샷 16개가
함께 잡혔기 때문입니다. 그 테이블들은 아래처럼 처분했습니다.
:::info _p4b_bak_* 백필 스냅샷 — 2026-08-07 덤프 보존 후 DROP
#2798 P4b 가 workspace_id 를 백필하기 직전 상태를 테이블째 복사해 둔 것입니다
(백업 쪽 workspace_id 는 전부 NULL). 백필이 끝나고 NOT NULL 까지 시행돼 롤백
창이 닫혔으므로 정리했습니다.
그냥 지우면 안 됐습니다 — 대부분은 원본에 그대로 있지만 18행은 백업에만 있었습니다(백필 이후 원본에서 삭제된 행):
| 테이블 | 백업 | 원본에 남음 | 유일본 |
|---|---|---|---|
document_chunk_records | 43 | 28 | 15 |
pipeline_templates | 1 | 0 | 1 |
ingested_files | 8 | 7 | 1 |
pii_column_registry | 1 | 0 | 1 |
그래서 16개 테이블 전체(674행)를 pg_dump -Fc 로 덤프하고, 격리 DB 에
pg_restore --exit-on-error 로 실제 복원까지 검증한 뒤 DROP 했습니다
(--list 목차 확인만으로는 복원 가능성을 보증하지 않습니다).
이후 env.py 의 _p4b_bak_ 접두어 필터는 죽은 코드라 제거했습니다 — 남겨두면
같은 이름의 테이블이 다시 생겨도 드리프트에서 조용히 빠집니다.
:::
:::
여기까지 온 경로: 정합화 직후 17 → s2869jsonb(모델 jsonb·팩 slug 유니크) 13 →
t2869idx(인덱스 소유권 이관) 0(1.18.4 기준) → 1.19.0 재측정 4 →
u2869ck(CHECK 제약 이름 rename) 0.
2026-08-07 적용분 (한 번의 alembic upgrade head, t2869idx → y2989video):
| 리비전 | 내용 | prod 영향 |
|---|---|---|
u2869ck | CHECK 제약 이름 → ck_* (RENAME 만) | 드리프트 4 → 0 |
v2869fk | ingestion_run_items.workspace_id FK (RESTRICT) | FK 1개 추가 (대상 0행) |
q2989quota · x2989chunk | 데이터셋 쿼터·청크 세션 테이블 (#2989) | no-op — init_db 가 이미 생성, 리비전이 가드 |
w2869mrg | 2-head 병합 | DDL 없음 |
y2989video | training_datasets modality CHECK 에 video 추가 | 제약 교체 (멱등) |
스키마 델타는 정확히 3건이었습니다 — 제약 rename 2 + FK 1. 테이블 구성 변화 0, 행 수 변화 0.
마지막 단계가 핵심이었습니다 — init_db 의 raw SQL 과 모델이 같은 인덱스를
각자 만들고 있어서, 모델 메타데이터에 없는 11종을 autogenerate 가 영구히
"삭제 대상" 으로 보고했습니다. 지우면 다음 파드 부팅에 init_db 가 다시 만들어
무한 왕복이 됩니다. 부분/표현식 인덱스를 술어까지 그대로 모델로 옮겨
소유자를 하나로 만들었습니다.
이제 autogenerate 가 무언가를 보고하면 그건 진짜 드리프트입니다.
scripts/../apps/api/tests/test_index_ownership_single_source.py 가 raw SQL 로
인덱스를 되살리는 것을 차단합니다.
kubectl -n gend exec deploy/gend-api -- python -m alembic check
:::
:::tip prod 마이그레이션 전 2가지
1) 논리 백업 — 목록 읽기가 아니라 실제 복원으로 검증
DUMP="gend-prod-$(date -u +%Y%m%dT%H%M%SZ)-pre.dump"
pg_dump -Fc -Z6 -h "$PGHOST" -p "$PGPORT" -U gend -d gend -f "$DUMP" # 58MB 기준 ~9초
sha256sum "$DUMP" | tee "$DUMP.sha256"
pg_restore --list "$DUMP" >/dev/null # ① 아카이브가 읽히는가 (최소 조건)
createdb -h "$PGHOST" -U gend gend_restore_check
pg_restore --exit-on-error -h "$PGHOST" -U gend -d gend_restore_check "$DUMP" # ② 실제 복원
dropdb -h "$PGHOST" -U gend gend_restore_check
pg_restore --list 는 아카이브 목차만 읽습니다 — TABLE DATA 건수를 세도
복원 가능성을 보증하지 않습니다. 격리 DB 에 --exit-on-error 로 실제 복원해야
검증입니다.
2) 락 대기로 트래픽을 막지 않도록 fail-fast
export PGOPTIONS="-c lock_timeout=8000 -c statement_timeout=180000" # 로컬 실행 시
Job 으로 돌릴 때 셸의 export 는 컨테이너에 닿지 않습니다 —
infra/onprem/alembic-upgrade-job.yaml 의 컨테이너 env 에 PGOPTIONS 가
들어 있습니다(수정 시 함께 유지할 것).
적용 직후 alembic current 와 헬스(/health 의 postgresql: connected)를
확인하고, 파드 로그에 IntegrityError/NotNullViolation 이 없는지 봅니다.
:::
:::danger Job 을 쓰려면 NetworkPolicy allowlist 등재가 선행돼야 합니다
restrict-postgresql 이 PostgreSQL:5432 접근을 파드 라벨 app 로 제한합니다.
Job 의 파드 라벨(app: gend-alembic-upgrade)이 목록에 없으면 연결 타임아웃으로
죽습니다 — 2026-08-05 prod 실증에서 정확히 그렇게 실패했고, 등재 후 성공했습니다.
kubectl -n gend get networkpolicy restrict-postgresql \
-o jsonpath='{.spec.ingress[0].from[0].podSelector.matchExpressions[0].values}'
정본은 infra/azure-deploy/k8s-azure/05-network-policy.yaml 이고,
scripts/test_pg_client_allowlist.py 가 매니페스트와 allowlist 를 교차 대조합니다
(같은 유형의 실패가 #2646·#2683·#2869 세 번 재발했습니다).
★ NetworkPolicy 는 파드를 선택합니다 — CronJob/Deployment 객체의 라벨은 소용없습니다. 파드 템플릿에 붙여야 합니다.
:::
:::danger prod 는 PG TLS 를 클라이언트가 강제해야 합니다
prod PostgreSQL 의 pg_hba 는 hostssl 이 아니라 host 로 열려 있어
평문 접속도 수락합니다. 클라이언트가 강제하지 않으면 조용히 평문으로 붙습니다.
Job 매니페스트는 gend-api 와 동일하게 ?ssl=verify-full +
GEND_PG_SSL_CA_PATH=/etc/postgresql-tls/ca.crt + postgresql-tls 시크릿 마운트를
기본 활성으로 갖습니다. PG TLS 를 쓰지 않는 온프렘 사이트만 이 셋을 함께
제거하세요.
:::
2. 컨테이너 이미지 교체
REG=<registry> # 예: genosprodacr.azurecr.io 또는 사이트 미러 레지스트리
V=v1.1.0
kubectl -n gend set image deploy/gend-api gend-api=${REG}/gend-api:${V}
kubectl -n gend set image deploy/gend-ui ui=${REG}/gend-ui:${V}
kubectl -n gend rollout status deploy/gend-api --timeout=300s
kubectl -n gend rollout status deploy/gend-ui --timeout=300s
# Dagster 를 운영하는 경우
kubectl -n gend set image deploy/dagster-daemon dagster-daemon=${REG}/gend-pipelines:${V}
kubectl -n gend set image deploy/dagster-webserver dagster-webserver=${REG}/gend-pipelines:${V}
:::note UI 이미지는 도메인이 빌드타임에 새겨집니다
릴리스 gend-ui 이미지는 gend.genon.ai 기준으로 빌드됩니다. 도메인이 다른
온프렘 사이트는 부트스트랩 스크립트(infra/azure-deploy/scripts/01-push-images.sh,
DOMAIN 환경변수)로 사이트 도메인에 맞게 재빌드하세요.
:::
3. 검증
curl -fsS https://<도메인>/version # version 이 대상 버전, build_sha 가 릴리스 커밋인지
curl -fsS https://<도메인>/health # status: healthy
UI 하단 StatusBar 의 버전 표기도 함께 확인합니다.
Kind 로컬 업그레이드
# 이미지 빌드 — ★레포 루트에서. gend-api 는 docs-site/docs/ 를 COPY 하므로
# apps/api 컨텍스트로 빌드하면 실패합니다.
TAG="$(cat VERSION)-dev.$(date +%Y%m%d)-$(git rev-parse --short HEAD)"
docker build -t gend/gend-api:${TAG} -f apps/api/Dockerfile .
kind load docker-image gend/gend-api:${TAG} --name gend-local
kubectl -n gend set image deploy/gend-api gend-api=gend/gend-api:${TAG}
kubectl -n gend rollout status deploy/gend-api
반드시 고유 태그를 사용하세요.
latest태그는 Kind에서 이미지 캐시 문제를 유발합니다.
Trino 업그레이드
# values.yaml에서 image.tag 변경
helm upgrade trino trino/trino -n gend -f infra/helm/trino/values.yaml
# Worker graceful shutdown 적용
kubectl apply -f infra/trino/worker-graceful-shutdown.yaml
Keycloak 업그레이드
Keycloak은 PostgreSQL DB 스키마를 자동 마이그레이션합니다.
# 이미지 태그 변경 후 재배포
kubectl apply -f infra/keycloak/deployment.yaml
롤백
# Deployment 롤백 (또는 직전 버전 태그로 set image 재실행)
kubectl rollout undo deployment/gend-api -n gend
# Helm 롤백
helm rollback trino -n gend
# ArgoCD 롤백
argocd app rollback gend-api
DB 마이그레이션이 포함된 업그레이드의 롤백은 이미지 롤백만으로 충분하지 않을 수 있습니다 — 업그레이드 전 백업으로 복원하는 경로를 기본으로 하세요.
버전 호환성
업그레이드 시 다음 호환성을 확인하세요:
| 구성 요소 | 호환 주의 |
|---|---|
| Trino | 카탈로그 커넥터 설정 변경 확인 |
| Keycloak | Realm 내보내기/가져오기 호환성 |
| Vault | API 버전 호환성 |
| PostgreSQL | pg_dump/pg_restore 버전 매칭 |