refactor(gitops): establish platform ownership boundaries

This commit is contained in:
donghyeon-ka
2026-07-26 01:34:29 +09:00
parent 293ee6fc97
commit a6f6c663e0
121 changed files with 2801 additions and 1204 deletions
+150 -29
View File
@@ -1,38 +1,159 @@
# Argo CD layout
# Argo CD architecture
`bootstrap/argocd/root-application.yaml`이 유일한 수동 seed입니다. 이
Application은 `clusters/dev-k3s`를 source로 사용하고 다음 리소스를
소유합니다.
## Control-plane ownership
Controller 설치 후 다음 두 bootstrap object를 순서대로 수동 seed합니다.
1. `bootstrap/argocd/control-plane-project.yaml`
2. `bootstrap/argocd/root-application.yaml`
`gitops-control-plane` AppProject는 canonical Gitea repository와 in-cluster
`argocd` namespace, AppProject/ApplicationSet kind만 허용합니다. 단일 root
Application은 이 Project를 사용하고 다음 control-plane 구성을 source로
사용합니다.
```text
clusters/dev-k3s
platform/control-plane/argocd
├── projects
└── applications
├── foundation
│ ├── sealed-secrets
── vault
│ └── vault-agent-injector
├── platform
│ └── auth-system
── workloads
├── auth-server
└── api-server
│ ├── platform-addons.yaml
├── platform-services.yaml
│ ├── systems.yaml
── workloads.yaml
└── application-sets
├── platform-addons.yaml
├── platform-services.yaml
── systems.yaml
└── workloads.yaml
```
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 가능한 형태여야 합니다.
Root는 AppProject와 ApplicationSet까지만 직접 소유합니다. 각
ApplicationSet의 list inventory가 실제 child Application을 생성합니다.
Routine 변경에 category별 root나 직접 `kubectl apply`를 추가하지 않습니다.
모든 child Application은 auto-sync, prune, self-heal을 사용합니다.
Application 삭제와 parent prune은 확인이 필요하며, shared resource
소유권 충돌은 `FailOnSharedResource=true`로 실패시킵니다.
## AppProject boundary
Sync hook이 있는 `auth-server``auth-system`에는 selective sync 옵션을
사용하지 않습니다. DB migration과 Keycloak client sync는 같은
Application 내부 wave로 순서를 제어합니다.
| AppProject | 소유 범위 | 허용 destination |
|---|---|---|
| `platform-addons` | Sealed Secrets, Vault Agent Injector 같은 외부 cluster addon | `kube-system`, `vault` |
| `platform-services` | 저장소가 소유하는 Vault shared service | `vault` |
| `systems` | Project Auth 전용 PostgreSQL, Keycloak, sync job | `auth-system-dev` |
| `workloads` | first-party `auth-server`, `api-server` | `auth-dev`, `api-dev` |
클러스터가 하나이고 child Application 수가 적으므로 현재는 명시적
Application을 사용합니다. 두 번째 클러스터나 실제 production이 생길
때 foundation/platform/workload별 ApplicationSet 도입을 검토합니다.
Project는 ApplicationSet 파일마다 고정하며 inventory 값으로 template하지
않습니다. 이렇게 해야 element 변경으로 권한 경계를 넘을 수 없습니다.
각 Project는 필요한 source repository, destination, resource kind만
allowlist합니다.
## ApplicationSet contract
ApplicationSet은 strict Go template와 list generator를 사용합니다.
- `goTemplate: true`
- `goTemplateOptions: ["missingkey=error"]`
- 공통 element: `component`, `cluster`, `server`, `namespace`, quoted string
`autoSync`
- Git source element: ownership grammar를 따르는 `path`; `targetRevision`
template의 `main`으로 고정
- Helm addon element: allowlisted `repoURL`, `chart`, chart `revision`,
`helmValues`
- 파일별 고정 project
필수 key가 빠지면 빈 문자열로 잘못 배포하지 않고 render가 실패해야 합니다.
Application 이름, destination, source path는 같은 element에서 파생하되
project와 Git repository/revision trust boundary는 template하지 않습니다.
외부 addon의 Helm `repoURL`은 element에서 template되지만 고정 AppProject의
`sourceRepos` allowlist 밖 URL은 sync할 수 없습니다.
## `autoSync` stage gate
`autoSync`는 bootstrap 준비 상태와 routine reconciliation을 분리합니다.
Inventory schema는 Go template 비교를 위해 boolean이 아닌 quoted string
`"true"`/`"false"`를 사용합니다. Template patch는 값이 `"true"`
element에만 다음 정책을 추가합니다.
```yaml
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
```
초기 gate는 다음과 같습니다.
| Application | 초기 `autoSync` | 열기 전 확인 |
|---|---:|---|
| Sealed Secrets | `"true"` | Argo가 chart source를 읽을 수 있음 |
| Vault | `"true"` | dev PVC와 NetworkPolicy 변경 검토 |
| Vault Agent Injector | `"false"` | Vault init, `vault-foundation`, `vault-workloads`, runtime secret seed 완료 |
| `auth-system` | `"false"` | Injector Healthy와 Vault login/secret capability 확인 |
| `auth-server` | `"false"` | PostgreSQL Healthy, `vault-database`, Keycloak/sync 준비 완료 |
| `api-server` | `"false"` | `auth-server` Healthy와 호출 경로 확인 |
Gate는 단계별 PR로 하나씩 엽니다. `false`여도 Application 생성과 diff
표시는 계속되며 수동 Sync 자체를 기술적으로 막지는 않습니다. 따라서
Argo RBAC에서 sync 권한을 제한하고, 수동 Sync에는 명시적 change record를
요구합니다.
비활성 기간 동안 쌓인 모든 diff가 gate를 여는 순간 함께 반영됩니다.
`autoSync: "true"` PR은 현재 live-to-desired 전체 diff를 검토한 뒤
승인해야 합니다.
## Ordering과 failure handling
Control-plane sync wave는 AppProject(`-10`)를 ApplicationSet(`-5`)보다
먼저 생성합니다. 네 ApplicationSet은 모두 같은 wave이고 element별
stage/wave field는 없습니다. Generated Application의 실제 readiness
순서는 `autoSync` 전환과 health 확인이 담당합니다.
Vault Agent, workload와 hook은 선행 API가 늦게 준비될 때 retry할 수 있어야
합니다. `auth-system`의 Keycloak client sync와 `auth-server`의 database
migration은 idempotent Sync hook입니다. Hook을 사용하는 Application에는
`ApplyOutOfSyncOnly=true`를 설정하지 않습니다.
Generated Application은 automated 상태에서 `PruneLast=true`
`FailOnSharedResource=true`를 사용합니다. ApplicationSet은
`applicationsSync: create-update``preserveResourcesOnDeletion: true`
사용합니다. Parent prune과 Application 삭제에는 확인을 요구합니다.
CRD와 cluster-wide RBAC를 포함할 수 있는 `platform-addons`
Application-level `Prune=confirm`도 사용하므로 chart upgrade의 삭제는
별도 승인이 필요합니다.
Stateful path rename이나 ownership 이동은 별도 migration으로 수행하며
ApplicationSet element를 먼저 삭제하지 않습니다.
`create-update`에서는 generator element를 제거해도 기존 generated
Application이 자동 삭제되지 않고 stale 상태로 남습니다. Element 제거와
Application/resource decommission은
[application decommission runbook](../runbooks/application-decommission.md)의
inventory, gate, backup, 명시적 삭제 절차를 따릅니다.
## Generator 확장 기준
현재는 cluster가 `dev-k3s` 하나이므로 네 ApplicationSet의 explicit List
generator가 가장 쉽게 검토됩니다. 존재하지 않는 production이나 미래
cluster를 위해 Matrix abstraction을 미리 만들지 않습니다.
두 번째 실제 cluster가 생겨 `cluster`, `server`와 cluster별 gate를 여러
component에서 반복하게 될 때 `clusters/<cluster>/config.yaml`을 Git files
generator로 읽고 component inventory와 Matrix generator로 결합합니다.
그때도 AppProject는 ApplicationSet template에 고정하고, cluster별
`autoSync`는 quoted string과 승인 gate로 유지합니다.
## Further reading
- Argo CD: [cluster bootstrapping](https://argo-cd.readthedocs.io/en/stable/operator-manual/cluster-bootstrapping/),
[ApplicationSet modification policy](https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Controlling-Resource-Modification/),
[ApplicationSet deletion](https://argo-cd.readthedocs.io/en/stable/operator-manual/applicationset/Application-Deletion/),
[automated sync semantics](https://argo-cd.readthedocs.io/en/stable/user-guide/auto_sync/)
- Kubernetes:
[Kustomize](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/)
- Terraform: [state refactoring](https://developer.hashicorp.com/terraform/language/state/refactor),
[`terraform_remote_state` security warning](https://developer.hashicorp.com/terraform/language/state/remote-state-data),
[write-only arguments](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/write-only)
- Vault:
[JWT/OIDC authentication](https://developer.hashicorp.com/vault/docs/auth/jwt)
## Repository-only change
이 구조와 gate 설계는 2026-07-26 현재 Git에서만 작성·검증했습니다. 실제
cluster migration이나 sync는 이 리팩터링 리뷰 범위에 포함되지 않습니다.
+87 -25
View File
@@ -5,29 +5,71 @@
```text
Gitea main
|
+-- Argo CD root -> AppProjects + child Applications -> Kubernetes
+-- Argo CD root
| -> AppProjects + ApplicationSets
| -> generated Applications
| -> Kubernetes
|
+-- approved Terraform runner -> Vault API
+-- approved Terraform runner
-> one of three Vault states
-> Vault API
```
Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치/root
seed 문서화된 recovery 외에는 직접 cluster mutation을 하지 않습니다.
Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지
않습니다.
Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치
bootstrap 전용 AppProject/root seed, 문서화된 recovery 외에는 직접
cluster mutation을 하지 않습니다. Terraform은 Config Management
Plugin이나 Argo hook 안에서 실행하지 않습니다. GHCR은 image artifact
registry이며 desired state source가 아닙니다.
## Kustomize ownership
## Kubernetes ownership
- `platform/`, `workloads/`: 환경 중립 base
- `clusters/dev-k3s/manifests/`: namespace, host, image, Vault role 및
NetworkPolicy를 포함하는 최종 cluster composition
- Argo CD Application: final composition만 source로 사용
| Layer | 역할 |
|---|---|
| `platform/control-plane/argocd` | AppProject와 ApplicationSet control plane |
| `platform/shared-services/*/base` | 환경 중립을 목표로 하는 공유 cluster service base |
| `systems/*/base` | 환경 중립을 목표로 하는 bounded-context backing system base |
| `workloads/*/base` | first-party 애플리케이션 base |
| `clusters/dev-k3s/overlays/*` | dev namespace, host, digest, Vault role/path, NetworkPolicy를 합친 최종 구성 |
지원하지 않는 production overlay는 존재하지 않습니다. production
계약과 승인 경계가 확정될 때 별도로 생성합니다.
현재 concrete ownership은 Vault가 platform shared service,
PostgreSQL/Keycloak이 `systems/auth-system`, 두 서버가 workload입니다.
Argo CD Application은 base가 아니라 최종 cluster overlay만 source로
사용합니다.
현재 Keycloak base의 `start-dev`와 Vault base의
TLS-off/single-node identity는 dev-specific 예외입니다. 내부 Service
참조는 짧은 DNS로 namespace 중립화했지만, 남은 값을 overlay로 추출하는
작업은 후속 리팩터링입니다.
지원하지 않는 production overlay는 존재하지 않습니다. Production trust,
approval, TLS, availability contract가 확정될 때 별도로 설계합니다.
## Bootstrap progression
```text
Argo root
-> Sealed Secrets + Vault autoSync
-> Vault init
-> vault-foundation
-> vault-workloads
-> runtime secret seed
-> Vault Agent Injector autoSync gate
-> auth-system autoSync gate
-> PostgreSQL Healthy
-> vault-database
-> Keycloak/client sync ready
-> auth-server autoSync gate
-> auth-server Healthy
-> api-server autoSync gate
```
이 순서는 Application sync wave로 강제하지 않습니다. 각 전환은 health와
plan/diff를 확인한 별도 PR입니다. Gate가 닫힌 동안에도 generated
Application은 OutOfSync diff를 보여 줍니다.
## In-application ordering
`auth-server`의 한 sync operation 안에서:
`auth-server`의 한 sync operation 안에서는 다음 ordering을 사용합니다.
- generated ConfigMap과 일반 리소스: wave `0`
- database migration Sync hook: wave `5`
@@ -36,20 +78,40 @@ Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지
`auth-system`의 Keycloak client sync도 idempotent Sync hook이며 deadline,
backoff, `BeforeHookCreation,HookSucceeded` cleanup을 사용합니다.
Application 간 준비 순서와 Application 내부 hook 순서를 혼동하지
않습니다.
## Stateful lifecycle
Vault와 PostgreSQL PVC는 `Prune=false`로 보호합니다. child Application
prune/delete는 확인이 필요합니다. path 이동이나 Application rename 전에는
새 owner가 동일 live resource를 정상적으로 추적하는지 확인한 후 이전
owner를 non-cascading 방식으로 제거합니다.
Vault PVC`Prune=confirm,Delete=confirm`이 명시되어 있습니다.
PostgreSQL PVC는 StatefulSet `volumeClaimTemplates`가 생성하며 현재
manifest에 별도 Argo prune annotation이 없습니다. Namespace와 generated
Application 삭제 보호만 믿지 말고 PostgreSQL retention/backup을 직접
확인해야 합니다. Path, namespace, Application 이름을 이동할 때는 다음을
별도 migration으로 다룹니다.
## Image promotion
1. 기존 live object와 owner를 inventory합니다.
2. 새 owner가 같은 object를 안전하게 추적할 수 있는지 render/diff로
확인합니다.
3. Stateful data backup과 rollback 지점을 확보합니다.
4. 기존 owner를 non-cascading 방식으로 제거한 뒤 새 owner를 연결합니다.
첫-party image는 애플리케이션 CI가 얻은 정확한 GHCR digest를 Gitea
workflow에 전달합니다. workflow는 digest 변경 PR을 만들고, validation과
승인을 거쳐 merge된 뒤 Argo CD가 배포합니다.
이번 리팩터링에서는 `platform` namespace의 auth-system을
`auth-system-dev`로 옮기는 live 작업을 실행하지 않았습니다.
현재 short-SHA tag는 migration 시점의 예외입니다. private GHCR을 읽을
자격증명이 이 저장소 실행 환경에 없으므로 임의 digest로 바꾸지 않았고,
다음 정상 promotion에서 `digest:`로 교체됩니다.
## Image promotion과 GHCR
정상 promotion에서 first-party image CI는 검증한 정확한 GHCR digest를
Gitea workflow에 전달합니다. Workflow는 전용 branch와 digest 변경 PR을
만들고 validation과 승인을 거쳐 merge된 뒤 Argo CD가 배포합니다.
Renovate는 외부 chart, third-party image와 Terraform provider만 갱신하며
두 first-party GHCR package는 비활성화합니다. 따라서 동일 image field를
promotion workflow와 Renovate가 동시에 쓰지 않습니다.
Private GHCR pull credential만 SealedSecret으로 Git에 저장합니다. 평문
credential이나 registry token은 manifest, Actions log, Terraform state에
남기지 않습니다.
현재 short-SHA tag는 migration 시점의 예외입니다. Registry 검증 없이
임의 digest를 만들지 않고 다음 정상 promotion에서 immutable digest로
교체합니다.
+157
View File
@@ -0,0 +1,157 @@
# Repository taxonomy
이 문서는 새 리소스를 어느 디렉터리에 둘지 결정하는 기준입니다. 이
저장소는 Project Auth를 예제로 삼는 독립 reference lab이며, 디렉터리
이름은 조직의 중요도나 설치 순서가 아니라 소유권을 표현합니다.
## 분류 기준
| 분류 | 판단 질문 | 현재 예 |
|---|---|---|
| `platform` | GitOps control plane이거나, 둘 이상의 system이 독립 lifecycle로 소비할 cluster capability인가? | Argo inventory, Vault shared service |
| `systems` | 하나의 bounded context가 함께 소유하는 backing system인가? | Project Auth의 PostgreSQL, Keycloak, realm/client sync |
| `workloads` | 별도 source repository에서 빌드하는 first-party 실행 단위인가? | `auth-server`, `api-server` |
| `clusters` | 특정 클러스터의 최종 composition 값인가? | namespace, host, digest, Vault role, NetworkPolicy |
| `iac` | Kubernetes가 아닌 외부 API 객체를 선언하는가? | Vault mounts, policies, auth roles, database roles |
| `bootstrap` | GitOps controller가 존재하기 전에 필요한 최소 seed인가? | Argo CD 설치 버전, 제한된 control-plane AppProject와 root Application |
다음 세 질문을 순서대로 사용합니다.
1. 누가 소비하고 장애 영향을 받는가?
2. 누가 변경을 승인하고 lifecycle을 책임지는가?
3. 다른 bounded context와 독립적으로 교체하거나 배포할 수 있는가?
제품 이름만으로 분류하지 않습니다. 예를 들어 Keycloak이 여러 system의
공용 identity service가 되고 별도 owner와 release cadence를 갖게 되면
실행 서비스는 `platform/`으로 이동할 수 있습니다. 그래도 Project Auth
realm/client 구성은 `systems/auth-system/`에 남습니다. 현재 Keycloak과
PostgreSQL은 Project Auth 전용이므로 모두 system 소유입니다.
## Path contract
환경 중립 base와 cluster-specific overlay를 분리하는 것이 목표
contract입니다.
```text
platform/shared-services/<name>/base
systems/<system>/base
workloads/<workload>/base
clusters/<cluster>/overlays/platform/<name>
clusters/<cluster>/overlays/systems/<system>
clusters/<cluster>/overlays/workloads/<workload>
platform/control-plane/argocd/projects
platform/control-plane/argocd/application-sets
```
Base에는 재사용 가능한 workload 구조, Service, ServiceAccount와 기본
configuration contract를 둡니다. Overlay에는 다음처럼 클러스터와 환경을
알아야 하는 값을 둡니다.
- namespace와 public/internal host
- image reference; 정상 promotion의 목표는 immutable digest
- Vault auth role과 KV path annotation
- NetworkPolicy의 namespace/CIDR
- dev-only resource profile와 TLS 차이
Argo CD는 base를 직접 source로 사용하지 않고 반드시 최종 overlay를
reconcile합니다.
현재 first-party overlay의 짧은 commit tag는 이관 예외입니다. Registry를
검증할 credential 없이 임의 digest로 바꾸지 않고 다음 정상 promotion
PR에서 immutable digest로 전환합니다.
### 현재 base의 알려진 예외
Base 내부의 PostgreSQL·Keycloak 참조는 namespace를 포함하지 않은 짧은
Service DNS를 사용하므로 overlay namespace에 재사용할 수 있습니다. 다만
아직 다음 dev/single-node 가정은 남아 있습니다.
- `systems/auth-system/base`의 Keycloak 실행 command가 `start-dev`입니다.
- `platform/shared-services/vault/base/files/vault/vault.hcl`
`tls_disable = 1`과 고정된 single-node `node_id`를 사용합니다.
이는 숨겨진 환경 중립성이 아니라 명시적인 리팩터링 부채입니다. 두 번째
환경이나 replica를 만들기 전에 dev 전용 command, TLS와 node identity를
overlay 또는 입력 가능한 configuration으로 옮깁니다.
```text
base -> dev-k3s overlay -> ApplicationSet inventory -> generated Application
-> Argo CD -> Kubernetes
```
## Platform 안의 두 역할
`platform` ownership에는 다음 두 종류가 있습니다.
- Cluster addon: Kubernetes API를 확장하거나 admission/control-plane
기능을 제공하는 외부 chart. 현재 Sealed Secrets와 Vault Agent Injector가
해당합니다. Inventory는
`platform/control-plane/argocd/application-sets/platform-addons.yaml`
둡니다.
- Shared service: 일반 workload처럼 namespace에서 실행되지만 여러 system이
사용할 수 있는 capability. 현재 Vault가 해당합니다.
외부 Helm chart를 복사해 base처럼 유지하지 않습니다. chart version과
values는 Argo inventory에서 pin합니다. 저장소가 직접 소유하는 shared
service manifest만 `platform/shared-services/`에 둡니다.
`foundation`은 소유권 분류가 아닙니다. Bootstrap 때 먼저 필요하다는 뜻은
분리된 ApplicationSet category, `autoSync` gate와 runbook 순서로
표현합니다. 따라서 새로운 `foundation/` business directory를 만들지
않습니다.
## System와 workload의 경계
`systems/auth-system`은 인증 bounded context가 함께 책임지는 데이터와
identity backing services입니다.
- PostgreSQL StatefulSet와 초기 database contract
- Keycloak server와 Project Auth realm
- Keycloak client synchronization
`workloads/auth-server``workloads/api-server`는 각각 별도 source
repository와 release digest가 있는 애플리케이션입니다. Workload가
auth-system을 사용하더라도 두 lifecycle을 합치지 않습니다.
Dev namespace도 소유권을 드러냅니다.
| 소유 단위 | Namespace |
|---|---|
| Vault shared service와 injector | `vault` |
| Project Auth backing system | `auth-system-dev` |
| Auth workload | `auth-dev` |
| API workload | `api-dev` |
## Vault path grammar
KV path도 같은 소유권 언어를 사용합니다.
```text
kv/dev/systems/auth-system/postgres/superuser
kv/dev/systems/auth-system/postgres/auth-server
kv/dev/systems/auth-system/postgres/keycloak
kv/dev/systems/auth-system/keycloak/bootstrap-admin
kv/dev/workloads/auth-server/keycloak-client
```
Vault policy 파일에는 KV-v2 API path인 `kv/data/...`를 사용하고, CLI에는
mount-relative path인 `kv/dev/...`를 사용합니다. 이전
`kv/dev/platform/...` 경로는 legacy migration source일 뿐 새 desired
state가 아닙니다.
## 새 항목 배치 예
| 변경 | 위치 |
|---|---|
| 또 다른 공용 admission controller | `platform/control-plane/argocd/application-sets/platform-addons.yaml` |
| 공용 object storage service base | `platform/shared-services/object-storage/base` |
| Project Auth 전용 Redis | `systems/auth-system/base` |
| 새 first-party worker | `workloads/<worker>/base` |
| dev worker digest/secret annotation | `clusters/dev-k3s/overlays/workloads/<worker>` |
| Vault workload policy/role | `vault-workloads` Terraform state와 `policies/vault/` |
| Vault auth backend | `vault-foundation` Terraform state |
분류가 애매하면 설치 순서가 아니라 owner와 소비자 경계를 ADR에 먼저
기록합니다.
+98 -26
View File
@@ -2,54 +2,126 @@
## Dev Vault
`dev-k3s`는 단일 self-hosted Vault를 사용합니다. 동일 workload
클러스터에 별도 Transit Vault 두지 않습니다. 단일 Vault는 다음
소유합니다.
`dev-k3s` workload 클러스터 안의 단일 self-hosted Vault를 사용합니다.
별도 Transit Vault 두지 않습니다. Vault는 다음 API 객체를 제공합니다.
- KV-v2 runtime secret path
- Kubernetes auth와 workload role
- dynamic PostgreSQL credential
- 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을 사용해야 합니다.
Dev Vault는 Shamir 1-of-1로 한 번 초기화하고 재시작 명시적으로
unseal합니다. 이는 폐기 가능한 개발 환경 전용입니다. Production에서는
managed Vault 또는 독립 failure domain의 HA integrated-Raft와 KMS/HSM
auto-unseal이 필요합니다.
## Terraform
## Three Terraform states
`vault-core` state는 mounts, auth, policies, roles와 JWT key를 소유하며
제한된 관리자만 적용합니다. `vault-database`는 PostgreSQL connection
dynamic roles만 소유하고 `vault-database-automation-dev` 정책을 사용합니다.
```text
vault-foundation
-> mounts/auth configuration
-> delegated automation policies
-> optional, separated CI JWT login roles
Terraform variable로 전달되는 token과 PostgreSQL password는 ephemeral/
write-only 경계를 사용합니다. KV payload는 Terraform resource/data
source로 읽거나 쓰지 않습니다.
vault-workloads
-> workload policies and Kubernetes auth roles
-> project-auth-jwt Transit key
vault-database
-> auth-system PostgreSQL connection
-> auth-db-migration-dev dynamic role
```
`vault-foundation`은 routine runner가 아니라 bootstrap 또는 승인된 보안
관리자가 실행합니다. 이 state가 workloads/database automation policy를
만들고, OIDC/JWT trust가 설정됐을 때만 두 login role을 분리해 만듭니다.
Workloads/database exact claim map은 최소 한 공통 discriminator key에서
다른 값을 가져야 합니다. 실제 issuer가 그 repository/ref/job claim을
신뢰할 수 있게 발행하는지 확인하지 못하면 CI JWT auth를 활성화하지
않습니다. Delegated state는 자신에게 권한을 추가할 수 없고 맡은 정확한
Vault API path만 변경합니다.
Exact API path 허용이 runner를 완전한 sandbox로 만들지는 않습니다.
`vault-workloads` runner가 허용된 ACL policy 내용이나 Kubernetes auth role
payload를 악의적으로 바꾸면 더 강한 policy를 연결하는 권한 상승이
가능합니다. 따라서 이 runner는 신뢰된 security automation으로 취급하고,
protected branch, policy lint, saved-plan 승인과 Vault audit log를 함께
trust boundary로 사용합니다.
세 state는 `terraform_remote_state`로 연결하지 않습니다. Policy/role 이름은
checked-in contract로 공유하고 runbook 또는 CI stage가 실행 순서를
보장합니다.
Provider token과 PostgreSQL password는 ephemeral/write-only 입력으로만
전달합니다. KV payload는 Terraform resource/data source로 읽거나 쓰지
않습니다.
## KV ownership paths
Secret path는 repository taxonomy와 같은 owner를 표현합니다.
| Consumer | Vault CLI path |
|---|---|
| PostgreSQL bootstrap | `kv/dev/systems/auth-system/postgres/superuser` |
| Auth database bootstrap/runtime | `kv/dev/systems/auth-system/postgres/auth-server` |
| Keycloak database | `kv/dev/systems/auth-system/postgres/keycloak` |
| Keycloak bootstrap admin | `kv/dev/systems/auth-system/keycloak/bootstrap-admin` |
| Auth-server Keycloak client | `kv/dev/workloads/auth-server/keycloak-client` |
Vault ACL과 Agent annotation은 KV-v2 API path인 `kv/data/...`를 사용합니다.
CLI의 `vault kv put``kv/dev/...`를 사용합니다. 이전
`kv/dev/platform/...` 값은 migration source이며 새 policy가 계속
허용하면 안 됩니다.
## Workload authentication
workload는 audience `vault`, TTL 1시간의 projected ServiceAccount token으로
Vault Kubernetes auth에 로그인합니다. token은 Vault Agent가 사용하며
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에 남기지 않습니다.
Role은 ServiceAccount, namespace, audience, token policy와 TTL을 정확히
묶습니다. 현재 role은 Project Auth backing service의 `auth-system-dev`
Vault를 사용하는 `auth-dev` ServiceAccount에만 존재합니다. `api-dev`에는
Vault role도 Vault NetworkPolicy ingress도 없으며 필요가 생기기 전에는
권한을 추가하지 않습니다.
Secret 값은 승인된 운영자가 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 검증 직후
폐기합니다.
`0600`으로 생성됩니다. Encrypted custody로 이동한 working copy를
제거합니다.
Sealed Secrets는 private GHCR pull credential에만 사용합니다. controller
private key는 별도 복구 저장소에 백업해야 합니다.
Initial root token은 다음 작업에만 사용합니다.
1. `vault-foundation` apply
2. OIDC/JWT가 없는 빈 lab의 짧은 TTL bootstrap token 발급과 capability
검증
3. 최초 runtime secret seed
4. 필요한 break-glass/recovery 절차 확인
`vault-database` 적용까지 끝나면 replacement token의 대표 update
capability와 root policy 부재를 확인한 뒤 initial root와 bootstrap token을
폐기합니다. 상시 cluster ServiceAccount에 broad `platform-admin` 정책을
연결하지 않습니다. `vault-workloads``vault-database`의 routine 실행은
각각 짧은 TTL identity를 사용합니다.
Initial root 폐기 뒤에는 상시 foundation administrator가 없습니다. Future
foundation 변경은 encrypted unseal custody의 승인을 받아 Vault
generated-root ceremony를 수행하고, 승인된 plan 적용 뒤 생성한 root를
즉시 폐기해야 합니다.
Sealed Secrets는 private GHCR pull credential에만 사용합니다. Controller
private key는 Git과 분리된 recovery custody에 백업합니다.
## Dev limitations
- Vault, PostgreSQL, ingress가 아직 TLS를 사용하지 않음
- single-node Vault와 PostgreSQL
- Vault, PostgreSQL, ingress가 TLS를 사용하지 않음
- Single-node Vault와 PostgreSQL
- Kubernetes API egress CIDR가 현재 dev cluster에 종속
- 정적 bootstrap secret은 coordinated rotation 필요
- Namespace/path migration이 아직 실제 cluster에 적용되지 않음
이 제약은 production에서 허용되지 않습니다.
+95
View File
@@ -0,0 +1,95 @@
# Terraform boundary
Terraform은 VM만 정의하는 도구가 아니라 provider가 노출하는 API 객체의
desired state를 선언하고 plan/apply하는 framework입니다. 이 저장소에는
machine/cloud provider가 없으므로 서버, 네트워크, k3s 설치를 Terraform이
소유하지 않습니다. 현재 적용 범위는 Vault API 객체뿐입니다.
## 도구별 소유권
| 대상 | 소유 도구 | 이유 |
|---|---|---|
| Kubernetes manifest와 rollout | Argo CD | Git revision을 지속적으로 reconcile |
| Vault mount, auth, policy, role, Transit key, DB connection | Terraform Vault provider | API 객체의 plan과 state ownership 필요 |
| Vault init/unseal, initial secret seed | 승인된 operator runbook | 일회성 ceremony와 secret material을 state에서 제외 |
| KV secret payload | 외부 secret authority/operator | Git과 Terraform state에 값이 남지 않아야 함 |
| VM, network, k3s | 현재 소유자 없음 | 실제 provider와 lifecycle이 정해지지 않음 |
Kubernetes와 Helm을 Terraform에 다시 넣지 않습니다. 같은 object를 Argo
CD와 Terraform이 동시에 소유하면 두 reconciler가 충돌합니다. 반대로
Terraform을 Argo hook에서 실행하면 cluster reconciliation이 Vault state
lock과 privileged credential lifecycle까지 떠안게 됩니다.
## State 경계
```text
vault-foundation
creates delegation
| |
v v
vault-workloads vault-database
runtime access PostgreSQL integration
```
- `vault-foundation`은 mount, Kubernetes auth와 위임 policy/login role을
소유합니다. Routine CI apply 대상이 아닙니다.
- `vault-workloads`는 runtime ACL/Kubernetes role과 애플리케이션 Transit
key만 소유합니다.
- `vault-database`는 실제 PostgreSQL이 준비된 뒤 connection과 migration
dynamic role만 소유합니다.
위임받은 state는 자기 runner policy나 login role을 만들지 않습니다.
State 간 이름은 checked-in contract로 공유하며 `terraform_remote_state`
다른 state snapshot을 읽지 않습니다.
Policy HCL이 정확한 API path를 허용하므로 `kv`, `database`, `transit`,
`kubernetes`, `project-auth-jwt`, `auth-system-postgres-dev` 같은 보안
경계 이름은 각 root의 local contract로 고정합니다. 변수로 한쪽만
override해 plan은 성공하지만 권한이 어긋나는 상태를 허용하지 않습니다.
이 이름을 바꿀 때는 foundation policy, delegated root와 runtime consumer를
하나의 migration 설계에서 함께 변경합니다.
Mount, Kubernetes auth와 선택적 CI JWT auth에는 `prevent_destroy`
적용합니다. 입력 누락이 기존 auth mount 삭제로 이어지지 않으며, 실제
제거는 consumer/token inventory를 거친 별도 decommission revision에서만
보호를 명시적으로 해제합니다.
## 실행 계약
1. Remote backend는 encryption, versioning, locking과 root별 access
control을 제공해야 합니다.
2. `terraform-plan`이 만든 saved plan을 검토하고, 같은 `PLAN_FILE`
`terraform-apply`가 사용합니다.
3. Plan은 sensitive artifact로 취급하며 apply 성공 후 제거합니다.
4. Provider token과 PostgreSQL password는 Terraform 1.11 이상의
ephemeral variable/write-only argument로 실행 시점에 다시 주입합니다.
5. Delegated runner token은 짧은 TTL, no-default-policy와 정확한 object
path만 사용합니다. Capability 확인, self lookup과 명시적 self revoke에
필요한 세 self-service API만 별도로 허용합니다.
6. Foundation, workloads, database apply는 서로 다른 승인 단계입니다.
## 두 번째 클러스터 또는 machine IaC
두 번째 클러스터가 생겨도 state를 합치지 않습니다. Cluster별 backend와
Vault instance ownership이 독립이면 같은 세 root contract를 reusable
module로 승격합니다. 실제 VM/network provider, account, failure domain과
destroy/backup 책임이 정해졌을 때만 다음처럼 별도 machine root를
추가합니다.
```text
iac/terraform/live/<cluster>/machine
iac/terraform/live/<cluster>/vault-foundation
iac/terraform/live/<cluster>/vault-workloads
iac/terraform/live/<cluster>/vault-database
```
Machine root output을 읽기 위해 Vault state 전체를 공유하지 않습니다.
필요한 endpoint는 명시적 configuration contract나 최소 권한의 별도
configuration store로 전달합니다.
## Further reading
- [Terraform ephemeral values and write-only arguments](https://developer.hashicorp.com/terraform/language/manage-sensitive-data/ephemeral)
- [Terraform state refactoring](https://developer.hashicorp.com/terraform/language/state/refactor)
- [Remote state data security warning](https://developer.hashicorp.com/terraform/language/state/remote-state-data)
- [Vault provider write-only attributes](https://registry.terraform.io/providers/hashicorp/vault/latest/docs/guides/using_write_only_attributes)