refactor: reorganize GitOps control plane
This commit is contained in:
@@ -0,0 +1,209 @@
|
||||
# Bootstrap an empty dev-k3s cluster
|
||||
|
||||
이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. production에
|
||||
사용하지 않습니다.
|
||||
|
||||
## 1. Preflight
|
||||
|
||||
```bash
|
||||
kubectl config current-context
|
||||
kubectl cluster-info
|
||||
make validate
|
||||
```
|
||||
|
||||
의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo CD가
|
||||
root repository를 읽을 수 있는 read-only credential을 외부 secret
|
||||
authority에서 먼저 provision해야 합니다. credential은 이 저장소에
|
||||
commit하지 않습니다.
|
||||
|
||||
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-database.s3.hcl.example \
|
||||
.local/terraform-backend/dev-k3s/vault-database.s3.hcl
|
||||
```
|
||||
|
||||
실제 bucket, endpoint와 workload identity를 설정합니다. backend credential은
|
||||
파일에 넣지 않습니다.
|
||||
|
||||
## 2. Argo CD와 root Application
|
||||
|
||||
```bash
|
||||
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
|
||||
kubectl -n argocd get application project-gitops-dev-k3s
|
||||
```
|
||||
|
||||
이 명령이 수행하는 직접 cluster mutation은 Argo CD 설치와 root seed뿐입니다.
|
||||
Child Application은 root가 생성합니다.
|
||||
|
||||
## 3. Dev Vault 초기화
|
||||
|
||||
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 forward합니다.
|
||||
이 port-forward는 최초 dev bootstrap용이며 routine runner 모델이 아닙니다.
|
||||
|
||||
```bash
|
||||
kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
|
||||
kubectl -n vault port-forward deployment/vault 8200:8200
|
||||
```
|
||||
|
||||
별도 terminal:
|
||||
|
||||
```bash
|
||||
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과
|
||||
분리합니다.
|
||||
|
||||
## 4. Vault core
|
||||
|
||||
초기 root token을 shell history에 직접 적지 않습니다.
|
||||
|
||||
```bash
|
||||
export TF_VAR_vault_addr="$VAULT_ADDR"
|
||||
export TF_VAR_vault_token="$(
|
||||
jq -r '.root_token' .local/vault/dev-k3s-init.json
|
||||
)"
|
||||
|
||||
make terraform-plan \
|
||||
TF_ROOT=vault-core \
|
||||
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.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
|
||||
```
|
||||
|
||||
plan에서 mount, auth backend, policy, role, JWT Transit key 이외 객체가
|
||||
나오면 apply하지 않습니다.
|
||||
|
||||
## 5. Runtime secret seed
|
||||
|
||||
Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 terminal
|
||||
session에만 유지합니다.
|
||||
|
||||
```bash
|
||||
read -r -s -p "PostgreSQL superuser password: " POSTGRES_SUPERUSER_PASSWORD
|
||||
echo
|
||||
read -r -s -p "Auth database password: " AUTH_DB_PASSWORD
|
||||
echo
|
||||
read -r -s -p "Keycloak database password: " KEYCLOAK_DB_PASSWORD
|
||||
echo
|
||||
read -r -s -p "Keycloak bootstrap admin password: " KEYCLOAK_ADMIN_PASSWORD
|
||||
echo
|
||||
read -r -s -p "Auth-server Keycloak client secret: " KEYCLOAK_CLIENT_SECRET
|
||||
echo
|
||||
|
||||
secret_file="$(mktemp)"
|
||||
trap 'rm -f "$secret_file"' EXIT
|
||||
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"
|
||||
|
||||
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"
|
||||
|
||||
jq -n --arg password "$KEYCLOAK_DB_PASSWORD" \
|
||||
'{KEYCLOAK_DB_PASSWORD: $password}' >"$secret_file"
|
||||
vault kv put kv/dev/platform/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"
|
||||
|
||||
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"
|
||||
|
||||
rm -f "$secret_file"
|
||||
trap - EXIT
|
||||
```
|
||||
|
||||
PostgreSQL이 Vault Agent 주입 후 시작하는지 확인합니다.
|
||||
|
||||
```bash
|
||||
kubectl -n platform rollout status statefulset/postgres --timeout=600s
|
||||
```
|
||||
|
||||
## 6. Vault database state
|
||||
|
||||
초기 root token으로 TTL이 짧은 database 전용 token을 발급합니다.
|
||||
|
||||
```bash
|
||||
export TF_VAR_vault_token="$(
|
||||
vault token create \
|
||||
-policy=vault-database-automation-dev \
|
||||
-ttl=30m \
|
||||
-format=json |
|
||||
jq -r '.auth.client_token'
|
||||
)"
|
||||
export TF_VAR_postgres_admin_password="$POSTGRES_SUPERUSER_PASSWORD"
|
||||
export TF_VAR_postgres_admin_password_version=1
|
||||
|
||||
make terraform-plan \
|
||||
TF_ROOT=vault-database \
|
||||
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl
|
||||
|
||||
make terraform-apply \
|
||||
TF_ROOT=vault-database \
|
||||
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl \
|
||||
APPROVE_APPLY=dev-k3s/vault-database
|
||||
```
|
||||
|
||||
## 7. Root token 폐기
|
||||
|
||||
Kubernetes auth operator login이 동작하는지 먼저 검증합니다.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
검증 후 initial root token을 폐기합니다.
|
||||
|
||||
```bash
|
||||
./hack/vault-init.sh revoke-root
|
||||
unset operator_jwt operator_token
|
||||
unset TF_VAR_vault_token TF_VAR_postgres_admin_password
|
||||
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 정책에 따라 제거합니다.
|
||||
|
||||
## 8. 확인
|
||||
|
||||
```bash
|
||||
kubectl -n argocd get applications
|
||||
kubectl -n vault get pods
|
||||
kubectl -n platform 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을 사용합니다.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Sealed Secrets backup and recovery
|
||||
|
||||
Existing SealedSecrets are decryptable only with a controller private key.
|
||||
Back up every key after first install and after rotation.
|
||||
|
||||
## Backup
|
||||
|
||||
Use an encrypted operator workstation:
|
||||
|
||||
```bash
|
||||
umask 077
|
||||
kubectl -n kube-system get secret \
|
||||
-l sealedsecrets.bitnami.com/sealed-secrets-key \
|
||||
-o json > .local/sealed-secrets-keys.json
|
||||
chmod 0600 .local/sealed-secrets-keys.json
|
||||
```
|
||||
|
||||
Encrypt the file with the organization's recovery mechanism, store at least
|
||||
two independently controlled copies, and delete the plaintext working copy.
|
||||
Record cluster, date and checksum without recording private key data.
|
||||
|
||||
## Restore
|
||||
|
||||
Restore keys before application SealedSecrets sync:
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system apply -f .local/sealed-secrets-keys.json
|
||||
kubectl -n kube-system rollout restart deployment/sealed-secrets-controller
|
||||
kubectl -n kube-system rollout status deployment/sealed-secrets-controller
|
||||
```
|
||||
|
||||
Verify both `auth-dev/ghcr-regcred` and `api-dev/ghcr-regcred` are created.
|
||||
|
||||
If no key backup exists, old ciphertext cannot be recovered. Create a new
|
||||
controller key and reseal every Secret from the original credential source.
|
||||
|
||||
## Rotation
|
||||
|
||||
The chart requests periodic key renewal. Old keys must remain until all
|
||||
ciphertext has been resealed and verified. GHCR credential rotation requires:
|
||||
|
||||
1. issue an organization-owned read-only package credential;
|
||||
2. create namespace-scoped SealedSecrets with the current controller cert;
|
||||
3. merge and verify image pulls;
|
||||
4. revoke the previous credential;
|
||||
5. update the encrypted key backup.
|
||||
@@ -0,0 +1,88 @@
|
||||
# Terraform v2 state migration
|
||||
|
||||
새 클러스터에는 이 runbook이 필요하지 않습니다. legacy local state 또는
|
||||
이전 `provider-foundation`, `workload-foundation`, `workload-config`,
|
||||
`database-config` remote state가 실제로 존재할 때만 수행합니다.
|
||||
|
||||
State 이동은 live object 삭제보다 위험할 수 있습니다. maintenance window와
|
||||
독립 backup 없이 진행하지 않습니다.
|
||||
|
||||
## 목표
|
||||
|
||||
| 이전 state | 목표 |
|
||||
|---|---|
|
||||
| provider Transit Vault state | archive 후 provider Vault 폐기 절차에서 별도 처리 |
|
||||
| workload foundation | `vault-core`의 기준 state |
|
||||
| workload config | 소유 객체를 `vault-core`로 이동 |
|
||||
| database config | `vault-database`로 backend key migration |
|
||||
|
||||
동일 클러스터 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를
|
||||
사용합니다.
|
||||
|
||||
```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
|
||||
```
|
||||
|
||||
이미 다른 state에 address가 있으면 import하지 말고 state ownership을 먼저
|
||||
이동합니다.
|
||||
|
||||
## 5. Cutover 완료 조건
|
||||
|
||||
- 두 목표 state가 remote backend와 locking을 사용
|
||||
- 동일 Vault path가 두 state list에 나타나지 않음
|
||||
- plan에 예상하지 않은 create/delete가 없음
|
||||
- legacy state와 backup은 immutable archive
|
||||
- repo와 runner에 local state/provider directory가 없음
|
||||
|
||||
검증이 끝나기 전 legacy backend를 삭제하지 않습니다.
|
||||
@@ -0,0 +1,44 @@
|
||||
# Vault backup and recovery
|
||||
|
||||
`dev-k3s` Vault는 single-node integrated Raft입니다. upgrade, Terraform
|
||||
state migration, storage 작업 전에 snapshot을 생성합니다.
|
||||
|
||||
## Snapshot
|
||||
|
||||
short-lived authorized token과 Vault API 연결을 준비합니다.
|
||||
|
||||
```bash
|
||||
umask 077
|
||||
vault operator raft snapshot save .local/vault/dev-k3s.snap
|
||||
sha256sum .local/vault/dev-k3s.snap
|
||||
chmod 0600 .local/vault/dev-k3s.snap
|
||||
```
|
||||
|
||||
즉시 암호화해 init/unseal material과 다른 위치에 보관합니다. snapshot에는
|
||||
secret payload가 포함됩니다.
|
||||
|
||||
## Recovery material
|
||||
|
||||
- Raft snapshot
|
||||
- unseal key
|
||||
- 현재 manifest revision
|
||||
- Terraform remote state backup
|
||||
- Sealed Secrets controller key
|
||||
|
||||
Initial root token은 recovery material이 아닙니다. bootstrap 완료 후
|
||||
폐기해야 합니다.
|
||||
|
||||
## Restore exercise
|
||||
|
||||
분기마다 isolated disposable cluster에서 다음을 검증합니다.
|
||||
|
||||
1. 동일 Vault version과 storage 설정 배포
|
||||
2. Vault init 후 snapshot restore
|
||||
3. 복구 key로 unseal
|
||||
4. Kubernetes auth 재연결 확인
|
||||
5. KV read, JWT signing, dynamic database lease issue/revoke 확인
|
||||
6. recovery time과 누락된 의존성 기록
|
||||
|
||||
현재 dev 구성은 production recovery 설계가 아닙니다. production은
|
||||
independent HA Vault, TLS, auto-unseal과 더 엄격한 backup custody가
|
||||
필요합니다.
|
||||
Reference in New Issue
Block a user