# Flyway 예시 전 예시는 `kubectl apply -f` 가능한 완성 매니페스트다. 1000+ 서비스 규모에서 복사/수정해 쓸 수 있도록 full manifest로 구성했다. --- ## 좋은 예시 1: 완전한 Flyway Job (Helm hook 패턴) ### (1) ConfigMap — migration SQL ```yaml --- apiVersion: v1 kind: ConfigMap metadata: name: auth-flyway-sql namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: db-migration app.kubernetes.io/part-of: auth-platform app.kubernetes.io/managed-by: Helm data: V1__init_auth_schema.sql: | CREATE TABLE IF NOT EXISTS users ( id bigserial PRIMARY KEY, email text NOT NULL, display_name text, created_at timestamptz NOT NULL DEFAULT now() ); CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(lower(email)); V2__add_refresh_tokens.sql: | CREATE TABLE IF NOT EXISTS refresh_tokens ( id bigserial PRIMARY KEY, user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, token_hash bytea NOT NULL, expires_at timestamptz NOT NULL, created_at timestamptz NOT NULL DEFAULT now() ); CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id); V3__add_last_login_column.sql: | ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at timestamptz; V4__create_idx_last_login_concurrently.sql: | -- flyway:executeInTransaction=false -- Long-running DDL. Schedule in low-traffic window. CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_last_login ON users(last_login_at); R__refresh_active_users_view.sql: | CREATE OR REPLACE VIEW active_users AS SELECT id, email, display_name, last_login_at FROM users WHERE last_login_at > now() - interval '30 days'; ``` ### (2) Vault Secrets Operator — DB password ```yaml --- apiVersion: secrets.hashicorp.com/v1beta1 kind: VaultStaticSecret metadata: name: auth-pg-app namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: db-migration spec: type: kv-v2 mount: kv path: auth-prod/postgres/app destination: name: auth-pg-app create: true type: Opaque refreshAfter: 1h vaultAuthRef: vault-auth-auth-prod ``` ### (3) Flyway Job — pre-upgrade / pre-install ```yaml --- apiVersion: v1 kind: ServiceAccount metadata: name: auth-flyway namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: db-migration --- apiVersion: batch/v1 kind: Job metadata: name: auth-flyway-migrate namespace: auth-prod labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod app.kubernetes.io/component: db-migration app.kubernetes.io/part-of: auth-platform app.kubernetes.io/managed-by: Helm app.kubernetes.io/version: "2026.04.16" 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: metadata: labels: app.kubernetes.io/name: auth-server app.kubernetes.io/component: db-migration spec: serviceAccountName: auth-flyway restartPolicy: Never securityContext: runAsNonRoot: true runAsUser: 1000 runAsGroup: 1000 fsGroup: 1000 seccompProfile: {type: RuntimeDefault} initContainers: - name: flyway-info image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d imagePullPolicy: IfNotPresent args: ["info"] env: &flywayEnv - {name: FLYWAY_URL, value: "jdbc:postgresql://auth-pg-rw.data-prod.svc:5432/auth?sslmode=require"} - {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_OUT_OF_ORDER, value: "false"} - {name: FLYWAY_MIXED, value: "false"} - {name: FLYWAY_CLEAN_DISABLED, value: "true"} - name: FLYWAY_PASSWORD valueFrom: {secretKeyRef: {name: auth-pg-app, key: password}} resources: requests: {cpu: "50m", memory: "128Mi"} limits: {cpu: "500m", memory: "512Mi"} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: {drop: ["ALL"]} volumeMounts: - {name: sql, mountPath: /flyway/sql, readOnly: true} - {name: tmp, mountPath: /tmp} - name: flyway-validate image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d imagePullPolicy: IfNotPresent args: ["validate"] env: *flywayEnv resources: requests: {cpu: "50m", memory: "128Mi"} limits: {cpu: "500m", memory: "512Mi"} securityContext: allowPrivilegeEscalation: false readOnlyRootFilesystem: true capabilities: {drop: ["ALL"]} volumeMounts: - {name: sql, mountPath: /flyway/sql, readOnly: true} - {name: tmp, mountPath: /tmp} containers: - name: flyway-migrate image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d imagePullPolicy: IfNotPresent args: ["-X", "migrate"] env: *flywayEnv 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 Deployment보다 **먼저** 실행 (`pre-upgrade,pre-install`, weight `-10`) - `before-hook-creation,hook-succeeded` 삭제 정책으로 과거 Job 정리 - initContainer로 `info` + `validate`를 먼저 실행해 실패를 앞당김 - 메인 container에서 `migrate` (advisory lock 덕분에 같은 Job이 중복 실행돼도 직렬화됨) - `FLYWAY_CLEAN_DISABLED=true` (production 필수) - `FLYWAY_BASELINE_ON_MIGRATE=false`, `FLYWAY_OUT_OF_ORDER=false` - digest pinning, restricted PSA, anchor/alias로 env 중복 제거 - `parallelism: 1`, `backoffLimit: 0`, `activeDeadlineSeconds: 1800` --- ## 좋은 예시 2: 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 app.kubernetes.io/managed-by: argocd 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: # (containers 세부는 예시 1과 동일; 요지만 재현) - 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 annotations: argocd.argoproj.io/sync-wave: "0" labels: app.kubernetes.io/name: auth-server app.kubernetes.io/instance: auth-server-prod 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가 wave `-1` → `0` 순서로 sync - Helm hook과 혼용하지 않음 - `BeforeHookCreation` 정책으로 이전 Job 정리 후 새 Job 실행 --- ## 좋은 예시 3: non-transactional DDL 전용 migration `V4__create_idx_last_login_concurrently.sql`: ```sql -- flyway:executeInTransaction=false -- CREATE INDEX CONCURRENTLY는 Postgres에서 트랜잭션 내 실행 불가. -- Flyway 8.2+ directive로 파일 단위 트랜잭션 비활성화. -- Runtime estimate: 약 15분 (50M rows 기준). -- Deploy window: 주간 트래픽 저점 (예: 화요일 03:00 UTC) CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_last_login ON users(last_login_at); ``` 왜 좋은가: - 파일 단독으로 분리 (다른 statement 없음) - 주석에 runtime / window 명시 - `IF NOT EXISTS`로 재실행 안전성 (CREATE INDEX CONCURRENTLY 실패 시 INVALID 인덱스가 남을 수 있음 — 별도 cleanup 필요) ❌ 나쁜 예시 1: 트랜잭션 내 CREATE INDEX CONCURRENTLY ```sql -- V4__.sql (executeInTransaction directive 없음) CREATE INDEX CONCURRENTLY idx_users_last_login ON users(last_login_at); ``` 문제: - Flyway가 자동으로 트랜잭션을 열어 실행 → `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block` - 해결: `-- flyway:executeInTransaction=false` directive --- ## 좋은 예시 4: history table schema를 명시적으로 분리 env: ```yaml - {name: FLYWAY_CREATE_SCHEMAS, value: "false"} - {name: FLYWAY_INIT_SQL, value: "CREATE SCHEMA IF NOT EXISTS auth_server; CREATE SCHEMA IF NOT EXISTS flyway_history"} - {name: FLYWAY_DEFAULT_SCHEMA, value: "flyway_history"} - {name: FLYWAY_SCHEMAS, value: "flyway_history,auth_server"} - {name: FLYWAY_TABLE, value: "flyway_schema_history"} ``` 왜 좋은가: - history table은 `flyway_history.flyway_schema_history` - migration 대상 schema는 `auth_server` - `createSchemas=false` 조건 하에서 `initSql`로 schema 사전 생성 --- ## 좋은 예시 5: 운영 절차 (Helm + on-demand CNPG backup 연계) ```bash # 1. pending migration 확인 (로컬) docker run --rm -v $PWD/sql:/flyway/sql:ro \ -e FLYWAY_URL=jdbc:postgresql://stage.../auth \ -e FLYWAY_USER=auth_app -e FLYWAY_PASSWORD=... \ flyway/flyway@sha256:... info # 2. PR review + migration 영향 분석 # 3. 운영 배포 직전 on-demand backup cat <