refactor(gitops): establish platform ownership boundaries

This commit is contained in:
donghyeon-ka
2026-07-26 01:34:29 +09:00
parent 293ee6fc97
commit a6f6c663e0
121 changed files with 2801 additions and 1204 deletions
+71
View File
@@ -0,0 +1,71 @@
# ApplicationSet decommission
이 절차는 `applicationsSync: create-update`를 사용하는 generated
Application을 안전하게 해체하기 위한 runbook입니다. List element를 지우는
것만으로 Application이 삭제되지 않는 것은 오류가 아니라 삭제 보호
동작입니다.
## 중단 조건
다음 중 하나라도 만족하면 진행하지 않습니다.
- 대상 Application 이름, ApplicationSet, 클러스터가 명확하지 않음
- 최신 backup과 복구 테스트가 없음
- PVC/PV와 StorageClass의 reclaim policy를 확인하지 않음
- Application에 예상하지 못한 finalizer가 있음
- live-to-target diff에 대상 밖 리소스가 포함됨
- `argocd-cmd-params-cm`의 전역 ApplicationSet policy가 저장소의
`create-update` 의도를 덮어쓰는지 확인하지 않음
Generated Application template에는 resource finalizer를 두지 않습니다.
`preserveResourcesOnDeletion: true`도 유지합니다. ApplicationSet 전체를
삭제해서 개별 component를 해체하지 않습니다.
## 공통 준비
1. 대상 element의 `autoSync``false`로 바꾸는 PR을 먼저 병합합니다.
2. 비활성 기간에 누적된 live-to-target 전체 diff를 저장합니다.
3. 대상이 stateful이면 application-level backup과 restore test를
완료합니다.
4. List element를 제거하는 별도 PR을 병합합니다. `create-update` 정책
때문에 기존 Application은 의도적으로 남아야 합니다.
5. 남은 Application의 소유 관계와 finalizer를 확인합니다.
```bash
kubectl -n argocd get application <application-name> \
-o json |
jq '{ownerReferences: .metadata.ownerReferences, finalizers: (.metadata.finalizers // [])}'
```
예상하지 못한 finalizer를 강제로 제거하지 않습니다.
## 리소스를 보존하고 관리만 중단
List element를 제거한 뒤 generated Application이 더 이상 재생성되지 않는
것을 확인합니다. Template에 resource finalizer가 없으므로 Application
객체 삭제는 workload를 orphan으로 남깁니다.
```bash
kubectl -n argocd delete application <application-name>
```
삭제 후 workload가 그대로 존재하고 Argo CD에 다시 나타나지 않는지
확인합니다. 보존된 리소스는 더 이상 drift correction을 받지 않으므로,
다른 소유자에게 즉시 인계하거나 별도 정리 계획을 기록합니다.
## 리소스까지 제거
리소스 삭제는 List element 제거와 같은 PR에 섞지 않습니다.
1. 대상 Application은 inventory에 남기고 `autoSync: "false"` 상태를
유지합니다.
2. 별도 PR에서 component의 desired state를 해체용 빈 구성으로 바꿉니다.
3. Argo CD diff에서 삭제 대상이 정확한지 검토합니다.
4. Namespace, PVC 등 `Prune=confirm,Delete=confirm` 대상의 backup과
reclaim policy를 다시 확인하고 승인된 prune을 수동 실행합니다.
5. 리소스가 제거된 뒤 List element 제거 PR을 병합합니다.
6. 남은 Application 객체를 삭제합니다.
승인 시각 annotation을 자동화하거나 우회하지 않습니다. `kubectl delete
applicationset` 및 finalizer 강제 제거는 복구 runbook과 별도 승인 없이는
사용하지 않습니다.
+203 -71
View File
@@ -1,7 +1,11 @@
# Bootstrap an empty dev-k3s cluster
이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. production
사용하지 않습니다.
이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. Production
기존 live cluster migration에는 사용하지 않습니다.
> 2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다.
> 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이
> 아닙니다.
## 1. Preflight
@@ -11,38 +15,59 @@ kubectl cluster-info
make validate
```
의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo CD가
root repository를 읽을 수 있는 read-only credential을 외부 secret
authority에서 먼저 provision해야 합니다. credential은 이 저장소에
의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo
CD가 root repository를 읽을 수 있는 read-only credential을 외부 secret
authority에서 먼저 provision해야 합니다. Credential은 이 저장소에
commit하지 않습니다.
remote state backend 파일을 준비합니다.
remote state backend 파일을 준비합니다.
```bash
mkdir -p .local/terraform-backend/dev-k3s
cp iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-core.s3.hcl
cp iac/terraform/backend/dev-k3s/vault-foundation.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
cp iac/terraform/backend/dev-k3s/vault-workloads.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
cp iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-database.s3.hcl
```
실제 bucket, endpoint와 workload identity를 설정합니다. backend credential은
파일에 넣지 않습니다.
실제 bucket, endpoint와 workload identity를 설정합니다. Backend
credential은 파일에 넣지 않습니다. 세 backend key가 서로 다르고 locking이
활성화됐는지 확인합니다.
## 2. Argo CD와 root Application
```bash
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
kubectl -n argocd get application project-gitops-dev-k3s
kubectl -n argocd get appproject gitops-control-plane
kubectl -n argocd get application project-gitops-control-plane
kubectl -n argocd get applicationsets
```
이 명령이 수행하는 직접 cluster mutation은 Argo CD 설치와 root seed뿐입니다.
Child Application은 root가 생성합니다.
직접 cluster mutation은 pinned Argo CD 설치, 제한된
`gitops-control-plane` AppProject, root Application seed뿐입니다.
Bootstrap script는 이 순서를 지킵니다. Root가 네 AppProject와 네
ApplicationSet을 만들고, ApplicationSet이 child Application을 생성합니다.
초기 inventory에서 Sealed Secrets와 Vault만 `autoSync: "true"`입니다.
Vault Agent Injector, `auth-system`, `auth-server`, `api-server` gate는
닫힌 상태여야 합니다.
### Sealed Secrets key 준비
Checked-in `ghcr-regcred` ciphertext는 암호화에 사용한 controller private
key로만 복호화할 수 있습니다. 기존 key backup이 있으면 workload gate를
열기 전에 복원하고, 없으면 새 controller certificate와 원본 credential
authority를 사용해 두 SealedSecret을 다시 seal한 PR을 merge합니다.
[Sealed Secrets recovery runbook](sealed-secrets-recovery.md)을 따르며 평문
GHCR credential을 Git이나 log에 남기지 않습니다.
## 3. Dev Vault 초기화
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 forward합니다.
port-forward는 최초 dev bootstrap용이며 routine runner 모델이 아닙니다.
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서
port-forward합니다. 이는 최초 dev bootstrap용이며 routine runner 모델이
아닙니다.
```bash
kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
@@ -56,13 +81,14 @@ export VAULT_ADDR=http://127.0.0.1:8200
./hack/vault-init.sh init
```
`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody 복사합니다.
dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과
분리합니다.
`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody 복사합니다.
Dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과
분리합니다. Local working copy는 root revoke 때까지 mode `0600`으로
유지하며 shell history나 CI log에 token을 출력하지 않습니다.
## 4. Vault core
## 4. Vault foundation
초기 root token을 shell history에 직접 적지 않습니다.
Foundation은 초기 root token으로 한 번 적용합니다.
```bash
export TF_VAR_vault_addr="$VAULT_ADDR"
@@ -71,24 +97,89 @@ export TF_VAR_vault_token="$(
)"
make terraform-plan \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
make terraform-apply \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-core
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-foundation
```
plan에 mount, auth backend, policy, role, JWT Transit key 이외 객체가
나오면 apply하지 않습니다.
Plan에 mounts, auth configuration, workloads/database automation
policy와, OIDC/JWT를 명시적으로 구성한 경우에만 분리된 CI JWT role이
있어야 합니다. Workload runtime policy, workload Kubernetes role,
application Transit key, database connection이 보이면 중단합니다.
## 5. Runtime secret seed
CI JWT를 구성할 때 workloads/database exact claim map은 repository와
protected ref를 묶고, 최소 한 공통 job discriminator key에 서로 다른 값을
가져야 합니다. 실제 issuer token payload로 그 claim을 확인하지 못하면
OIDC/JWT 입력을 비워 둡니다.
Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 terminal
session에만 유지합니다.
CI JWT auth를 구성했다면 Foundation이 만든 두 delegated identity에 실제
로그인해 허용/거부 capability를 확인합니다. 구성하지 않았다면 정책
capability를 검사하고 승인된 관리자가 발급한 bootstrap용 short-lived
token을 사용합니다.
- Workloads identity는 승인된 workload policy/role와
`transit/keys/project-auth-jwt`만 변경할 수 있어야 합니다.
- Database identity는 승인된 `database/config``database/roles` 경로만
변경할 수 있어야 합니다.
- 둘 다 mount, auth backend, 임의 policy, token 발급 경로를 변경할 수
없어야 합니다.
장기 token이나 임의 AppRole을 대신 만들지 않습니다. 실제 Gitea
OIDC/JWT가 검증되지 않았다면 승인된 관리자가 발급한 short-lived bootstrap
token을 사용합니다.
## 5. Vault workload access
`TF_VAR_vault_token`을 workloads 전용 short-lived token으로 교체한 뒤
적용합니다. 다음은 OIDC/JWT를 아직 구성하지 않은 빈 dev lab의 bootstrap
예시입니다. Root token의 child revocation에 묶이지 않도록 짧은 TTL orphan
token을 발급하며, routine automation에서는 사용하지 않습니다.
```bash
export VAULT_WORKLOADS_TOKEN="$(
VAULT_TOKEN="$TF_VAR_vault_token" \
vault token create \
-orphan \
-no-default-policy \
-renewable=false \
-explicit-max-ttl=2h \
-policy=vault-workloads-automation-dev \
-ttl=2h \
-format=json |
jq -r '.auth.client_token'
)"
export TF_VAR_vault_token="$VAULT_WORKLOADS_TOKEN"
make terraform-plan \
TF_ROOT=vault-workloads \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
make terraform-apply \
TF_ROOT=vault-workloads \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-workloads
```
Plan에는 workload ACL, Kubernetes auth role와 `project-auth-jwt` Transit
key만 있어야 합니다. `auth-system-dev``auth-dev`의 정확한
ServiceAccount binding과 audience `vault`를 검토합니다. Vault를 사용하지
않는 `api-dev`에는 role이 생성되지 않아야 합니다.
## 6. Runtime secret seed
Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 operator
terminal session에만 유지합니다. 빈 dev lab의 최초 seed는 initial root
token을 잠깐 사용하고 seed 직후 CLI 환경에서 제거합니다.
```bash
export VAULT_TOKEN="$(
jq -r '.root_token' .local/vault/dev-k3s-init.json
)"
read -r -s -p "PostgreSQL superuser password: " POSTGRES_SUPERUSER_PASSWORD
echo
read -r -s -p "Auth database password: " AUTH_DB_PASSWORD
@@ -106,47 +197,72 @@ chmod 0600 "$secret_file"
jq -n --arg password "$POSTGRES_SUPERUSER_PASSWORD" \
'{POSTGRES_SUPERUSER_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/postgres/superuser @"$secret_file"
vault kv put kv/dev/systems/auth-system/postgres/superuser @"$secret_file"
jq -n \
--arg password "$AUTH_DB_PASSWORD" \
'{AUTH_DB_PASSWORD: $password, APP_DATASOURCE_USERNAME: "project_auth", APP_DATASOURCE_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/postgres/auth-server @"$secret_file"
vault kv put kv/dev/systems/auth-system/postgres/auth-server @"$secret_file"
jq -n --arg password "$KEYCLOAK_DB_PASSWORD" \
'{KEYCLOAK_DB_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/postgres/keycloak @"$secret_file"
vault kv put kv/dev/systems/auth-system/postgres/keycloak @"$secret_file"
jq -n --arg password "$KEYCLOAK_ADMIN_PASSWORD" \
'{KC_BOOTSTRAP_ADMIN_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/platform/keycloak/bootstrap-admin @"$secret_file"
vault kv put kv/dev/systems/auth-system/keycloak/bootstrap-admin @"$secret_file"
jq -n --arg secret "$KEYCLOAK_CLIENT_SECRET" \
'{KEYCLOAK_CLIENT_SECRET: $secret, APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET: $secret}' >"$secret_file"
vault kv put kv/dev/platform/keycloak/client-auth-server @"$secret_file"
vault kv put kv/dev/workloads/auth-server/keycloak-client @"$secret_file"
rm -f "$secret_file"
trap - EXIT
unset VAULT_TOKEN
```
PostgreSQL이 Vault Agent 주입 후 시작하는지 확인합니다.
## 7. Vault Agent Injector gate
Vault auth, workload policy/role와 secret metadata를 확인한 뒤 Git PR에서
`vault-agent-injector` element만 `autoSync: "true"`로 바꿉니다. Merge 후
injector Deployment와 webhook health를 확인합니다. 이 단계에서도
`auth-system`과 두 workload gate는 닫혀 있어야 합니다.
## 8. Auth system gate
Secret metadata와 workload policy를 확인한 뒤 Git PR에서 `auth-system`
inventory element만 `autoSync: "true"`로 바꿉니다. PR에는 현재 전체 Argo
diff를 첨부합니다. Child manifest를 직접 apply하거나 Argo UI에서 임의로
Sync하지 않습니다.
Merge 후 PostgreSQL과 Keycloak health를 확인합니다.
```bash
kubectl -n platform rollout status statefulset/postgres --timeout=600s
kubectl -n auth-system-dev rollout status statefulset/postgres --timeout=600s
kubectl -n auth-system-dev rollout status deployment/keycloak --timeout=600s
```
## 6. Vault database state
## 9. Vault database state
초기 root token으로 TTL이 짧은 database 전용 token을 발급합니다.
`TF_VAR_vault_token`을 database 전용 short-lived token으로 교체하고
PostgreSQL credential을 실행 시점에만 전달합니다.
```bash
export TF_VAR_vault_token="$(
vault token create \
-policy=vault-database-automation-dev \
-ttl=30m \
-format=json |
export VAULT_DATABASE_TOKEN="$(
VAULT_TOKEN="$(
jq -r '.root_token' .local/vault/dev-k3s-init.json
)" \
vault token create \
-orphan \
-no-default-policy \
-renewable=false \
-explicit-max-ttl=2h \
-policy=vault-database-automation-dev \
-ttl=2h \
-format=json |
jq -r '.auth.client_token'
)"
export TF_VAR_vault_token="$VAULT_DATABASE_TOKEN"
export TF_VAR_postgres_admin_password="$POSTGRES_SUPERUSER_PASSWORD"
export TF_VAR_postgres_admin_password_version=1
@@ -160,50 +276,66 @@ make terraform-apply \
APPROVE_APPLY=dev-k3s/vault-database
```
## 7. Root token 폐기
Plan에는 `database/config/auth-system-postgres-dev` connection과
`auth-db-migration-dev` dynamic role만 있어야 합니다. Dynamic credential
발급과 revoke를 검증합니다.
Kubernetes auth operator login이 동작하는지 먼저 검증합니다.
## 10. Root token 폐기
```bash
operator_jwt="$(
kubectl -n vault create token vault-operator \
--audience=vault \
--duration=10m
)"
operator_token="$(
VAULT_TOKEN= vault write \
-format=json \
auth/kubernetes/login \
role=vault-operator-dev \
jwt="$operator_jwt" |
jq -r '.auth.client_token'
)"
VAULT_TOKEN="$operator_token" vault token lookup >/dev/null
```
Database state와 delegated login/recovery 절차를 검증한 뒤, application
gate를 열기 전에 initial root token을 폐기합니다.
Script는 두 replacement token을 반드시 요구하고 다음을 자동 검증합니다.
검증 후 initial root token을 폐기합니다.
- `VAULT_WORKLOADS_TOKEN``sys/policies/acl/auth-server-dev``update`
capability를 가짐
- `VAULT_DATABASE_TOKEN`
`database/config/auth-system-postgres-dev``update` capability를 가짐
- 어느 replacement token도 `root` policy를 갖지 않음
이후 foundation을 다시 적용할 routine administrator는 없습니다. Encrypted
unseal custody의 담당자와 승인된 Vault generated-root recovery 절차를
확인할 수 없으면 root를 폐기하지 않습니다.
```bash
./hack/vault-init.sh revoke-root
unset operator_jwt operator_token
VAULT_TOKEN="$VAULT_WORKLOADS_TOKEN" vault token revoke -self
VAULT_TOKEN="$VAULT_DATABASE_TOKEN" vault token revoke -self
unset TF_VAR_vault_token TF_VAR_postgres_admin_password
unset VAULT_WORKLOADS_TOKEN VAULT_DATABASE_TOKEN
unset POSTGRES_SUPERUSER_PASSWORD AUTH_DB_PASSWORD KEYCLOAK_DB_PASSWORD
unset KEYCLOAK_ADMIN_PASSWORD KEYCLOAK_CLIENT_SECRET
```
encrypted custody로 옮긴 init material의 local working copy는 조직의
dev recovery 정책에 따라 제거합니다.
`revoke-root`는 local init JSON에서도 root token field를 제거합니다. 두
bootstrap orphan token도 검증 직후 self-revoke하며 routine credential로
재사용하지 않습니다.
## 8. 확인
## 11. Workload gates
Keycloak realm/client sync와 database migration credential이 준비된 것을
확인한 뒤 단계별 PR을 사용합니다.
1. `auth-server``autoSync: "true"`로 변경합니다.
2. `auth-dev/ghcr-regcred` Secret 생성, migration hook 성공과 Deployment
health를 확인합니다.
3. `api-server``autoSync: "true"`로 변경합니다.
4. `api-dev/ghcr-regcred` Secret 생성, north-south와 service-to-service
경로를 확인합니다.
한 PR에서 모든 gate를 동시에 열지 않습니다.
## 12. 최종 확인
```bash
kubectl -n argocd get applications
kubectl -n vault get pods
kubectl -n platform get pods
kubectl -n auth-system-dev get pods
kubectl -n auth-dev get pods
kubectl -n api-dev get pods
```
모든 Application의 sync/health를 확인하고 DB migration 및 Keycloak client
sync hook 결과를 검토합니다. 실패한 hook을 고치기 위해 child manifest를
직접 apply하지 말고 Git PR을 사용합니다.
Encrypted custody로 옮긴 init material의 local working copy는 조직의 dev
recovery 정책에 따라 제거합니다. 실패를 고치기 위해 live child manifest를
직접 수정하지 말고 Git PR을 사용합니다.
+230 -74
View File
@@ -1,88 +1,244 @@
# Terraform v2 state migration
# Terraform state migration to three Vault states
새 클러스터에는 이 runbook이 필요하지 않습니다. legacy local state 또는
이전 `provider-foundation`, `workload-foundation`, `workload-config`,
`database-config` remote state가 실제로 존재할 때만 수행합니다.
새 클러스터에는 이 runbook이 필요하지 않습니다. Legacy `vault-core` 또는
더 오래된 `provider-foundation`, `workload-foundation`, `workload-config`,
`database-config` state가 실제로 존재할 때만 사용합니다.
State 이동은 live object 삭제보다 위험할 수 있습니다. maintenance window와
독립 backup 없이 진행하지 않습니다.
> 2026-07-26 리팩터링에서는 이 절차를 실제 backend나 cluster에 실행하지
> 않았습니다. Maintenance window, 독립 backup과 승인 없이 시작하지
> 않습니다.
## 목표
State 이동은 live object 삭제보다 위험할 수 있습니다. State split,
Terraform module refactor, Vault KV path/namespace cutover를 한 apply에
섞지 않습니다.
| 이전 state | 목표 |
## 목표 ownership
| Legacy ownership | 목표 state |
|---|---|
| provider Transit Vault state | archive 후 provider Vault 폐기 절차에서 별도 처리 |
| workload foundation | `vault-core`의 기준 state |
| workload config | 소유 객체를 `vault-core`로 이동 |
| database config | `vault-database`로 backend key migration |
| `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 절차 |
동일 클러스터 Transit Vault는 더 이상 desired state가 아닙니다. Terraform
state에서 먼저 삭제하거나 destroy하지 않습니다. snapshot과 seal dependency
해제 확인 후 별도 decommission 승인을 받아 처리합니다.
## 1. Inventory와 backup
모든 operator machine, runner, remote backend에서 state 위치를 확인합니다.
```bash
find . -type f \
\( -name 'terraform.tfstate*' -o -name '*.tfplan' \) \
-not -path './.git/*'
```
각 state를 `terraform state pull`로 encrypted offline custody에 저장하고
checksum을 기록합니다. backup에는 secret data가 포함될 수 있습니다.
## 2. Backend key migration
`workload-foundation` backend에 연결한 상태에서 새 `vault-core` backend
configuration으로 `terraform init -migrate-state`를 수행합니다.
`database-config`도 같은 방식으로 `vault-database` key로 이동합니다.
실제 backend 파일과 이전 key는 환경마다 다르므로 명령에 값을 하드코딩하지
않습니다. migration 전후 `terraform state pull` checksum과 `state list`
비교합니다.
## 3. Workload configuration ownership 이동
이전 `workload-config` state의 다음 객체를 `vault-core` state의 선언된
address로 이동합니다.
- application Vault policies
- workload Kubernetes auth roles
`terraform state mv -state=<source-backup> -state-out=<target-working-copy>`
사용해 offline copy에서 먼저 연습합니다. target address는 현재
`module.workload_policies``module.workload_roles``terraform state list`
결과를 기준으로 합니다. resource 이름을 추측하지 않습니다.
이동 후 두 state 모두 plan합니다.
- `vault-core`: 변경 없음 또는 address-only 이동
- legacy workload-config: 삭제할 live object 없음
두 plan 중 하나라도 destroy를 제안하면 중단하고 backup state를 복원합니다.
## 4. Database state
기존 database state가 없고 live Vault 객체만 존재할 때만 다음 import ID를
사용합니다.
목표 backend key는 각각 달라야 합니다.
```text
vault_database_secret_backend_connection.platform_postgres database/config/platform-postgres-dev
vault_database_secret_backend_role.auth_db_migration database/roles/auth-db-migration-dev
vault_database_secret_backend_role.postgres_operator database/roles/postgres-operator-dev
dev-k3s/vault-foundation.tfstate
dev-k3s/vault-workloads.tfstate
dev-k3s/vault-database.tfstate
```
이미 다른 state에 address가 있으면 import하지 말고 state ownership을 먼저
동일 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 형식은 다음과 같습니다.
```text
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을 둡니다.
```hcl
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 이름은 모두 바뀝니다.
```text
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는
다음과 같습니다.
```text
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을 먼저
이동합니다.
## 5. Cutover 완료 조건
## 6. Retired broad/unused access
- 두 목표 state가 remote backend와 locking을 사용
- 동일 Vault path가 두 state list에 나타나지 않음
- plan에 예상하지 않은 create/delete가 없음
- legacy state와 backup은 immutable archive
- repo와 runner에 local state/provider directory가 없음
다음 legacy object는 새 state의 desired ownership이 아닙니다.
검증이 끝나기 전 legacy backend를 삭제하지 않습니다.
- 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로 이관합니다.
```text
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를
삭제하지 않습니다.