Files
project-gitops/docs/runbooks/terraform-state-migration.md
T

10 KiB

Terraform state migration to three Vault states

새 클러스터에는 이 runbook이 필요하지 않습니다. Legacy vault-core 또는 더 오래된 provider-foundation, workload-foundation, workload-config, database-config state가 실제로 존재할 때만 사용합니다.

2026-07-26 리팩터링에서는 이 절차를 실제 backend나 cluster에 실행하지 않았습니다. Maintenance window, 독립 backup과 승인 없이 시작하지 않습니다.

State 이동은 live object 삭제보다 위험할 수 있습니다. State split, Terraform module refactor, Vault KV path/namespace cutover를 한 apply에 섞지 않습니다.

목표 ownership

Legacy ownership 목표 state
vault-core의 mounts/auth/delegation 객체 vault-foundation
vault-core 또는 workload-config의 workload policy/role와 Transit key vault-workloads
vault-database 또는 database-config의 auth-system connection과 migration role vault-database
별도 same-cluster Transit provider Vault State archive 후 별도 decommission 절차

목표 backend key는 각각 달라야 합니다.

dev-k3s/vault-foundation.tfstate
dev-k3s/vault-workloads.tfstate
dev-k3s/vault-database.tfstate

동일 Vault API path가 두 state에 동시에 남아 있으면 cutover가 끝난 것이 아닙니다. State끼리 terraform_remote_state를 추가하지 않습니다.

1. Freeze, inventory, backup

모든 Terraform apply와 관련 image/config promotion을 중단합니다. ApplicationSet의 injector, auth-system, auth-server, api-server autoSync gate도 닫습니다. 이 gate는 Argo의 자동 sync만 멈추며 이미 실행 중인 Pod의 재시작, node drain 또는 controller 동작을 막지 않습니다. Migration 중 legacy path를 병행 유지하고, 완전한 quiesce가 필요하면 workload별 scale/maintenance 절차를 별도로 승인합니다.

각 legacy backend에서 다음을 확보합니다.

  • terraform state pull 원본
  • state checksum
  • terraform state list
  • 이동할 각 address의 terraform state show
  • 현재 Vault object ID/path와 provider version

Backup에는 credential과 secret data가 포함될 수 있으므로 encrypted offline custody에 보관합니다. Backend lock이 작동하는지 확인하고 source state를 수정할 runner를 하나로 제한합니다.

2. Migration code 준비

먼저 실제로 배포된 legacy Git revision에서 임시 migration branch를 만듭니다. 그 revision의 policy 문서, namespace binding, role payload를 그대로 유지한 vault-workloads root와 import block만 추가합니다. 현재 branch의 새 KV path와 auth-system-dev binding을 이 단계에 복사하면 ownership 이동과 live policy 변경이 섞이므로 사용할 수 없습니다.

Source와 destination은 같은 legacy object payload를 선언해야 합니다. 실제 import ID는 backup의 state show로 확정하며 이름을 추측하지 않습니다. Ownership split이 양쪽 no-op으로 끝난 뒤에만 현재 desired revision을 별도 path/namespace cutover PR로 적용합니다.

일반적인 import ID 형식은 다음과 같습니다.

vault_policy                         <policy-name>
vault_kubernetes_auth_backend_role  auth/kubernetes/role/<role-name>
vault_transit_secret_backend_key    transit/keys/project-auth-jwt

Legacy core/foundation source에는 이동 대상별 removed block을 둡니다.

removed {
  from = module.workload_policies

  lifecycle {
    destroy = false
  }
}

removed {
  from = module.workload_roles

  lifecycle {
    destroy = false
  }
}

removed {
  from = vault_transit_secret_backend_key.project_auth_jwt

  lifecycle {
    destroy = false
  }
}

실제 address가 다르면 현재 state list를 사용합니다. 위 예를 그대로 복사하지 않습니다.

Module 구조 개선은 아직 하지 않습니다. 우선 기존 address/선언으로 ownership만 옮기고 후속 PR에서 moved block을 사용합니다.

3. 양쪽 plan 검토

Source와 destination을 같은 revision에서 plan합니다.

Plan 허용 결과
Source core/foundation 대상 object를 state에서만 제거, live destroy 0
Destination workloads 기존 live object import, create/change/destroy 0

둘 중 하나라도 live create, update, delete를 제안하면 중단합니다. Policy 내용이나 Vault path rename은 이 단계에 포함하지 않습니다.

4. Workload ownership split

승인된 maintenance window에서 다음 순서로 진행합니다.

  1. Source의 removed { destroy = false } apply
  2. 즉시 destination import apply
  3. 양쪽 state list에서 object가 정확히 한 번만 나타나는지 확인
  4. 양쪽 plan이 no-op인지 확인

Manual remote state push나 offline state mv -state/-state-out을 primary 절차로 사용하지 않습니다. Source 제거와 destination import 사이에 문제가 생기면 다른 apply를 진행하지 말고 backup과 승인된 rollback 절차를 사용합니다.

