# db / migration 예시 모든 YAML은 `kubectl apply` 가능하다. 상세 Flyway Job 예시는 `docs/examples/infra/flyway.md` 참조. --- ## 좋은 예시 1: auth-server와 keycloak DB 경계 분리 (CNPG 2 cluster) ```yaml --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: auth-pg namespace: data-prod labels: app.kubernetes.io/name: auth-pg app.kubernetes.io/part-of: auth-platform backup.platform.io/tier: gold spec: instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8 bootstrap: initdb: database: auth owner: auth_app secret: {name: auth-pg-app} storage: {size: 50Gi, storageClass: fast-ssd-retain} walStorage: {size: 20Gi, storageClass: fast-ssd-retain} monitoring: {enablePodMonitor: true} backup: retentionPolicy: "30d" barmanObjectStore: destinationPath: s3://acme-prod-pg-backups/auth-pg s3Credentials: accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID} secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY} wal: {compression: gzip, maxParallel: 8} data: {compression: gzip, immediateCheckpoint: true, jobs: 4} --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: keycloak-pg namespace: data-prod labels: app.kubernetes.io/name: keycloak-pg app.kubernetes.io/part-of: identity-platform backup.platform.io/tier: gold spec: instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8 bootstrap: initdb: database: keycloak owner: keycloak secret: {name: keycloak-pg-app} storage: {size: 30Gi, storageClass: fast-ssd-retain} walStorage: {size: 10Gi, storageClass: fast-ssd-retain} monitoring: {enablePodMonitor: true} backup: retentionPolicy: "30d" barmanObjectStore: destinationPath: s3://acme-prod-pg-backups/keycloak-pg s3Credentials: accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID} secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY} wal: {compression: gzip, maxParallel: 8} data: {compression: gzip, jobs: 4} ``` 왜 좋은가: - auth와 keycloak이 별도 CNPG cluster → 장애 / 업그레이드 영향 분리 - 각각 schema ownership이 분리되어 migration 파이프라인도 분리 가능 - 백업 destination path도 분리 → retention / 암호화 정책 독립 ❌ 나쁜 예시 1: 하나의 cluster의 하나의 database에 두 서비스 schema ```yaml # single CNPG cluster, database=shared # auth-server uses schema "auth" # keycloak uses schema "keycloak" # one Flyway project manages both ``` 문제: - 서비스별 업그레이드 / restore 영향 격리 불가 - Flyway history가 서로 섞임 - 한 서비스가 lock을 오래 잡으면 다른 서비스가 멈춤 --- ## 좋은 예시 2: migration을 Helm hook으로 app보다 먼저 실행 ```yaml --- apiVersion: batch/v1 kind: Job metadata: name: auth-flyway-migrate namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: db-migration app.kubernetes.io/managed-by: Helm annotations: "helm.sh/hook": "pre-upgrade,pre-install" "helm.sh/hook-weight": "-10" "helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded" spec: parallelism: 1 completions: 1 backoffLimit: 0 activeDeadlineSeconds: 1800 ttlSecondsAfterFinished: 86400 template: spec: restartPolicy: Never securityContext: runAsNonRoot: true runAsUser: 1000 fsGroup: 1000 seccompProfile: {type: RuntimeDefault} containers: - name: flyway image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d args: ["-X", "migrate"] env: - {name: FLYWAY_URL, value: "jdbc:postgresql://auth-pg-rw.data-prod.svc:5432/auth"} - {name: FLYWAY_USER, value: "auth_app"} - {name: FLYWAY_LOCATIONS, value: "filesystem:/flyway/sql"} - {name: FLYWAY_SCHEMAS, value: "auth_server"} - {name: FLYWAY_DEFAULT_SCHEMA, value: "auth_server"} - {name: FLYWAY_TABLE, value: "flyway_schema_history"} - {name: FLYWAY_VALIDATE_ON_MIGRATE, value: "true"} - {name: FLYWAY_BASELINE_ON_MIGRATE, value: "false"} - {name: FLYWAY_CLEAN_DISABLED, value: "true"} - name: FLYWAY_PASSWORD valueFrom: {secretKeyRef: {name: auth-pg-app, key: password}} resources: requests: {cpu: "100m", memory: "256Mi"} limits: {cpu: "1", memory: "1Gi"} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: {drop: ["ALL"]} volumeMounts: - {name: sql, mountPath: /flyway/sql, readOnly: true} - {name: tmp, mountPath: /tmp} volumes: - name: sql configMap: {name: auth-flyway-sql} - name: tmp emptyDir: {} ``` 왜 좋은가: - Helm hook으로 app install/upgrade보다 **먼저** 실행 (`-10` weight) - `before-hook-creation,hook-succeeded` 삭제 정책으로 이전 Job 깨끗이 정리 - `cleanDisabled=true` 명시 (실수로 `flyway clean` 방지) - `parallelism: 1`, `backoffLimit: 0`, `activeDeadlineSeconds: 1800` - digest pinning, restricted PSA --- ## 좋은 예시 3: Argo CD sync wave로 순서 지정 ```yaml --- apiVersion: batch/v1 kind: Job metadata: name: auth-flyway-migrate namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: db-migration annotations: argocd.argoproj.io/sync-wave: "-1" argocd.argoproj.io/hook: Sync argocd.argoproj.io/hook-delete-policy: BeforeHookCreation spec: parallelism: 1 completions: 1 backoffLimit: 0 activeDeadlineSeconds: 1800 ttlSecondsAfterFinished: 86400 template: spec: restartPolicy: Never securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: type: RuntimeDefault containers: - name: flyway image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d args: ["-X", "migrate"] resources: requests: { cpu: 100m, memory: 256Mi } limits: { memory: 1Gi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] --- apiVersion: apps/v1 kind: Deployment metadata: name: auth-server namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod annotations: argocd.argoproj.io/sync-wave: "0" spec: replicas: 3 selector: matchLabels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod template: metadata: labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod spec: securityContext: runAsNonRoot: true runAsUser: 10001 runAsGroup: 10001 fsGroup: 10001 seccompProfile: type: RuntimeDefault containers: - name: auth-server image: registry.example.com/identity/auth-server:1.24.0 resources: requests: { cpu: 500m, memory: 1Gi } limits: { memory: 1536Mi } securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: drop: ["ALL"] ``` 왜 좋은가: - Argo CD는 sync-wave가 낮은 것부터 실행 - Helm hook과 혼용하지 않음 (한쪽만 사용) ❌ 나쁜 예시 2: Helm hook + Argo CD hook 혼용 ```yaml annotations: "helm.sh/hook": "pre-upgrade" "argocd.argoproj.io/sync-wave": "-1" "argocd.argoproj.io/hook": Sync ``` 문제: - Argo CD가 Helm chart를 렌더링할 때 Helm hook을 일반 리소스로 취급해 sync 순서가 꼬임 - 실행이 중복되거나 누락됨 - 한 방식으로 통일할 것 --- ## 좋은 예시 4: Expand → Migrate → Contract 3단계 릴리즈 ### 배경 `users` 테이블의 `email` 컬럼 (NULL 허용)을 NOT NULL + 정규화된 `email_canonical` 컬럼으로 바꾸고 싶다. ### Release 1 — Expand `V120__add_email_canonical_nullable.sql`: ```sql -- flyway:executeInTransaction=false ALTER TABLE users ADD COLUMN email_canonical text; CREATE INDEX CONCURRENTLY idx_users_email_canonical ON users(email_canonical); ``` `V121__backfill_email_canonical.sql` (같은 릴리즈 또는 별도 배치 Job): ```sql UPDATE users SET email_canonical = lower(trim(email)) WHERE email_canonical IS NULL AND email IS NOT NULL; ``` 앱은 쓰기: `email` + `email_canonical` 둘 다 채움. 읽기: 여전히 `email`. ### Release 2 — Migrate 앱 읽기 경로를 `email_canonical`로 전환. 새 가입/수정은 `email_canonical`만 보장. `V122__add_email_canonical_not_null.sql`: ```sql -- 이 시점에는 모든 row에 email_canonical이 채워져 있어야 함 ALTER TABLE users ALTER COLUMN email_canonical SET NOT NULL; ALTER TABLE users ADD CONSTRAINT users_email_canonical_unique UNIQUE (email_canonical); ``` ### Release 3 — Contract 앱이 `email` 컬럼을 더 이상 읽지/쓰지 않는 버전으로 완전히 롤아웃된 뒤. `V130__drop_legacy_email_column.sql`: ```sql ALTER TABLE users DROP COLUMN email; ``` 왜 좋은가: - 각 릴리즈가 N-1 ↔ N 동시 운영 가능 - `CREATE INDEX CONCURRENTLY`는 `-- flyway:executeInTransaction=false`로 분리 - Contract는 backfill + 앱 전환이 모두 끝난 뒤 별도 릴리즈 ❌ 나쁜 예시 3: 한 릴리즈에 expand + contract ```sql -- V100__rename_email.sql ALTER TABLE users RENAME COLUMN email TO email_old; ALTER TABLE users ADD COLUMN email text NOT NULL DEFAULT ''; -- 앱이 어느 버전이든 장애 발생 가능 ``` 문제: - rolling deploy 중간에 앱이 N-1 / N 모두 실행 → 컬럼 없음 / 이름 다름으로 에러 - rollback 시 DB 상태가 앞서가 있어 N-1 앱이 기동 안 됨 --- ## 좋은 예시 5: PITR 복구 계획 (CNPG) ```yaml --- apiVersion: postgresql.cnpg.io/v1 kind: Cluster metadata: name: auth-pg-restore namespace: data-prod spec: instances: 3 imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8 storage: {size: 50Gi, storageClass: fast-ssd-retain} walStorage: {size: 20Gi, storageClass: fast-ssd-retain} bootstrap: recovery: source: auth-pg-source recoveryTarget: targetTime: "2026-04-16 09:45:00+00" # 잘못된 migration 직전 externalClusters: - name: auth-pg-source barmanObjectStore: destinationPath: s3://acme-prod-pg-backups/auth-pg s3Credentials: accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID} secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY} wal: {maxParallel: 8} ``` 왜 좋은가: - 운영 cluster는 건드리지 않고 `auth-pg-restore`로 복원 - `recoveryTarget.targetTime`을 분단위로 지정 - 복원 후 검증 → 운영 전환은 별도 runbook --- ## 좋은 예시 6: non-transactional DDL을 별도 migration 파일로 `V200__create_idx_users_last_login.sql`: ```sql -- flyway:executeInTransaction=false -- Long-running DDL. Run in low-traffic window. -- Runtime estimate: ~15min on 50M rows. CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_last_login ON users(last_login_at); ``` 왜 좋은가: - `CREATE INDEX CONCURRENTLY`는 Postgres에서 트랜잭션 내 실행 불가 - Flyway 8.2+ `executeInTransaction=false` directive로 파일 단위 제어 - 주석에 runtime 추정치 / 영향 명시 ❌ 나쁜 예시 4: 트랜잭션 내 CREATE INDEX CONCURRENTLY ```sql -- V200__.sql (기본 트랜잭션 모드) CREATE INDEX CONCURRENTLY idx_users_last_login ON users(last_login_at); -- → ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block ``` 문제: - Flyway가 자동으로 트랜잭션을 열기 때문에 실패 - `-- flyway:executeInTransaction=false`가 필수 --- ## 좋은 예시 7: 운영 절차 runbook snippet ```text # auth-server DB schema change — 2026-04-16 02:00 UTC maintenance window ## Pre-check (T-1d) 1. Pending migration 검토: 로컬 `flyway info` 2. PR review + migration 영향 분석 문서 작성 (expand/migrate/contract 단계) 3. Backup 상태 확인: kubectl -n data-prod get scheduledbackup auth-pg-daily kubectl -n data-prod get backup -l cnpg.io/cluster=auth-pg --sort-by=.metadata.creationTimestamp ## T-5min 1. On-demand backup: cat < /backup/dump.sql ``` 문제: - PITR 불가, RPO = 24h - replication slot / extension / large object 누락 - 대규모 DB에서 restore 시간 폭증 - 같은 cluster 안 PVC에 저장하면 동시 소실 --- ## 나쁜 예시 7: U__ undo migration 작성 ``` flyway/ V120__add_column.sql U120__drop_column.sql ← OSS Flyway는 실행 불가 ``` 문제: - Flyway Community(OSS)는 undo 미지원 → `flyway undo`가 에러 - rollback 전략은 forward-only migration + PITR로 대체