refactor(gitops): establish platform ownership boundaries
This commit is contained in:
+150
-29
@@ -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는 이 리팩터링 리뷰 범위에 포함되지 않습니다.
|
||||
|
||||
@@ -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로
|
||||
교체합니다.
|
||||
|
||||
@@ -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에 먼저
|
||||
기록합니다.
|
||||
@@ -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에서 허용되지 않습니다.
|
||||
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user