Files
project-gitops/docs/runbooks/dev-bootstrap.md
T
2026-08-28 17:24:26 +09:00

13 KiB

Bootstrap an empty dev-k3s cluster

이 runbook은 폐기 가능한 빈 개발 클러스터만 대상으로 합니다. Production과 기존 live cluster migration에는 사용하지 않습니다.

2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다. 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이 아닙니다.

1. Preflight

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 파일을 준비합니다.

mkdir -p .local/terraform-backend/dev-k3s
cp infrastructure/live/dev-k3s/vault-foundation/backend.s3.hcl.example \
  .local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
cp infrastructure/live/dev-k3s/vault-workloads/backend.s3.hcl.example \
  .local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
cp infrastructure/live/dev-k3s/vault-database/backend.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

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을 따르며 평문 GHCR credential을 Git이나 log에 남기지 않습니다.

3. Dev Vault 초기화

Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 port-forward합니다. 이는 최초 dev bootstrap용이며 routine runner 모델이 아닙니다.

kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
kubectl -n vault port-forward deployment/vault 8200:8200

별도 terminal:

export VAULT_ADDR=http://127.0.0.1:8200
./scripts/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으로 한 번 적용합니다.

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/configdatabase/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에서는 사용하지 않습니다.

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-devauth-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 환경에서 제거합니다.

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를 확인합니다.

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을 실행 시점에만 전달합니다.

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_TOKENsys/policies/acl/auth-server-devupdate capability를 가짐
  • VAULT_DATABASE_TOKENdatabase/config/auth-system-postgres-devupdate capability를 가짐
  • 어느 replacement token도 root policy를 갖지 않음

이후 foundation을 다시 적용할 routine administrator는 없습니다. Encrypted unseal custody의 담당자와 승인된 Vault generated-root recovery 절차를 확인할 수 없으면 root를 폐기하지 않습니다.

./scripts/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-serverautoSync: "true"로 변경합니다.
  2. auth-dev/ghcr-regcred Secret 생성, migration hook 성공과 Deployment health를 확인합니다.
  3. api-serverautoSync: "true"로 변경합니다.
  4. api-dev/ghcr-regcred Secret 생성, north-south와 service-to-service 경로를 확인합니다.

한 PR에서 모든 gate를 동시에 열지 않습니다.

12. 최종 확인

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을 사용합니다.