342 lines
13 KiB
Markdown
342 lines
13 KiB
Markdown
# Bootstrap an empty dev-k3s cluster
|
|
|
|
이 runbook은 폐기 가능한 빈 개발 클러스터만 대상으로 합니다. Production과
|
|
기존 live cluster migration에는 사용하지 않습니다.
|
|
|
|
> 2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다.
|
|
> 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이
|
|
> 아닙니다.
|
|
|
|
## 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-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은 파일에 넣지 않습니다. 세 backend key가 서로 다르고 locking이
|
|
활성화됐는지 확인합니다.
|
|
|
|
## 2. Argo CD와 root Application
|
|
|
|
```bash
|
|
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
|
|
kubectl -n argocd get appproject gitops-control-plane
|
|
kubectl -n argocd get application project-gitops-control-plane
|
|
kubectl -n argocd get applicationsets
|
|
```
|
|
|
|
직접 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에서
|
|
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과
|
|
분리합니다. Local working copy는 root revoke 때까지 mode `0600`으로
|
|
유지하며 shell history나 CI log에 token을 출력하지 않습니다.
|
|
|
|
## 4. Vault foundation
|
|
|
|
Foundation은 초기 root token으로 한 번 적용합니다.
|
|
|
|
```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-foundation \
|
|
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
|
|
|
|
make terraform-apply \
|
|
TF_ROOT=vault-foundation \
|
|
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl \
|
|
APPROVE_APPLY=dev-k3s/vault-foundation
|
|
```
|
|
|
|
Plan에는 mounts, auth configuration, workloads/database automation
|
|
policy와, OIDC/JWT를 명시적으로 구성한 경우에만 분리된 CI JWT role이
|
|
있어야 합니다. Workload runtime policy, workload Kubernetes role,
|
|
application Transit key, database connection이 보이면 중단합니다.
|
|
|
|
CI JWT를 구성할 때 workloads/database exact claim map은 repository와
|
|
protected ref를 묶고, 최소 한 공통 job discriminator key에 서로 다른 값을
|
|
가져야 합니다. 실제 issuer token payload로 그 claim을 확인하지 못하면
|
|
OIDC/JWT 입력을 비워 둡니다.
|
|
|
|
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
|
|
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/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/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/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/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/workloads/auth-server/keycloak-client @"$secret_file"
|
|
|
|
rm -f "$secret_file"
|
|
trap - EXIT
|
|
unset VAULT_TOKEN
|
|
```
|
|
|
|
## 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 auth-system-dev rollout status statefulset/postgres --timeout=600s
|
|
kubectl -n auth-system-dev rollout status deployment/keycloak --timeout=600s
|
|
```
|
|
|
|
## 9. Vault database state
|
|
|
|
`TF_VAR_vault_token`을 database 전용 short-lived token으로 교체하고
|
|
PostgreSQL credential을 실행 시점에만 전달합니다.
|
|
|
|
```bash
|
|
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
|
|
|
|
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
|
|
```
|
|
|
|
Plan에는 `database/config/auth-system-postgres-dev` connection과
|
|
`auth-db-migration-dev` dynamic role만 있어야 합니다. Dynamic credential
|
|
발급과 revoke를 검증합니다.
|
|
|
|
## 10. Root token 폐기
|
|
|
|
Database state와 delegated login/recovery 절차를 검증한 뒤, application
|
|
gate를 열기 전에 initial root token을 폐기합니다.
|
|
Script는 두 replacement 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
|
|
|
|
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
|
|
```
|
|
|
|
`revoke-root`는 local init JSON에서도 root token field를 제거합니다. 두
|
|
bootstrap orphan token도 검증 직후 self-revoke하며 routine credential로
|
|
재사용하지 않습니다.
|
|
|
|
## 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 auth-system-dev get pods
|
|
kubectl -n auth-dev get pods
|
|
kubectl -n api-dev get pods
|
|
```
|
|
|
|
Encrypted custody로 옮긴 init material의 local working copy는 조직의 dev
|
|
recovery 정책에 따라 제거합니다. 실패를 고치기 위해 live child manifest를
|
|
직접 수정하지 말고 Git PR을 사용합니다.
|