5. Foundation backend key 전환

Workload object를 분리한 뒤 남은 legacy vault-core state 전체를 vault-foundation backend key로 terraform init -migrate-state 합니다. Migration 전후의 state list와 serial/lineage, checksum을 기록합니다.

vault-database의 state 경계는 유지하지만 connection address와 live object 이름은 모두 바뀝니다.

vault_database_secret_backend_connection.platform_postgres
  -> vault_database_secret_backend_connection.auth_system_postgres

database/config/platform-postgres-dev
  -> database/config/auth-system-postgres-dev

Checked-in moved block은 Terraform address만 이관합니다. Vault connection 이름 변경은 replacement이며 create_before_destroy도 생성/삭제 사이에 operator 승인 대기 시간을 만들지 않습니다. 기존 cluster에서는 final configuration을 바로 apply하지 말고 별도 blue/green cutover를 준비합니다.

  1. 임시 migration revision에서 old connection을 유지하고 new connection을 별도 resource로 추가합니다.
  2. Database runner policy가 maintenance window 동안 old/new 두 exact database/config path를 모두 허용하게 합니다.
  3. New connection 검증 뒤 auth-db-migration-dev role을 new connection으로 전환하고 credential issue/revoke를 시험합니다.
  4. Old connection에 연결된 lease를 inventory하고 만료 또는 명시적 revoke를 확인합니다.
  5. 후속 승인에서 old connection과 임시 policy path를 제거합니다.

기존 이름이 database-config이거나 backend key가 다를 때는 이 cutover와 분리해 vault-database backend key로 migration합니다.

Database object를 새로 import해야 하는 경우의 target address와 ID는 다음과 같습니다.

vault_database_secret_backend_connection.auth_system_postgres
  database/config/auth-system-postgres-dev
vault_database_secret_backend_role.auth_db_migration
  database/roles/auth-db-migration-dev

이미 다른 state에 address가 있으면 import하지 말고 ownership을 먼저 이동합니다.

6. Retired broad/unused access

다음 legacy object는 새 state의 desired ownership이 아닙니다.

  • Broad platform-admin-dev policy와 이를 사용한 vault-operator-dev role
  • Legacy 단일 project-gitops-dev CI JWT role
  • 미사용 keycloak-operator-dev, postgres-operator-dev policies
  • 미사용 database/roles/postgres-operator-dev dynamic role

State split 중 자동 destroy하지 않습니다. 우선 removed { destroy = false } 로 legacy source ownership에서 분리하고, Vault audit/consumer inventory로 사용자가 없음을 확인합니다. Token/lease revoke와 live object 삭제는 별도 보안 decommission 승인으로 수행합니다.

기존 jwt-ci auth mount가 state에 있으면 ownership 이동 동안 실제 issuer/discovery 입력을 유지합니다. 입력을 누락해 count = 0이 되어도 prevent_destroy가 mount 삭제를 차단해야 합니다. Mount와 그 하위 role을 제거할 때만 token/accessor와 consumer를 확인한 별도 decommission revision에서 명시적으로 보호를 해제합니다.

7. Vault path와 namespace cutover

State split이 no-op인 것을 확인한 뒤 별도 PR/maintenance window에서 다음 legacy path를 새 owner path로 이관합니다.

kv/dev/platform/postgres/*  -> kv/dev/systems/auth-system/postgres/*
kv/dev/platform/keycloak/bootstrap-admin
                             -> kv/dev/systems/auth-system/keycloak/bootstrap-admin
kv/dev/platform/keycloak/client-auth-server
                             -> kv/dev/workloads/auth-server/keycloak-client

KV payload는 Terraform으로 이동하지 않습니다. 승인된 operator가 값을 노출하지 않는 secret procedure로 새 path에 기록하고 metadata/version을 확인합니다. Policy, Kubernetes role, Agent annotation, namespace/DNS 변경을 render와 Vault capability test로 검증합니다.

platform에서 auth-system-dev로의 live namespace 이동은 Kubernetes state/data migration입니다. Terraform state split과 별도로 backup, non-cascading ownership transfer, rollback 계획을 가져야 합니다.

새 consumer가 정상 동작하고 rollback 기간이 끝날 때까지 legacy KV value와 policy를 삭제하지 않습니다. 삭제는 별도 승인 작업입니다.

8. 완료 조건

  • vault-foundation, vault-workloads, vault-database가 서로 다른 remote backend와 lock을 사용
  • 각 Vault API path가 정확히 한 state list에만 존재
  • 세 plan에 예상하지 않은 create/change/delete가 없음
  • Delegated state가 자신의 automation policy/login role을 소유하지 않음
  • Workload/database identity의 허용·거부 capability test 통과
  • Legacy state와 backup이 immutable archive에 있음
  • Repository와 runner working directory에 local state, plan, provider directory가 없음
  • New KV path와 auth-system-dev cutover 전에는 관련 autoSync gate가 닫힘

검증이 끝나기 전 legacy backend, Vault path, namespace 또는 PVC를 삭제하지 않습니다.