17 KiB
17 KiB
Flyway 예시
전 예시는 kubectl apply -f 가능한 완성 매니페스트다. 1000+ 서비스 규모에서 복사/수정해 쓸 수 있도록 full manifest로 구성했다.
좋은 예시 1: 완전한 Flyway Job (Helm hook 패턴)
(1) ConfigMap — migration SQL
---
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
---
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
---
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 패턴
---
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:
-- 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
-- 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=falsedirective
좋은 예시 4: history table schema를 명시적으로 분리
env:
- {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 연계)
# 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 <<EOF | kubectl apply -f -
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: auth-pg-pre-2026-04-16
namespace: data-prod
spec:
cluster: {name: auth-pg}
method: barmanObjectStore
EOF
# 4. Helm upgrade (pre-upgrade hook이 Flyway Job 실행)
helm upgrade auth-server ./charts/auth-server \
--namespace auth-prod \
--values values/prod.yaml \
--atomic --timeout 20m
# 5. Flyway Job 로그 확인
kubectl -n auth-prod logs job/auth-flyway-migrate --all-containers
# 6. Deployment rollout 확인
kubectl -n auth-prod rollout status deploy/auth-server --timeout=10m
# 7. flyway info 재실행 (post-check)
왜 좋은가:
- migration 직전 on-demand backup으로 PITR 지점 확보
helm upgrade --atomic으로 실패 시 자동 롤백- hook이
hook-succeeded정책으로 정리됨
좋은 예시 6: expand → migrate → contract (여러 릴리즈)
Release 1 (V120~V121) — Expand
-- V120__add_email_canonical_nullable.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
-- Flyway 기본 트랜잭션 모드 — 소규모 테이블용. 대용량은 별도 배치 Job.
UPDATE users
SET email_canonical = lower(trim(email))
WHERE email_canonical IS NULL
AND email IS NOT NULL;
앱: 쓰기 시 두 컬럼 채움. 읽기는 아직 email.
Release 2 (V122) — Migrate
-- V122__add_email_canonical_constraints.sql
ALTER TABLE users ALTER COLUMN email_canonical SET NOT NULL;
ALTER TABLE users ADD CONSTRAINT users_email_canonical_unique UNIQUE (email_canonical);
앱: 읽기/쓰기 모두 email_canonical. 기존 email도 fallback 유지.
Release 3 (V130) — Contract
-- V130__drop_legacy_email_column.sql
ALTER TABLE users DROP COLUMN email;
왜 좋은가:
- N-1 ↔ N 동시 배포 허용
- 각 릴리즈가 독립 롤백 가능 (V130 제외 모두 non-destructive)
- expand와 contract가 같은 릴리즈에 섞이지 않음
좋은 예시 7: repeatable migration은 정의성 오브젝트에만
sql/
V120__add_email_canonical_nullable.sql
V121__backfill_email_canonical.sql
V122__add_email_canonical_constraints.sql
V130__drop_legacy_email_column.sql
R__refresh_active_users_view.sql
R__user_signup_function.sql
왜 좋은가:
- 핵심 schema change는 versioned
- view / function만 repeatable — 체크섬 변경 시 재적용
❌ 나쁜 예시 2: 순서 중요한 schema change를 R__로
R__create_users_table.sql ← 잘못. 순서 보장 없음
R__add_refresh_tokens.sql
문제:
- repeatable은 ordering 보장 없음 — 의존성 있는 change에 부적합
나쁜 예시 3: app startup에 migration 숨김
# application.properties
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.out-of-order=true
문제:
- replicas=3이면 Pod 3개가 동시 migrate 시도 (advisory lock이 직렬화는 하지만 기동 latency 증가)
- app rollout 실패와 migration 실패가 섞임
- 신규 Pod 기동마다 validate 수행 → 오차 탐지 시점이 흐려짐
baseline-on-migrate=true+out-of-order=true조합은 migration history 신뢰도 저하
나쁜 예시 4: validate 실패 후 바로 repair
flyway validate || flyway repair
flyway migrate
문제:
- history 문제를 원인 분석 없이 덮음
- repair를 정상 운영 흐름처럼 사용 — 탐지력 저하
나쁜 예시 5: 적용된 migration 파일 수정
V42__add_refresh_token_column.sql
# 처음엔 빈 migration
# prod apply 후 컬럼 타입을 나중에 editor로 수정
문제:
- checksum mismatch → validate 실패
- 환경 간 재현성 깨짐
- 대응은 "새 V__ migration으로 교정"
나쁜 예시 6: U__ undo migration 작성
V120__add_column.sql
U120__drop_column.sql ← OSS Flyway는 실행 불가
문제:
flyway undo는 Teams/Enterprise 전용- OSS 환경에서는 U__ 파일이 실행되지 않아 오해 유발
- rollback은 forward-only + PITR로
나쁜 예시 7: parallelism 누락 + 재시도 무한
spec:
# parallelism, backoffLimit, activeDeadlineSeconds 모두 누락
template:
spec:
restartPolicy: OnFailure # 무한 재시도 유발
문제:
backoffLimit기본 6 +restartPolicy: OnFailure→ 실패 시 지수 backoff로 계속 재시도activeDeadlineSeconds없음 → hang된 migration이 영원히 살아있음- advisory lock이 걸린 실패 Job이 새 Job을 블록
나쁜 예시 8: Helm hook + Argo CD hook 혼용
annotations:
"helm.sh/hook": "pre-upgrade"
"helm.sh/hook-weight": "-10"
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
문제:
- Argo CD가 Helm chart를 렌더링할 때 Helm hook annotation을 일반 리소스의 annotation으로 해석
- 결과적으로 Flyway Job이 일반 리소스로 취급되거나, 두 시스템이 서로 다른 시점에 Job을 만들어 race 발생
- 하나의 배포 도구에 맞춰 한쪽만 사용할 것