refactor: reorganize GitOps control plane

This commit is contained in:
donghyeon-ka
2026-07-25 23:55:31 +09:00
parent d507ac6ee9
commit 293ee6fc97
191 changed files with 7046 additions and 9034 deletions
+12
View File
@@ -0,0 +1,12 @@
# ADR 0001: Internal Gitea is canonical
Status: accepted
The internal repository
`https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops` is the only
writable deployment source.
Argo CD and Gitea Actions use this URL. A GitHub copy may exist only as a
read-only mirror with monitored replication; it must never be an independent
deployment branch. GHCR remains an image registry and does not require GitHub
to host the GitOps source.
+24
View File
@@ -0,0 +1,24 @@
# ADR 0002: Terraform state ownership
Status: accepted
Terraform은 VM/네트워크뿐 아니라 provider가 제공되는 Vault API 객체도
관리할 수 있다. 현재 저장소의 Terraform 범위는 Vault API이고 실제
machine provisioning은 provider가 확정될 때 별도 root로 추가한다.
`dev-k3s`는 두 state만 사용한다.
- `vault-core`: mounts, auth backends, policies, Kubernetes/JWT roles,
application Transit key
- `vault-database`: PostgreSQL connection과 dynamic roles
resource/API path 하나는 한 state에만 속한다. state는 암호화, versioning,
access control, locking이 가능한 remote backend에 저장한다.
`vault-core`는 privilege-escalation 가능한 객체를 포함하므로 제한된
관리자 실행만 허용한다. `vault-database`는 core가 생성한
`vault-database-automation-dev` 정책의 short-lived identity로 실행한다.
Secret payload는 Terraform resource/data source로 관리하지 않는다.
필수 credential은 ephemeral variable과 provider write-only argument를
통해서만 apply에 전달한다.
+25
View File
@@ -0,0 +1,25 @@
# ADR 0003: Vault topology
Status: accepted for dev, production decision pending
동일한 단일 노드 K3s 안의 두 Vault는 failure domain을 분리하지 못하면서
초기화, Transit credential, rotation과 staged apply 절차를 추가했다.
따라서 `dev-k3s`는 단일 self-hosted Vault로 단순화한다.
Dev profile:
- single-node integrated Raft
- Shamir 1-of-1 init/unseal
- TLS 미적용
- 명시적 backup/recovery runbook
이 구성은 production에 사용할 수 없다. production은 다음 중 하나를
선택해야 한다.
- managed Vault
- workload cluster 밖의 독립 HA Vault
- 최소 3-node integrated-Raft + TLS + KMS/HSM auto-unseal + PDB,
anti-affinity와 정기 restore exercise
production Vault가 결정되기 전에는 production manifest와 Terraform root를
만들지 않는다.
+18
View File
@@ -0,0 +1,18 @@
# ADR 0004: Single Argo CD root
Status: accepted
Argo CD 설치 후 `bootstrap/argocd/root-application.yaml` 하나만 seed한다.
root는 `clusters/dev-k3s`의 AppProject와 모든 child Application을 소유한다.
반복 `kubectl apply`와 foundation/platform/application별 root wrapper는
제거한다. routine deployment는 Git merge만으로 시작한다.
Child Application의 sync wave는 객체 생성 순서를 가독성 있게 표현하지만
서로 다른 Application의 readiness dependency로 간주하지 않는다.
Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할 수 있어야 한다.
Root가 child Application을 prune하거나 삭제하려면 확인이 필요하다.
shared resource 소유권 충돌은 sync를 실패시킨다. 현재 규모에서는 명시적
Application을 사용하고 두 번째 클러스터가 생길 때 ApplicationSet을
검토한다.
+17
View File
@@ -0,0 +1,17 @@
# ADR 0005: Cluster-first repository layout
Status: accepted
현재는 하나의 platform 팀, 하나의 dev cluster와 소수 workload를 가지므로
GitOps configuration monorepo를 유지한다. application source repository와
deployment configuration repository는 분리한다.
- `platform/`, `workloads/`: 환경 중립 base
- `clusters/<cluster>/manifests`: cluster-specific final composition
- `clusters/<cluster>/applications`: Argo reconciliation inventory
- `iac/terraform`: Kubernetes manifest와 분리된 external API IaC
- `bootstrap`: controller가 존재하기 전의 최소 seed
production 접근권한, 소유 팀, Terraform backend 또는 release cadence가
실제로 갈라질 때 platform GitOps, workload GitOps, IaC repo 분리를
재검토한다. 존재하지 않는 환경의 skeleton은 유지하지 않는다.
+33
View File
@@ -0,0 +1,33 @@
# ADR 0006: Gateway API first, Istio deferred
Status: accepted
현재 단일 노드 K3s와 auth/api 중심 workload에는 service mesh 운영 비용을
정당화할 mTLS identity, L7 authorization, canary traffic policy 또는
multi-team 요구가 없다. 이번 개편에는 Istio를 설치하지 않는다.
선행 작업:
1. Traefik Gateway API provider와 GatewayClass 검증
2. Ingress를 Gateway/HTTPRoute로 이관
3. north-south TLS
4. 내부 호출의 ingress hairpin 제거
5. Vault/PostgreSQL native TLS
6. NetworkPolicy regression test와 observability/SLO
Istio 요구가 실제화되면 sidecar가 아니라 ambient mode로 제한 pilot한다.
초기 범위는 api-server와 auth-server이며 Vault, Vault injector,
PostgreSQL은 제외한다. ztunnel L4부터 시작하고 L7 정책이 필요할 때만
waypoint를 추가한다.
다음 기능 요구 중 두 개 이상과 운영 선행조건이 모두 충족될 때 ADR을
재검토한다.
- ServiceAccount identity 기반 east-west mTLS
- path/JWT 기반 L7 authorization
- canary traffic split/retry/timeout/outlier detection
- 지속적인 서비스·namespace·팀 증가
- application instrumentation만으로 해결하기 어려운 장애 분석
현재 Ingress를 즉시 제거하지 않는다. TLS, DNS, GatewayClass 계약이
확정되기 전 가상의 Gateway 설정을 배포하지 않기 위함이다.
+38
View File
@@ -0,0 +1,38 @@
# Argo CD layout
`bootstrap/argocd/root-application.yaml`이 유일한 수동 seed입니다. 이
Application은 `clusters/dev-k3s`를 source로 사용하고 다음 리소스를
소유합니다.
```text
clusters/dev-k3s
├── projects
└── applications
├── foundation
│ ├── sealed-secrets
│ ├── vault
│ └── vault-agent-injector
├── platform
│ └── auth-system
└── workloads
├── auth-server
└── api-server
```
AppProject는 root sync wave `-10`, foundation은 `0~1`, platform은 `10`,
workload는 `20`입니다. 이 wave는 child Application 객체 생성 순서만
표현하며 서로 다른 Application의 readiness dependency로 사용하지
않습니다. Vault Agent와 workload는 필요한 Vault/DB API가 준비될 때까지
자체 retry 가능한 형태여야 합니다.
모든 child Application은 auto-sync, prune, self-heal을 사용합니다.
Application 삭제와 parent prune은 확인이 필요하며, shared resource
소유권 충돌은 `FailOnSharedResource=true`로 실패시킵니다.
Sync hook이 있는 `auth-server``auth-system`에는 selective sync 옵션을
사용하지 않습니다. DB migration과 Keycloak client sync는 같은
Application 내부 wave로 순서를 제어합니다.
클러스터가 하나이고 child Application 수가 적으므로 현재는 명시적
Application을 사용합니다. 두 번째 클러스터나 실제 production이 생길
때 foundation/platform/workload별 ApplicationSet 도입을 검토합니다.
+55
View File
@@ -0,0 +1,55 @@
# Deployment architecture
## Reconciliation boundaries
```text
Gitea main
|
+-- Argo CD root -> AppProjects + child Applications -> Kubernetes
|
+-- approved Terraform runner -> Vault API
```
Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치/root
seed와 문서화된 recovery 외에는 직접 cluster mutation을 하지 않습니다.
Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지
않습니다.
## Kustomize ownership
- `platform/`, `workloads/`: 환경 중립 base
- `clusters/dev-k3s/manifests/`: namespace, host, image, Vault role 및
NetworkPolicy를 포함하는 최종 cluster composition
- Argo CD Application: final composition만 source로 사용
지원하지 않는 production overlay는 존재하지 않습니다. production
계약과 승인 경계가 확정될 때 별도로 생성합니다.
## In-application ordering
`auth-server`의 한 sync operation 안에서:
- generated ConfigMap과 일반 리소스: wave `0`
- database migration Sync hook: wave `5`
- Deployment: wave `10`
- north-south route: wave `20`
`auth-system`의 Keycloak client sync도 idempotent Sync hook이며 deadline,
backoff, `BeforeHookCreation,HookSucceeded` cleanup을 사용합니다.
## Stateful lifecycle
Vault와 PostgreSQL PVC는 `Prune=false`로 보호합니다. child Application
prune/delete는 확인이 필요합니다. path 이동이나 Application rename 전에는
새 owner가 동일 live resource를 정상적으로 추적하는지 확인한 후 이전
owner를 non-cascading 방식으로 제거합니다.
## Image promotion
첫-party image는 애플리케이션 CI가 얻은 정확한 GHCR digest를 Gitea
workflow에 전달합니다. workflow는 digest 변경 PR을 만들고, validation과
승인을 거쳐 merge된 뒤 Argo CD가 배포합니다.
현재 short-SHA tag는 migration 시점의 예외입니다. private GHCR을 읽을
자격증명이 이 저장소 실행 환경에 없으므로 임의 digest로 바꾸지 않았고,
다음 정상 promotion에서 `digest:`로 교체됩니다.
+55
View File
@@ -0,0 +1,55 @@
# Secret trust boundaries
## Dev Vault
`dev-k3s`는 단일 self-hosted Vault를 사용합니다. 동일 workload
클러스터에 별도의 Transit Vault를 두지 않습니다. 단일 Vault는 다음을
소유합니다.
- KV-v2 runtime secret path
- Kubernetes auth와 workload role
- dynamic PostgreSQL credential
- 애플리케이션 JWT signing용 Transit key
dev Vault는 Shamir 1-of-1로 한 번 초기화하고 재시작 시 명시적으로
unseal합니다. 이 방식은 개발 환경 전용입니다. production에서는 managed
Vault 또는 독립 failure domain의 HA integrated-Raft와 KMS/HSM
auto-unseal을 사용해야 합니다.
## Terraform
`vault-core` state는 mounts, auth, policies, roles와 JWT key를 소유하며
제한된 관리자만 적용합니다. `vault-database`는 PostgreSQL connection과
dynamic roles만 소유하고 `vault-database-automation-dev` 정책을 사용합니다.
Terraform variable로 전달되는 token과 PostgreSQL password는 ephemeral/
write-only 경계를 사용합니다. KV payload는 Terraform resource/data
source로 읽거나 쓰지 않습니다.
## Workload authentication
workload는 audience `vault`, TTL 1시간의 projected ServiceAccount token으로
Vault Kubernetes auth에 로그인합니다. token은 Vault Agent가 사용하며
application container에 Kubernetes bearer token을 직접 노출하지 않습니다.
Secret payload는 승인된 운영자가 Vault에 직접 기록합니다. 값은 Git,
Gitea Actions log, Terraform state, Kubernetes manifest에 남기지 않습니다.
## Bootstrap material
Vault init output은 기본적으로 `.local/vault/dev-k3s-init.json`에 mode
`0600`으로 생성됩니다. encrypted custody로 이동한 후 working copy를
제거합니다. initial root token은 `vault-core`와 operator auth 검증 직후
폐기합니다.
Sealed Secrets는 private GHCR pull credential에만 사용합니다. controller
private key는 별도 복구 저장소에 백업해야 합니다.
## Dev limitations
- Vault, PostgreSQL, ingress가 아직 TLS를 사용하지 않음
- single-node Vault와 PostgreSQL
- Kubernetes API egress CIDR가 현재 dev cluster에 종속
- 정적 bootstrap secret은 coordinated rotation 필요
이 제약은 production에서 허용되지 않습니다.
+97
View File
@@ -0,0 +1,97 @@
# Agent Guide
이 파일은 이 저장소에서 작업을 시작하는 사람과 에이전트를 위한 첫 진입 문서입니다.
## Read First
작업을 시작하면 아래 순서로 문서를 읽습니다.
1. 이 파일 `AGENT.md`
2. 루트 [README.md](/home/donghyeon/dev/Project-Auth-GitOps/README.md)
3. 현재 작업과 직접 관련된 runbook
- Vault transit: [runbooks/vault-transit/dev/README.md](/home/donghyeon/dev/Project-Auth-GitOps/runbooks/vault-transit/dev/README.md)
- Workload Vault: [runbooks/vault/dev/README.md](/home/donghyeon/dev/Project-Auth-GitOps/runbooks/vault/dev/README.md)
- Argo CD 구조: [argocd/README.md](/home/donghyeon/dev/Project-Auth-GitOps/argocd/README.md)
## Working Rules
- README 최상단 `현재 최신 Dev 아키텍처` 는 최신 상태로 유지합니다.
- 기존 cycle은 지우지 말고 README 맨 아래에 새 cycle을 추가합니다.
- ops 변경이나 장애 대응을 했으면 명령, 관찰, 판단 근거, 수정, 검증을 함께 남깁니다.
- 워크플로 구조를 크게 바꿨으면 README에 새 cycle을 추가합니다.
- 민감한 값은 절대 문서에 기록하지 않습니다.
- token, password, kubeconfig 본문, secret payload 금지
- 대신 존재 여부, 길이, secret name, 리소스 상태만 기록
- repo 밖 임시 파일(`/tmp/...`)로 작업한 secret manifest는 Git에 넣지 않습니다.
- Vault 관련 자동화는 **bootstrap****reconcile** 을 분리합니다.
- bootstrap: privileged token 필요, 수동 runbook
- reconcile: workflow AppRole 기반 routine CI
- GitHub Actions YAML에는 긴 Bash를 직접 넣지 않고 `scripts/ci/*.sh` wrapper로 분리합니다.
- GitHub context(`inputs`, `client_payload`) 는 inline Bash에서 직접 쓰지 않고 step `env:` 로 매핑한 뒤 스크립트에서 읽습니다.
- `scripts/ci/reconcile-vault-dev.sh` 는 기본적으로 port-forward 모드지만, 장기적으로는 `RECONCILE_USE_PORT_FORWARD=false` + in-cluster service URL로 돌리는 방향이 목표입니다.
- `vault-dev-reconcile.yaml` 은 아래 GitHub Variables 로 runner/네트워크 전환을 제어합니다.
- `VAULT_DEV_RECONCILE_RUNS_ON`
- `RECONCILE_USE_PORT_FORWARD`
- `TRANSIT_VAULT_ADDR`
- `WORKLOAD_VAULT_ADDR`
## Troubleshooting Format
README cycle의 `### 6. 트러블슈팅 메모` 에 아래 형식으로 남깁니다.
- 재현/확인 명령: 실제로 사용한 명령
- 핵심 관찰값: 에러 문구, 상태 변화, 이벤트, 로그 핵심
- 판단 근거: 왜 그 관찰값을 보고 해당 원인이라고 판단했는지
- 수정 또는 조치: 어떤 파일/리소스를 바꿨는지
- 검증 명령: 해결 여부를 다시 확인한 명령
## Current Dev Pitfalls
- Argo CD `Application``Repository not found` 를 내면 로컬 git 인증이 아니라 `argocd` namespace의 repo credential secret부터 확인합니다.
- self-hosted runner 문제는 workflow YAML보다 먼저 runner 호스트의 실제 CLI 설치 여부를 확인합니다.
- `command -v kubectl vault jq base64 curl terraform`
- Vault raft 사용 시 `api_addr`, `cluster_addr`, listener `cluster_address`, service/deployment의 `8201` 포트 일관성을 먼저 확인합니다.
- `hashicorp/vault` 이미지를 ConfigMap mount와 함께 쓸 때는 read-only mount 충돌과 단일 PVC rollout 충돌을 같이 봅니다.
- 필요하면 `/tmp` 로 config 복사 후 실행, `strategy: Recreate` 적용
- Vault Agent template에 여러 `export` 줄을 렌더링할 때는 aggressive trim(`{{- ... -}}`) 때문에 줄바꿈이 붙지 않는지 실제 `/vault/secrets/*` 파일을 확인합니다.
- Postgres PVC를 유지하는 상태에서 Vault KV만 바꾸면 DB 내부 사용자 비밀번호와 주입값이 어긋날 수 있습니다. KV 값과 persisted DB state를 함께 확인합니다.
- Headless Service를 DB connection host로 쓸 때는 pod 안에서 실제로 어떤 이름이 해석되는지 먼저 확인합니다.
- `postgres.platform.svc.cluster.local` 이 안 되면 `postgres-0.postgres.platform.svc.cluster.local` 같이 pod FQDN 확인
- CI에서 Terraform local backend를 쓰면 self-hosted runner가 기존 `.terraform-state/*.tfstate` 를 실제로 보고 있는지 먼저 확인합니다. state가 완전히 없을 때뿐 아니라 일부 리소스만 남은 partial state도 위험합니다.
- `terraform state list`
- `terraform state show <resource>`
- workflow에서 빠진 리소스를 개별 import 하도록 유지
- 단, `vault_approle_auth_backend_role_secret_id` 처럼 provider가 import를 지원하지 않는 리소스는 예외로 두고 재생성으로 수렴시킵니다.
- Vault KV-v2 mount는 provider import 결과가 `type=kv` + `options.version=2` 로 보이므로, 선언도 그 형태로 맞추고 `prevent_destroy = true` 를 유지합니다.
- seal token을 state로 다시 맞출 때는 `auth/token/revoke-accessor` 권한이 필요합니다.
- `vault_mount`, `vault_auth_backend`, `vault_token.seal` 같이 bootstrap 성격이 강한 리소스는 routine CI에서 replacement가 나지 않게 drift를 최소화합니다.
- 필요하면 `ignore_changes`
- 더 좋게는 bootstrap 단계와 reconcile 단계를 분리
- `kubectl rollout status` 실패 시 `describe`, `logs`, `events` 를 세트로 봅니다.
## Default Command Order
Argo CD app 문제:
```bash
kubectl -n argocd get applications
kubectl -n argocd describe application <name>
kubectl -n argocd get secrets
```
Runner 문제:
```bash
command -v kubectl
command -v vault
command -v terraform
```
Vault pod 문제:
```bash
kubectl -n <ns> get deploy,pods
kubectl -n <ns> describe deployment <name>
kubectl -n <ns> logs deploy/<name> --tail=200
kubectl -n <ns> get events --sort-by=.lastTimestamp | tail -n 30
```
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
# Archived documentation
Files in this directory describe historical repository states. Paths,
workflows, credentials and operational commands may no longer exist.
Do not execute archived procedures. Use the root README and `docs/runbooks/`.
+209
View File
@@ -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을 사용합니다.
+46
View File
@@ -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를 삭제하지 않습니다.
+44
View File
@@ -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가
필요합니다.