refactor(gitops): establish platform ownership boundaries
This commit is contained in:
@@ -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
@@ -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을 사용합니다.
|
||||
|
||||
@@ -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를
|
||||
삭제하지 않습니다.
|
||||
|
||||
Reference in New Issue
Block a user