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
+2 -2
View File
@@ -52,7 +52,7 @@ jobs:
echo "image_digest must be an immutable sha256 digest" >&2
exit 1
fi
manifest="clusters/dev-k3s/manifests/${SERVICE}/kustomization.yaml"
manifest="clusters/dev-k3s/overlays/workloads/${SERVICE}/kustomization.yaml"
temporary="$(mktemp "${manifest}.XXXXXX")"
awk -v image_name="$image_name" -v image_digest="$IMAGE_DIGEST" '
$1 == "-" && $2 == "name:" { target = ($3 == image_name) }
@@ -85,7 +85,7 @@ jobs:
git config user.name "gitops-bot"
git config user.email "gitops-bot@hyeonworks.local"
git switch -c "$branch"
git add "clusters/dev-k3s/manifests/${SERVICE}/kustomization.yaml"
git add "clusters/dev-k3s/overlays/workloads/${SERVICE}/kustomization.yaml"
git commit -m "chore(gitops): promote ${SERVICE} dev digest"
git push --set-upstream origin "$branch"
echo "PROMOTION_BRANCH=$branch" >>"$GITHUB_ENV"
+10
View File
@@ -7,6 +7,16 @@
Vault init JSON, unseal/recovery material or plaintext credentials.
- A Vault API object may be owned by only one Terraform state.
- Keep secret payloads outside Terraform resources and data sources.
- Classify shared capabilities under `platform`, bounded-context backing
services under `systems`, and first-party runtimes under `workloads`.
- Manage child Argo CD Applications through the permission-scoped
ApplicationSets in `platform/control-plane/argocd`; do not add explicit
child Applications or use the `default` AppProject.
- Add new ApplicationSet entries with `autoSync: "false"` and open each gate
only after its documented external prerequisites have been verified.
- Keep `vault-foundation`, `vault-workloads`, and `vault-database` as separate
states. A delegated state must not own the policy or login role that grants
its own execution identity.
- Routine GitOps automation changes Git only; direct cluster mutation is
reserved for documented bootstrap and recovery runbooks.
- Run `make validate` before handing off repository changes.
+30
View File
@@ -1,5 +1,35 @@
# Changelog
## 2026-07-26
- Clarified that this repository is an independent GitOps reference lab, not a
shared production platform or an application source monorepo.
- Reclassified Kubernetes ownership as `platform`, `systems` and `workloads`;
moved the Project Auth PostgreSQL/Keycloak boundary to
`systems/auth-system`.
- Replaced generic cluster `manifests` with ownership-aligned
`clusters/dev-k3s/overlays`.
- Moved the Argo control-plane inventory to
`platform/control-plane/argocd` and separated permission-scoped AppProjects
for addons, shared services, systems and workloads.
- Added a bootstrap-only `gitops-control-plane` AppProject so the root no
longer reconciles through Argo CD's unrestricted `default` project.
- Replaced repeated child Application definitions with strict
list-generated ApplicationSets and explicit `autoSync` bootstrap gates.
- Renamed the Project Auth backing-system namespace to `auth-system-dev` and
aligned service DNS, NetworkPolicy and ConfigMap ownership.
- Changed Vault KV ownership from legacy `dev/platform` paths to
`dev/systems/auth-system` and `dev/workloads/auth-server` paths.
- Split the broad `vault-core` ownership into `vault-foundation` and
`vault-workloads`, retaining `vault-database` as a third isolated state.
- Documented delegated Terraform identities, stage-by-stage bootstrap and
non-destructive state/path migration procedures.
- Kept GHCR as the image artifact boundary and made immutable digests the
promotion target; existing short commit tags remain until a registry-verified
promotion PR replaces them.
- Performed repository-only refactoring and static validation; no Kubernetes,
Vault, Argo CD, registry or remote Terraform backend was mutated.
## 2026-07-25
- Established the internal Gitea repository as the single GitOps source.
+72 -30
View File
@@ -1,74 +1,116 @@
# Intern Guide
# Intern guide
## 먼저 이해할 것
## 이 저장소의 역할
이 저장소서로 다른 세 개의 reconciliation 경계가 있습니다.
이 저장소는 Project Auth를 예제로 한 독립 GitOps reference lab입니다.
애플리케이션 소스나 범용 production platform이 아닙니다. 현재 지원하는
환경은 `dev-k3s` 하나입니다.
1. Gitea가 승인된 desired-state revision을 저장합니다.
서로 다른 세 reconciliation 경계를 먼저 구분합니다.
1. Gitea `main`이 승인된 desired-state revision을 저장합니다.
2. Argo CD가 그 revision의 Kubernetes 리소스를 지속적으로 맞춥니다.
3. Terraform이 승인된 실행 환경에서 Vault API 객체를 관리합니다.
Argo CD가 Terraform을 실행하지 않으며 CI가 routine deployment를 위해
`kubectl apply`를 호출하지 않습니다. secret 값도 Git이나 Terraform을
GHCR은 빌드된 image를 보관할 뿐 desired-state source가 아닙니다. Argo
CD가 Terraform을 실행하지 않으며 CI가 routine deployment를 위해
`kubectl apply`를 호출하지 않습니다. Secret 값도 Git이나 Terraform을
통과하지 않습니다.
## 디렉터리를 고르는 법
- 여러 system이 공유하는 cluster capability: `platform`
- Project Auth bounded context 전용 backing service: `systems/auth-system`
- 별도 source repository에서 빌드하는 서버: `workloads`
- Dev namespace, digest, host, Vault annotation: `clusters/dev-k3s/overlays`
- Vault API 객체: `iac/terraform`
현재 Vault는 platform shared service, PostgreSQL과 Keycloak은 auth-system,
`auth-server``api-server`는 workload입니다. 설치 순서나 중요도로
`foundation`을 만들지 않습니다. 자세한 기준은
`docs/architecture/repository-taxonomy.md`에 있습니다.
## 안전한 변경 흐름
1. `refactor/...`, `feat/...`, `fix/...` 브랜치에서 변경합니다.
1. `refactor/...`, `feat/...`, `fix/...` branch에서 변경합니다.
2. `make validate`를 실행합니다.
3. rendered manifest 또는 Terraform plan을 검토합니다.
3. Rendered manifest 또는 Terraform plan을 검토합니다.
4. 내부 Gitea에 PR을 생성합니다.
5. 승인 후 `main`에 merge합니다.
6. Kubernetes 변경은 Argo CD가 자동 반영합니다.
7. Terraform 변경은 별도 승인 후 실행합니다.
6. Kubernetes 변경은 열린 `autoSync` gate에서 Argo CD가 반영합니다.
7. Terraform 변경은 해당 state identity로 별도 승인 후 실행합니다.
새 ApplicationSet element는 기본적으로 `autoSync: "false"`로 추가합니다.
선행 controller, Vault 구성, secret과 database 준비를 확인한 별도 PR에서
gate를 엽니다. Gate가 닫혀도 수동 Sync는 가능하므로 임의로 누르지
않습니다.
금지 사항:
- `.terraform`, state, plan, tfvars, Vault init JSON, token commit
- 동일 Vault path/resource를 두 state에서 관리
- image promotion 자동화의 `main` 직접 push
- routine CI의 직접 `kubectl apply`
- production skeleton이나 이름뿐인 production Application 추가
- hook을 사용하는 Application에 `ApplyOutOfSyncOnly=true` 적용
- Secret payload를 Terraform resource/data source로 관리
- Image promotion workflow의 `main` 직접 push
- Routine CI 또는 사람의 직접 `kubectl apply`
- Production skeleton이나 이름뿐인 production Application 추가
- Hook을 사용하는 Application에 `ApplyOutOfSyncOnly=true` 적용
- `autoSync: "true"` 전환 PR에서 누적 live diff를 확인하지 않음
## 자주 쓰는 명령
## 자주 쓰는 읽기 전용 명령
최종 dev manifest 렌더링:
```bash
kubectl kustomize clusters/dev-k3s/manifests/auth-server
kubectl kustomize clusters/dev-k3s/manifests/auth-system
kubectl kustomize clusters/dev-k3s/overlays/workloads/auth-server
kubectl kustomize clusters/dev-k3s/overlays/systems/auth-system
kubectl kustomize clusters/dev-k3s/overlays/platform/vault
```
전체 검증:
전체 정적 검증:
```bash
make validate
```
backend 없이 Terraform configuration 검증:
Backend 없이 Terraform configuration 검증:
```bash
terraform -chdir=iac/terraform/live/dev-k3s/vault-core init -backend=false
terraform -chdir=iac/terraform/live/dev-k3s/vault-core validate
for terraform_root in vault-foundation vault-workloads vault-database; do
terraform_data_dir="$(mktemp -d)"
TF_DATA_DIR="$terraform_data_dir" \
terraform -chdir="iac/terraform/live/dev-k3s/${terraform_root}" \
init -backend=false -input=false -lockfile=readonly
TF_DATA_DIR="$terraform_data_dir" \
terraform -chdir="iac/terraform/live/dev-k3s/${terraform_root}" validate
rm -rf "$terraform_data_dir"
done
```
실제 plan:
일반적으로는 같은 검사를 포함한 `make validate`를 사용합니다. 위 예는
provider data를 repository의 `.terraform`에 남기지 않습니다.
실제 plan은 승인된 backend와 identity를 준비한 뒤 수행합니다.
```bash
make terraform-plan \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
TF_ROOT=vault-workloads \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
```
image 승격은 Gitea `Promote Dev Image by Pull Request` workflow에 정확한
`sha256:` digest를 전달합니다. workflow는 전용 브랜치와 PR을 만들며
Image 승격은 Gitea `Promote Dev Image by Pull Request` workflow에 정확한
`sha256:` digest를 전달합니다. Workflow는 전용 branch와 PR을 만들며
`main`에 직접 쓰지 않습니다.
## 읽는 순서
1. `README.md`
2. `docs/architecture/deployment.md`
3. `docs/architecture/secret-trust.md`
4. `docs/adr/`
5. 수행하려는 작업의 runbook
2. `docs/architecture/repository-taxonomy.md`
3. `docs/architecture/deployment.md`
4. `docs/architecture/argocd.md`
5. `docs/architecture/secret-trust.md`
6. `docs/architecture/terraform.md`
7. `docs/adr/`
8. 수행하려는 작업의 runbook
2026-07-26 리팩터링은 repository에서만 구현·검증했으며 실제 cluster에
적용하지 않았습니다. Live migration을 연습 과제로 실행하지 않습니다.
+12 -4
View File
@@ -4,6 +4,7 @@ KUBE_CONTEXT ?=
TF_ROOT ?=
BACKEND_CONFIG ?=
TF_DIR = iac/terraform/live/dev-k3s/$(TF_ROOT)
PLAN_FILE ?= $(CURDIR)/.local/terraform-plans/dev-k3s-$(TF_ROOT).tfplan
bootstrap:
@test -n "$(KUBE_CONTEXT)" || (echo "KUBE_CONTEXT is required" >&2; exit 1)
@@ -17,8 +18,8 @@ vault-init:
check-terraform-inputs:
@case "$(TF_ROOT)" in \
vault-core|vault-database) ;; \
*) echo "TF_ROOT must be vault-core or vault-database" >&2; exit 1 ;; \
vault-foundation|vault-workloads|vault-database) ;; \
*) echo "TF_ROOT must be vault-foundation, vault-workloads, or vault-database" >&2; exit 1 ;; \
esac
@test -f "$(BACKEND_CONFIG)" || (echo "BACKEND_CONFIG must point to a readable backend file" >&2; exit 1)
@@ -26,9 +27,16 @@ terraform-init: check-terraform-inputs
terraform -chdir="$(TF_DIR)" init -input=false -reconfigure -backend-config="$(abspath $(BACKEND_CONFIG))"
terraform-plan: terraform-init
terraform -chdir="$(TF_DIR)" plan -input=false -lock-timeout=5m
@mkdir -p "$(dir $(PLAN_FILE))"
@umask 077; terraform -chdir="$(TF_DIR)" plan \
-input=false \
-lock-timeout=5m \
-out="$(PLAN_FILE)"
terraform-apply: terraform-init
@test "$(APPROVE_APPLY)" = "dev-k3s/$(TF_ROOT)" || \
(echo "Set APPROVE_APPLY=dev-k3s/$(TF_ROOT) to continue" >&2; exit 1)
terraform -chdir="$(TF_DIR)" apply -input=false -lock-timeout=5m
@test -f "$(PLAN_FILE)" || \
(echo "Run terraform-plan first; approved plan is missing: $(PLAN_FILE)" >&2; exit 1)
terraform -chdir="$(TF_DIR)" apply -input=false -lock-timeout=5m "$(PLAN_FILE)"
@rm -f "$(PLAN_FILE)"
+98 -49
View File
@@ -1,78 +1,122 @@
# Project GitOps
# Project GitOps reference lab
Project Auth`dev-k3s` 배포 상태를 관리하는 GitOps configuration
저장소입니다. 배포 기준 저장소는 내부 Gitea 한 곳입니다.
Project Auth를 예제로 사용해 작은 팀의 GitOps 운영 경계를 학습하고 검증하는
독립 reference lab입니다. 범용 사내 플랫폼이나 애플리케이션 소스
monorepo가 아닙니다. 현재 지원 대상은 단일 `dev-k3s` 클러스터뿐입니다.
배포 기준 저장소는 내부 Gitea 한 곳입니다.
```text
https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
```
애플리케이션 소스 저장소는 이미지를 빌드해 GHCR에 올리고, 이 저장소에는
배포할 immutable digest를 변경하는 PR만 생성합니다. Kubernetes 리소스는
Argo CD만 반영하며, Terraform은 Vault API만 관리합니다.
`auth-server``api-server` 소스 저장소는 이미지를 빌드해 GHCR에
올립니다. 정상 promotion 계약은 검증된 immutable digest를 바꾸는 PR입니다.
현재 overlay에는 이관 전의 짧은 commit tag가 남아 있으며, registry를
검증할 credential 없이 임의 digest로 바꾸지 않았습니다. 다음 정상
promotion workflow가 digest로 전환합니다.
Kubernetes desired state는 Argo CD가, Vault API 객체는 Terraform이 각각
관리합니다. KV secret 값은 어느 쪽에도 저장하지 않습니다.
## 지원 범위
| 대상 | 상태 |
|---|---|
| `dev-k3s` | 지원하는 단일 노드 개발 환경 |
| production | 설계되지 않았으며 manifest/Application이 존재하지 않음 |
| Istio | 보류; 향후 ambient mode 후보 |
| production | 설계되지 않았으며 overlay/Application이 음 |
| Istio | 보류; 향후 ambient mode 도입 조건만 기록 |
현재 dev Vault PostgreSQL, ingress는 TLS가 적용되지 않은 개발 프로파일입니다.
production 용도로 사용할 수 없습니다.
현재 Vault, PostgreSQL, ingress는 TLS가 없는 개발 프로파일이며 single-node
failure domain을 공유합니다. production 용도로 사용할 수 없습니다.
2026-07-26의 repository/ownership 리팩터링은 Git 작업 트리에만 설계하고
검증했습니다. 실제 클러스터에는 적용하지 않았으며, 기존 리소스의 소유권
이관은 관련 runbook과 별도 승인 없이 실행하면 안 됩니다.
## 제어 흐름
```text
app CI -> GHCR digest -> GitOps PR -> validation -> main
|
v
Argo CD
|
v
Kubernetes
application CI -> GHCR digest -> GitOps PR -> validation -> main
|
v
Argo CD ApplicationSet
|
v
Kubernetes
IaC PR -> Terraform plan -> approval -> Terraform apply -> Vault API
IaC PR -> Terraform plan -> approval -> one Vault state apply -> Vault API
```
Argo CD 최초 설치와 root Application seed만 클러스터에 직접 적용합니다.
이후 routine deployment는 Git 변경으로만 수행합니다.
Argo CD 최초 설치 뒤 bootstrap 전용 `gitops-control-plane` AppProject와
단일 root Application만 순서대로 직접 적용합니다. 이후 routine deployment는
Git 변경으로만 수행합니다. Terraform은 Argo CD hook이나 Config Management
Plugin 안에서 실행하지 않습니다.
## 저장소 구조
## 저장소 분류
```text
bootstrap/argocd/ 최초 Argo CD 설치 버전과 단일 root Application
bootstrap/argocd/ controller 버전, 제한된 AppProject와 단일 root seed
clusters/dev-k3s/
projects/ AppProject 권한 경계
applications/ foundation, platform, workload child Applications
manifests/ dev-k3s가 실제 소비하는 최종 Kustomize 구성
platform/ Vault와 auth platform의 환경 중립 base
workloads/ auth-server와 api-server의 환경 중립 base
overlays/
platform/ 공유 서비스의 dev-k3s 최종 구성
systems/ bounded context system의 dev-k3s 최종 구성
workloads/ first-party workload의 dev-k3s 최종 구성
platform/
control-plane/argocd/ AppProject와 ApplicationSet inventory
shared-services/ 여러 system이 사용할 수 있는 cluster capability base
systems/ 특정 bounded context가 소유하는 backing system base
workloads/ source repository가 따로 있는 실행 애플리케이션 base
iac/terraform/
modules/ 재사용 Vault 모듈
live/dev-k3s/ vault-core, vault-database state
backend/dev-k3s/ secret을 포함하지 않는 backend 예시
policies/ Terraform이 소비하는 Vault ACL
hack/ bootstrap, validation, 조건부 Vault init entrypoint
docs/ architecture, ADR, recovery/operation runbook
live/dev-k3s/ vault-foundation, vault-workloads, vault-database roots
backend/dev-k3s/ secret 없는 remote backend 예시
policies/vault/ Terraform이 는 Vault ACL 문서
hack/ bootstrap, validation, dev Vault init entrypoint
docs/ architecture, ADR, migration/operation runbook
```
cluster-specific namespace, host, image, secret reference는
`clusters/dev-k3s/manifests/`에서 완결합니다. `platform/``workloads/`
base는 클러스터를 알지 못합니다.
분류는 제품 종류가 아니라 소비자, 소유자, 변경 주기로 결정합니다.
Vault는 공유 capability이므로 `platform/`, Project Auth 전용 PostgreSQL과
Keycloak은 `systems/auth-system/`, 직접 빌드하는 서버는 `workloads/`
속합니다. namespace, host, image digest, Vault role, NetworkPolicy 같은
클러스터별 값은 `clusters/dev-k3s/overlays/`에서 완결합니다.
자세한 판단 기준은
[repository taxonomy](docs/architecture/repository-taxonomy.md)와
[ADR 0007](docs/adr/0007-repository-ownership-boundaries.md)을 따릅니다.
## Argo CD 단계 gate
Root Application은 `platform/control-plane/argocd`의 AppProject와
ApplicationSet을 소유합니다. 각 inventory 항목은 `autoSync`를 quoted
string으로 명시해야 합니다. 새 항목이나 외부 준비 조건이 있는 항목은
`autoSync: "false"`로 시작하고, 선행 controller, Vault 구성, runtime
secret, database 준비를 확인한 PR에서만 `autoSync: "true"`로 바꿉니다.
Control-plane sync wave는 AppProject(`-10`)를 ApplicationSet(`-5`)보다
먼저 생성할 뿐, generated Application의 readiness를 보장하지 않습니다.
`autoSync` gate와 workload의 retry/idempotency가 실제 단계 전환을
담당합니다.
## Terraform state
| Root | 소유 범위 | 실행 권한 |
| Root | 소유 범위 | routine 실행 권한 |
|---|---|---|
| `vault-core` | mounts, auth backends, policies, Kubernetes/JWT roles, app Transit key | 제한된 관리자 |
| `vault-database` | PostgreSQL connection과 dynamic database roles | `vault-database-automation-dev` |
| `vault-foundation` | mounts, auth backends/config, delegated automation policy와 선택적 CI JWT roles | 없음; bootstrap 또는 보안 관리자 승인 실행 |
| `vault-workloads` | workload ACL, Kubernetes auth role, 애플리케이션 Transit key | workload 전용 short-lived identity |
| `vault-database` | PostgreSQL connection과 `auth-db-migration-dev` dynamic role | database 전용 short-lived identity |
Vault API 객체 하나는 정확히 한 state만 소유합니다. KV secret payload,
Vault init JSON, token, PostgreSQL password는 Git이나 Terraform state에
저장하지 않습니다. backend는 암호화, versioning, access control,
locking을 제공해야 합니다.
중요한 규칙은 다음과 같습니다.
- Vault API 객체 하나는 정확히 한 state만 소유합니다.
- Delegated state는 자신에게 권한을 부여하는 policy/login role을 만들지
않습니다. 그것은 `vault-foundation`이 소유합니다.
- State 사이에 `terraform_remote_state`를 사용하지 않습니다.
- KV payload, Vault init JSON, token, PostgreSQL password를 Git이나
Terraform state에 저장하지 않습니다.
- Backend는 encryption, versioning, access control, locking을 제공해야
합니다.
## 주요 명령
@@ -81,20 +125,25 @@ make validate
make bootstrap KUBE_CONTEXT=<expected-context>
make terraform-plan \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
```
`terraform-apply`는 동일 입력과
`terraform-apply`는 동일 입력과
`APPROVE_APPLY=dev-k3s/<root>`가 모두 있어야 실행되며 `auto-approve`
사용하지 않습니다.
사용하지 않습니다. 실제 bootstrap과 state 이동은 먼저 runbook의 중단
조건을 확인합니다.
## 문서
- [빈 dev 클러스터 bootstrap](docs/runbooks/dev-bootstrap.md)
- [Repository taxonomy](docs/architecture/repository-taxonomy.md)
- [배포 구조](docs/architecture/deployment.md)
- [secret trust boundary](docs/architecture/secret-trust.md)
- [Terraform v2 state 이관](docs/runbooks/terraform-state-migration.md)
- [Argo CD 구조와 stage gate](docs/architecture/argocd.md)
- [Secret trust boundary](docs/architecture/secret-trust.md)
- [Terraform 사용 경계](docs/architecture/terraform.md)
- [빈 dev 클러스터 bootstrap](docs/runbooks/dev-bootstrap.md)
- [Application 안전한 decommission](docs/runbooks/application-decommission.md)
- [Terraform state 이관](docs/runbooks/terraform-state-migration.md)
- [Vault backup/recovery](docs/runbooks/vault-backup-restore.md)
- [Sealed Secrets recovery](docs/runbooks/sealed-secrets-recovery.md)
- [입문 가이드](INTERN_GUIDE.md)
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: gitops-control-plane
namespace: argocd
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Bootstrap-only boundary for the project-gitops control plane
sourceRepos:
- https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- namespace: argocd
server: https://kubernetes.default.svc
namespaceResourceWhitelist:
- group: argoproj.io
kind: AppProject
- group: argoproj.io
kind: ApplicationSet
orphanedResources:
warn: true
@@ -2,6 +2,5 @@ apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- foundation
- platform
- workloads
- control-plane-project.yaml
- root-application.yaml
+3 -3
View File
@@ -1,14 +1,14 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: project-gitops-dev-k3s
name: project-gitops-control-plane
namespace: argocd
spec:
project: default
project: gitops-control-plane
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: clusters/dev-k3s
path: platform/control-plane/argocd
destination:
server: https://kubernetes.default.svc
namespace: argocd
+32
View File
@@ -0,0 +1,32 @@
# dev-k3s cluster profile
`dev-k3s`는 이 reference lab이 현재 지원하는 유일한 cluster profile입니다.
ApplicationSet의 `server`는 in-cluster API
`https://kubernetes.default.svc`를 사용합니다.
이 디렉터리는 base 복사본이 아니라 다음 cluster-specific composition만
소유합니다.
- `platform/vault`: dev Vault namespace, NetworkPolicy와 single-node profile
- `systems/auth-system`: `auth-system-dev` namespace, internal/public host,
Vault injection path와 system NetworkPolicy
- `workloads/*`: dev namespace, image reference, ingress, pull
SealedSecret과 workload NetworkPolicy
현재 Vault NetworkPolicy의 Kubernetes API CIDR와 node address는
`dev-k3s`에 종속됩니다. 다른 클러스터에 그대로 복사하지 말고 해당
클러스터의 service/node network를 확인해야 합니다.
두 번째 클러스터를 추가할 때는 다음 순서를 사용합니다.
1. 실제 차이가 있는 overlay만 `clusters/<cluster>/overlays`에 추가합니다.
2. Argo CD cluster credential을 Git 밖에서 등록합니다.
3. 각 AppProject destination에 정확한 API server/namespace를 추가합니다.
4. 권한별 ApplicationSet inventory에 `autoSync: "false"` element를
추가합니다.
5. Render, live diff와 외부 prerequisite를 검증한 단계별 PR로 gate를
엽니다.
Production은 이 profile의 이름 변경이나 복사로 만들지 않습니다. TLS,
availability, secret authority, storage/backup, approval과 recovery
contract를 먼저 별도 설계해야 합니다.
@@ -1,7 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- sealed-secrets.yaml
- vault.yaml
- vault-agent-injector.yaml
@@ -1,49 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: sealed-secrets-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "0"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: cluster-addons-dev
source:
repoURL: https://bitnami.github.io/sealed-secrets
chart: sealed-secrets
targetRevision: 2.17.9
helm:
values: |
fullnameOverride: sealed-secrets-controller
keyrenewperiod: 720h
image:
repository: bitnami/sealed-secrets-controller
tag: "0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
destination:
server: https://kubernetes.default.svc
namespace: kube-system
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,63 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault-agent-injector-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "2"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: cluster-addons-dev
source:
repoURL: https://helm.releases.hashicorp.com
chart: vault
targetRevision: 0.32.0
helm:
values: |
global:
externalVaultAddr: http://vault.vault.svc.cluster.local:8200
tlsDisable: true
server:
enabled: false
injector:
enabled: true
authPath: auth/kubernetes
image:
repository: hashicorp/vault-k8s
tag: "1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"
webhook:
failurePolicy: Fail
namespaceSelector:
matchLabels:
vault-injection: enabled
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
agentImage:
repository: hashicorp/vault
tag: "1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"
destination:
server: https://kubernetes.default.svc
namespace: vault
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "1"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
project: platform-dev
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: clusters/dev-k3s/manifests/vault
destination:
server: https://kubernetes.default.svc
namespace: vault
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: platform-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
project: platform-dev
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: clusters/dev-k3s/manifests/auth-system
destination:
server: https://kubernetes.default.svc
namespace: platform
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,5 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- auth-system.yaml
@@ -1,36 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-server-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "20"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: clusters/dev-k3s/manifests/api-server
destination:
server: https://kubernetes.default.svc
namespace: api-dev
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,36 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: auth-server-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "20"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: clusters/dev-k3s/manifests/auth-server
destination:
server: https://kubernetes.default.svc
namespace: auth-dev
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,6 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- auth-server.yaml
- api-server.yaml
@@ -4,6 +4,6 @@ kind: Kustomization
namespace: vault
resources:
- ../../../../platform/security/vault/base
- ../../../../../platform/shared-services/vault/base
- namespace.yaml
- networkpolicy.yaml
@@ -3,7 +3,7 @@ kind: Namespace
metadata:
name: vault
annotations:
argocd.argoproj.io/sync-options: Prune=false
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
@@ -48,7 +48,7 @@ spec:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform
kubernetes.io/metadata.name: auth-system-dev
podSelector:
matchLabels:
app: postgres
@@ -97,7 +97,7 @@ spec:
kubernetes.io/metadata.name: auth-dev
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform
kubernetes.io/metadata.name: auth-system-dev
ports:
- protocol: TCP
port: 8200
@@ -9,13 +9,13 @@ spec:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-keycloak-sync-env: "0644"
vault.hashicorp.com/agent-inject-secret-keycloak-sync-env: kv/data/dev/platform/keycloak/bootstrap-admin
vault.hashicorp.com/agent-inject-secret-keycloak-sync-env: kv/data/dev/systems/auth-system/keycloak/bootstrap-admin
vault.hashicorp.com/agent-inject-template-keycloak-sync-env: |
{{ with secret "kv/data/dev/platform/keycloak/bootstrap-admin" }}
{{ with secret "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" }}
export KC_BOOTSTRAP_ADMIN_PASSWORD={{ printf "%q" .Data.data.KC_BOOTSTRAP_ADMIN_PASSWORD }}
{{ end }}
{{ with secret "kv/data/dev/platform/keycloak/client-auth-server" }}
{{ with secret "kv/data/dev/workloads/auth-server/keycloak-client" }}
export KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.KEYCLOAK_CLIENT_SECRET }}
{{ end }}
vault.hashicorp.com/agent-pre-populate-only: "true"
@@ -44,7 +44,7 @@ spec:
ready=false
for _ in $(seq 1 60); do
if /opt/keycloak/bin/kcadm.sh config credentials \
--server http://keycloak.platform.svc.cluster.local \
--server http://keycloak \
--realm master \
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; then
@@ -72,15 +72,15 @@ spec:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
- name: KEYCLOAK_CLIENT_ID
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_CLIENT_ID
- name: AUTH_SERVER_BASE_URL
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: AUTH_SERVER_BASE_URL
@@ -7,7 +7,7 @@ metadata:
spec:
ingressClassName: traefik
rules:
- host: keycloak-public.platform.svc.cluster.local
- host: keycloak-public.auth-system-dev.svc.cluster.local
http:
paths:
- path: /
@@ -9,7 +9,7 @@ spec:
- name: keycloak
env:
- name: KC_HOSTNAME
value: keycloak-public.platform.svc.cluster.local
value: keycloak-public.auth-system-dev.svc.cluster.local
- name: KC_HOSTNAME_STRICT
value: "false"
- name: KC_PROXY_HEADERS
@@ -9,13 +9,13 @@ spec:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-keycloak-env: "0644"
vault.hashicorp.com/agent-inject-secret-keycloak-env: kv/data/dev/platform/postgres/keycloak
vault.hashicorp.com/agent-inject-secret-keycloak-env: kv/data/dev/systems/auth-system/postgres/keycloak
vault.hashicorp.com/agent-inject-template-keycloak-env: |
{{ with secret "kv/data/dev/platform/postgres/keycloak" }}
{{ with secret "kv/data/dev/systems/auth-system/postgres/keycloak" }}
export KC_DB_PASSWORD={{ printf "%q" .Data.data.KEYCLOAK_DB_PASSWORD }}
{{ end }}
{{ with secret "kv/data/dev/platform/keycloak/bootstrap-admin" }}
{{ with secret "kv/data/dev/systems/auth-system/keycloak/bootstrap-admin" }}
export KC_BOOTSTRAP_ADMIN_PASSWORD={{ printf "%q" .Data.data.KC_BOOTSTRAP_ADMIN_PASSWORD }}
{{ end }}
vault.hashicorp.com/role: keycloak-dev
@@ -44,16 +44,16 @@ spec:
- name: KC_DB
value: postgres
- name: KC_DB_URL
value: jdbc:postgresql://postgres.platform.svc.cluster.local:5432/keycloak
value: jdbc:postgresql://postgres:5432/keycloak
- name: KC_DB_USERNAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_DB_USER
- name: KC_HEALTH_ENABLED
value: "true"
- name: KC_BOOTSTRAP_ADMIN_USERNAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
@@ -1,10 +1,10 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: platform
namespace: auth-system-dev
resources:
- ../../../../platform/auth-system/base
- ../../../../../systems/auth-system/base
- namespace.yaml
- keycloak-ingress.yaml
- public-access.yaml
@@ -15,7 +15,7 @@ generatorOptions:
argocd.argoproj.io/sync-wave: "0"
configMapGenerator:
- name: platform-config
- name: auth-system-config
envs:
- config.env
@@ -1,9 +1,9 @@
apiVersion: v1
kind: Namespace
metadata:
name: platform
name: auth-system-dev
annotations:
argocd.argoproj.io/sync-options: Prune=false
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
labels:
vault-injection: enabled
pod-security.kubernetes.io/enforce: baseline
@@ -1,7 +1,7 @@
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-default-deny
name: auth-system-default-deny
spec:
podSelector: {}
policyTypes:
@@ -11,7 +11,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-allow-dns-egress
name: auth-system-allow-dns-egress
spec:
podSelector: {}
policyTypes:
@@ -33,7 +33,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-allow-vault-egress
name: auth-system-allow-vault-egress
spec:
podSelector: {}
policyTypes:
@@ -53,7 +53,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-allow-keycloak-egress-to-postgres
name: auth-system-allow-keycloak-egress-to-postgres
spec:
podSelector:
matchLabels:
@@ -72,7 +72,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-allow-keycloak-client-sync-to-keycloak
name: auth-system-allow-keycloak-client-sync-to-keycloak
spec:
podSelector:
matchLabels:
@@ -91,7 +91,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-allow-keycloak-ingress
name: auth-system-allow-keycloak-ingress
spec:
podSelector:
matchLabels:
@@ -120,7 +120,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: platform-allow-postgres-ingress
name: auth-system-allow-postgres-ingress
spec:
podSelector:
matchLabels:
@@ -9,17 +9,17 @@ spec:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-postgres-env: "0644"
vault.hashicorp.com/agent-inject-secret-postgres-env: kv/data/dev/platform/postgres/superuser
vault.hashicorp.com/agent-inject-secret-postgres-env: kv/data/dev/systems/auth-system/postgres/superuser
vault.hashicorp.com/agent-inject-template-postgres-env: |
{{ with secret "kv/data/dev/platform/postgres/superuser" }}
{{ with secret "kv/data/dev/systems/auth-system/postgres/superuser" }}
export POSTGRES_PASSWORD={{ printf "%q" .Data.data.POSTGRES_SUPERUSER_PASSWORD }}
{{ end }}
{{ with secret "kv/data/dev/platform/postgres/auth-server" }}
{{ with secret "kv/data/dev/systems/auth-system/postgres/auth-server" }}
export AUTH_DB_PASSWORD={{ printf "%q" .Data.data.AUTH_DB_PASSWORD }}
{{ end }}
{{ with secret "kv/data/dev/platform/postgres/keycloak" }}
{{ with secret "kv/data/dev/systems/auth-system/postgres/keycloak" }}
export KEYCLOAK_DB_PASSWORD={{ printf "%q" .Data.data.KEYCLOAK_DB_PASSWORD }}
{{ end }}
vault.hashicorp.com/role: postgres-dev
@@ -50,30 +50,30 @@ spec:
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: POSTGRES_SUPERUSER
- name: POSTGRES_DB
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: POSTGRES_DEFAULT_DB
- name: AUTH_DB_NAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: AUTH_DB_NAME
- name: AUTH_DB_USER
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: AUTH_DB_USER
- name: KEYCLOAK_DB_NAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_DB_NAME
- name: KEYCLOAK_DB_USER
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_DB_USER
@@ -4,7 +4,7 @@ kind: Kustomization
namespace: api-dev
resources:
- ../../../../workloads/api-server/base
- ../../../../../workloads/api-server/base
- namespace.yaml
- ingress.yaml
- public-access.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: Namespace
metadata:
name: api-dev
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
@@ -3,9 +3,9 @@ SERVER_FORWARD_HEADERS_STRATEGY=framework
APP_DOCS_TITLE=Project Auth Server API
APP_DOCS_DESCRIPTION=dev auth-server OpenAPI
APP_DOCS_VERSION=v1
APP_DATASOURCE_URL=jdbc:postgresql://postgres.platform.svc.cluster.local:5432/project_auth
APP_DATASOURCE_URL=jdbc:postgresql://postgres.auth-system-dev.svc.cluster.local:5432/project_auth
APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP=false
APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI=http://keycloak-public.platform.svc.cluster.local/realms/project-auth
APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI=http://keycloak-public.auth-system-dev.svc.cluster.local/realms/project-auth
APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_ID=project-auth-server
APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID=keycloak-google
APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT=google
@@ -9,14 +9,14 @@ spec:
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/platform/postgres/auth-server
vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/systems/auth-system/postgres/auth-server
vault.hashicorp.com/agent-inject-template-runtime-env: |
{{ with secret "kv/data/dev/platform/postgres/auth-server" }}
{{ with secret "kv/data/dev/systems/auth-system/postgres/auth-server" }}
export APP_DATASOURCE_USERNAME={{ printf "%q" .Data.data.APP_DATASOURCE_USERNAME }}
export APP_DATASOURCE_PASSWORD={{ printf "%q" .Data.data.APP_DATASOURCE_PASSWORD }}
{{ end }}
{{ with secret "kv/data/dev/platform/keycloak/client-auth-server" }}
{{ with secret "kv/data/dev/workloads/auth-server/keycloak-client" }}
export APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET }}
{{ end }}
vault.hashicorp.com/agent-inject-token: "true"
@@ -4,7 +4,7 @@ kind: Kustomization
namespace: auth-dev
resources:
- ../../../../workloads/auth-server/base
- ../../../../../workloads/auth-server/base
- namespace.yaml
- ingress.yaml
- public-access.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: Namespace
metadata:
name: auth-dev
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
labels:
vault-injection: enabled
pod-security.kubernetes.io/enforce: baseline
@@ -33,7 +33,7 @@ spec:
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: auth-dev-allow-platform-and-vault-egress
name: auth-dev-allow-auth-system-and-vault-egress
spec:
podSelector: {}
policyTypes:
@@ -42,7 +42,7 @@ spec:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: platform
kubernetes.io/metadata.name: auth-system-dev
podSelector:
matchLabels:
app: postgres
@@ -1,7 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- apps.yaml
- cluster-addons.yaml
- platform.yaml
+46 -15
View File
@@ -2,23 +2,54 @@
Status: accepted
Terraform은 VM/네트워크뿐 아니라 provider가 제공되는 Vault API 객체도
관리할 수 있다. 현재 저장소의 Terraform 범위는 Vault API이고 실제
machine provisioning은 provider가 확정될 때 별도 root로 추가한다.
Updated: 2026-07-26
`dev-k3s`는 두 state만 사용한다.
Terraform의 현재 범위는 Vault API 객체입니다. Kubernetes 리소스는 Argo
CD가 소유하며, machine/network provisioning은 provider와 운영 경계가
확정될 때 별도 root로 추가합니다.
- `vault-core`: mounts, auth backends, policies, Kubernetes/JWT roles,
application Transit key
- `vault-database`: PostgreSQL connection과 dynamic roles
`dev-k3s`는 정확히 세 state를 사용합니다.
resource/API path 하나는 한 state에만 속한다. state는 암호화, versioning,
access control, locking이 가능한 remote backend에 저장한다.
| State | 소유 객체 |
|---|---|
| `vault-foundation` | KV/database/Transit mounts, Kubernetes auth backend/config, delegated automation policy와 선택적 분리 CI JWT auth role |
| `vault-workloads` | workload ACL policy, Kubernetes auth role, `project-auth-jwt` Transit key |
| `vault-database` | `database/config/auth-system-postgres-dev` connection과 `auth-db-migration-dev` dynamic role |
`vault-core`는 privilege-escalation 가능한 객체를 포함하므로 제한된
관리자 실행만 허용한다. `vault-database`는 core가 생성한
`vault-database-automation-dev` 정책의 short-lived identity로 실행한다.
Resource 또는 Vault API path 하나는 한 state에만 속합니다. State 사이는
이름 contract와 실행 순서만 공유하며 `terraform_remote_state`로 서로의
snapshot을 읽지 않습니다. Backend는 encryption, versioning, access
control, locking을 제공해야 합니다.
Secret payload는 Terraform resource/data source로 관리하지 않는다.
필수 credential은 ephemeral variable과 provider write-only argument를
통해서만 apply에 전달한다.
Privilege delegation의 경계는 다음과 같습니다.
- `vault-foundation`은 bootstrap 또는 보안 관리자 승인 때만 실행합니다.
Routine CI identity를 두지 않습니다.
- `vault-foundation`이 workload/database 전용 automation policy와,
OIDC/JWT trust가 검증된 경우 서로 분리된 CI JWT login role을 생성합니다.
두 role의 exact claim map은 최소 한 공통 discriminator key에서 서로 다른
값을 가져야 하므로 동일 scalar-claim JWT가 둘 다 선택할 수 없습니다.
- `vault-workloads``vault-database`는 각각의 short-lived identity를
소비할 뿐 자신에게 권한을 부여하는 객체를 소유하지 않습니다.
- Delegated identity는 자신이 맡은 정확한 policy, auth role, database
path만 CRUD할 수 있습니다.
- Broad `platform-admin` 또는 상시 cluster-internal Vault administrator를
routine automation에 연결하지 않습니다.
실제 CI issuer가 repository, protected ref와 job discriminator claim을
어떤 형식으로 발행하는지 먼저 검증합니다. 그 계약을 확인할 수 없으면 JWT
auth를 활성화하지 않고 bootstrap용 short-lived token만 사용합니다.
Foundation의 future change는 routine identity가 아니라 encrypted unseal
custody를 사용한 승인된 generated-root ceremony가 필요합니다.
Secret payload는 Terraform resource/data source로 관리하지 않습니다.
Provider token과 PostgreSQL credential은 ephemeral variable과 write-only
argument를 통해 실행 시점에만 전달합니다. Vault init material, token,
password, plan과 state를 Git에 저장하지 않습니다.
기존 `vault-core`에서 세 state로 바꾸는 작업은 선언 이동과 state ownership
이관을 분리해 수행합니다. Source에서는 `removed { destroy = false }`,
destination에서는 import를 사용하고, 양쪽 plan의 destroy가 0인지 확인하기
전에는 apply하지 않습니다. Broad `platform-admin`/`vault-operator`
미사용 Keycloak/PostgreSQL operator policy/role은 새 state로 옮기지
않으며 consumer가 없음을 확인한 별도 decommission에서 제거합니다.
+37 -12
View File
@@ -1,18 +1,43 @@
# ADR 0004: Single Argo CD root
# ADR 0004: Single Argo CD root and stage-gated ApplicationSets
Status: accepted
Argo CD 설치 후 `bootstrap/argocd/root-application.yaml` 하나만 seed한다.
root는 `clusters/dev-k3s`의 AppProject와 모든 child Application을 소유한다.
Updated: 2026-07-26
반복 `kubectl apply`와 foundation/platform/application별 root wrapper는
제거한다. routine deployment는 Git merge만으로 시작한다.
Argo CD 설치 후 bootstrap 전용 `gitops-control-plane` AppProject와 단일
root Application을 순서대로 수동 seed합니다. Root는
`platform/control-plane/argocd`의 AppProject와 ApplicationSet을 소유하고,
ApplicationSet이 platform addon/shared service, system, workload
Application을 생성합니다. Bootstrap Project는 canonical repository,
`argocd` namespace와 AppProject/ApplicationSet kind만 허용합니다.
Child Application의 sync wave는 객체 생성 순서를 가독성 있게 표현하지만
서로 다른 Application의 readiness dependency로 간주하지 않는다.
Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할 수 있어야 한다.
반복 `kubectl apply`와 category별 root wrapper는 사용하지 않습니다.
Routine deployment는 Git merge만으로 시작합니다.
Root가 child Application을 prune하거나 삭제하려면 확인이 필요하다.
shared resource 소유권 충돌은 sync를 실패시킨다. 현재 규모에서는 명시적
Application을 사용하고 두 번째 클러스터가 생길 때 ApplicationSet을
검토한다.
ApplicationSet inventory 항목은 다음 계약을 명시합니다.
- 고유 이름, AppProject, source, destination namespace
- component/cluster/path와 automated reconciliation 허용 여부인 quoted
string `autoSync`
새 항목과 외부 준비 조건이 있는 항목은 `autoSync: "false"`로 시작합니다.
현재 bootstrap은 Sealed Secrets와 Vault만 열린 상태에서 시작해
foundation/workloads state와 secret seed, injector, auth-system, database,
first-party workload 순서로 별도 PR gate를 엽니다. Template은
`autoSync: "true"`인 항목에만 automated sync, prune, self-heal을
생성합니다.
Gate가 닫힌 Application의 수동 sync도 change record와 명시적 operator
판단을 요구합니다.
Sync wave는 AppProject(`-10`)를 ApplicationSet(`-5`)보다 먼저 생성합니다.
모든 ApplicationSet은 같은 wave이며 element별 stage field는 없습니다.
서로 다른 generated Application의 readiness는 gate와 runbook이
제어합니다. Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할
수 있고 idempotent해야 합니다.
Root는 AppProject와 ApplicationSet만 prune 대상으로 봅니다. Generated
Application의 owner는 ApplicationSet이며 `create-update`에서는 element
제거만으로 삭제되지 않습니다. Application/resource 해체는 별도
decommission runbook과 확인 승인을 사용합니다. Shared resource 소유권
충돌은 sync를 실패시킵니다. Sync hook을 사용하는 Application에는
`ApplyOutOfSyncOnly=true`를 사용하지 않습니다.
+24 -9
View File
@@ -2,16 +2,31 @@
Status: accepted
현재는 하나의 platform 팀, 하나의 dev cluster와 소수 workload를 가지므로
GitOps configuration monorepo를 유지한다. application source repository와
deployment configuration repository는 분리한다.
Updated: 2026-07-26
- `platform/`, `workloads/`: 환경 중립 base
- `clusters/<cluster>/manifests`: cluster-specific final composition
- `clusters/<cluster>/applications`: Argo reconciliation inventory
현재는 단일 `dev-k3s`와 소수 workload를 다루므로 GitOps configuration
monorepo를 유지합니다. Application source repository와 deployment
configuration repository는 분리합니다. 이 저장소 자체는 독립 reference
lab이며 범용 platform product로 간주하지 않습니다.
- `platform/`, `systems/`, `workloads/`: ownership별 base; 환경 중립은
목표 contract
- `clusters/<cluster>/overlays`: cluster-specific final composition
- `platform/control-plane/argocd/projects`: Argo 권한 경계
- `platform/control-plane/argocd/application-sets`: reconciliation inventory
- `iac/terraform`: Kubernetes manifest와 분리된 external API IaC
- `bootstrap`: controller가 존재하기 전의 최소 seed
production 접근권한, 소유 팀, Terraform backend 또는 release cadence가
실제로 갈라질 때 platform GitOps, workload GitOps, IaC repo 분리를
재검토한다. 존재하지 않는 환경의 skeleton은 유지하지 않는다.
`clusters`가 배포 가능한 최종 상태를 소유합니다. Argo CD는 top-level
base를 직접 source로 사용하지 않습니다. `foundation`은 directory
taxonomy가 아니라 bootstrap ordering/stage이고, 구체적인 ownership 분류는
ADR 0007을 따릅니다.
현재 Keycloak base의 `start-dev`와 Vault base의
TLS-off/single-node identity는 이 contract를 위반하는 알려진 리팩터링
부채입니다. 다른 환경을 추가하기 전에 해당 값을 component overlay나
configuration input으로 분리합니다.
Production 접근권한, 소유 팀, Terraform backend 또는 release cadence가
실제로 갈라질 때 platform GitOps, workload GitOps, IaC repository 분리를
재검토합니다. 존재하지 않는 환경의 skeleton은 유지하지 않습니다.
@@ -0,0 +1,65 @@
# ADR 0007: Repository ownership boundaries
Status: accepted
Date: 2026-07-26
## Context
기존 layout은 Vault, PostgreSQL, Keycloak과 Project Auth 구성을 모두
`platform` 또는 `foundation`으로 표현했습니다. 이 이름은 설치 순서를
보여 주지만 누가 소비하고 변경을 책임지는지 구분하지 못했습니다.
클러스터별 최종 구성도 `manifests`라는 일반 이름 아래 섞여 있어 base와
overlay의 관계가 불명확했습니다.
이 저장소는 하나의 실제 사내 플랫폼을 배포하는 저장소가 아니라 Project
Auth를 예제로 한 독립 GitOps reference lab입니다. 따라서 존재하지 않는
팀/환경을 가정한 추상화보다 현재 리소스의 실제 owner와 lifecycle을
명확히 해야 합니다.
## Decision
최상위 Kubernetes desired state를 다음 소유권으로 분류합니다.
- `platform`: 여러 system이 사용할 수 있고 독립 lifecycle을 가진 cluster
capability
- `systems`: 특정 bounded context가 소유하는 backing services와 domain
configuration
- `workloads`: 별도 source repository와 release digest를 가진 first-party
실행 애플리케이션
- `clusters/<cluster>/overlays`: 위 base에 namespace, image, host, secret
reference, network boundary를 결합한 최종 구성
Vault는 `platform/shared-services/vault`에 둡니다. Sealed Secrets와 Vault
Agent Injector는 cluster addon inventory로 관리합니다. PostgreSQL,
Keycloak, realm/client sync는 Project Auth 전용이므로
`systems/auth-system`으로 이동합니다. `auth-server``api-server`
`workloads`에 유지합니다.
Project Auth backing system의 namespace는 `auth-system-dev`로 정하고,
Vault KV 경로도 `systems/auth-system` 또는 실제 workload owner를
반영하도록 바꿉니다.
`foundation`은 ownership directory로 사용하지 않습니다. 준비 순서는
분리된 ApplicationSet category, 명시적 `autoSync` gate, runbook과 workload
retry/idempotency로 표현합니다.
## Consequences
- 디렉터리 경로만 보고 owner와 blast radius를 추론할 수 있습니다.
- Base는 환경 중립 contract를 목표로 하고 Argo CD는 cluster overlay만
source로 사용합니다. 현재 Keycloak/Vault base의 dev-only 값은 알려진
후속 리팩터링 대상입니다.
- auth-system 이동은 namespace, DNS, NetworkPolicy, Vault policy/path와
Terraform role binding을 함께 바꾸는 migration입니다. 단순 파일 이동으로
취급하면 안 됩니다.
- ApplicationSet 도입은 반복 YAML을 줄이지만 각 파일에 project를 고정하고
Git 항목에는 component, cluster, destination, path와 quoted `autoSync`
명시하도록 요구합니다. Git revision은 template의 `main`으로 고정합니다.
- 새 capability가 공용인지 system 전용인지 애매하면 소비자 수, owner,
release cadence가 분리되는지를 먼저 검토합니다.
- 실제 production 요구가 생기기 전에는 production skeleton을 만들지
않습니다.
세부 path와 예시는
`docs/architecture/repository-taxonomy.md`를 따른다.
+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)
+71
View File
@@ -0,0 +1,71 @@
# ApplicationSet decommission
이 절차는 `applicationsSync: create-update`를 사용하는 generated
Application을 안전하게 해체하기 위한 runbook입니다. List element를 지우는
것만으로 Application이 삭제되지 않는 것은 오류가 아니라 삭제 보호
동작입니다.
## 중단 조건
다음 중 하나라도 만족하면 진행하지 않습니다.
- 대상 Application 이름, ApplicationSet, 클러스터가 명확하지 않음
- 최신 backup과 복구 테스트가 없음
- PVC/PV와 StorageClass의 reclaim policy를 확인하지 않음
- Application에 예상하지 못한 finalizer가 있음
- live-to-target diff에 대상 밖 리소스가 포함됨
- `argocd-cmd-params-cm`의 전역 ApplicationSet policy가 저장소의
`create-update` 의도를 덮어쓰는지 확인하지 않음
Generated Application template에는 resource finalizer를 두지 않습니다.
`preserveResourcesOnDeletion: true`도 유지합니다. ApplicationSet 전체를
삭제해서 개별 component를 해체하지 않습니다.
## 공통 준비
1. 대상 element의 `autoSync``false`로 바꾸는 PR을 먼저 병합합니다.
2. 비활성 기간에 누적된 live-to-target 전체 diff를 저장합니다.
3. 대상이 stateful이면 application-level backup과 restore test를
완료합니다.
4. List element를 제거하는 별도 PR을 병합합니다. `create-update` 정책
때문에 기존 Application은 의도적으로 남아야 합니다.
5. 남은 Application의 소유 관계와 finalizer를 확인합니다.
```bash
kubectl -n argocd get application <application-name> \
-o json |
jq '{ownerReferences: .metadata.ownerReferences, finalizers: (.metadata.finalizers // [])}'
```
예상하지 못한 finalizer를 강제로 제거하지 않습니다.
## 리소스를 보존하고 관리만 중단
List element를 제거한 뒤 generated Application이 더 이상 재생성되지 않는
것을 확인합니다. Template에 resource finalizer가 없으므로 Application
객체 삭제는 workload를 orphan으로 남깁니다.
```bash
kubectl -n argocd delete application <application-name>
```
삭제 후 workload가 그대로 존재하고 Argo CD에 다시 나타나지 않는지
확인합니다. 보존된 리소스는 더 이상 drift correction을 받지 않으므로,
다른 소유자에게 즉시 인계하거나 별도 정리 계획을 기록합니다.
## 리소스까지 제거
리소스 삭제는 List element 제거와 같은 PR에 섞지 않습니다.
1. 대상 Application은 inventory에 남기고 `autoSync: "false"` 상태를
유지합니다.
2. 별도 PR에서 component의 desired state를 해체용 빈 구성으로 바꿉니다.
3. Argo CD diff에서 삭제 대상이 정확한지 검토합니다.
4. Namespace, PVC 등 `Prune=confirm,Delete=confirm` 대상의 backup과
reclaim policy를 다시 확인하고 승인된 prune을 수동 실행합니다.
5. 리소스가 제거된 뒤 List element 제거 PR을 병합합니다.
6. 남은 Application 객체를 삭제합니다.
승인 시각 annotation을 자동화하거나 우회하지 않습니다. `kubectl delete
applicationset` 및 finalizer 강제 제거는 복구 runbook과 별도 승인 없이는
사용하지 않습니다.
+203 -71
View File
@@ -1,7 +1,11 @@
# Bootstrap an empty dev-k3s cluster
이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. production
사용하지 않습니다.
이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. Production
기존 live cluster migration에는 사용하지 않습니다.
> 2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다.
> 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이
> 아닙니다.
## 1. Preflight
@@ -11,38 +15,59 @@ kubectl cluster-info
make validate
```
의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo CD가
root repository를 읽을 수 있는 read-only credential을 외부 secret
authority에서 먼저 provision해야 합니다. credential은 이 저장소에
의도한 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo
CD가 root repository를 읽을 수 있는 read-only credential을 외부 secret
authority에서 먼저 provision해야 합니다. Credential은 이 저장소에
commit하지 않습니다.
remote state backend 파일을 준비합니다.
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-foundation.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
cp iac/terraform/backend/dev-k3s/vault-workloads.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
cp iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-database.s3.hcl
```
실제 bucket, endpoint와 workload identity를 설정합니다. backend credential은
파일에 넣지 않습니다.
실제 bucket, endpoint와 workload identity를 설정합니다. Backend
credential은 파일에 넣지 않습니다. 세 backend key가 서로 다르고 locking이
활성화됐는지 확인합니다.
## 2. Argo CD와 root Application
```bash
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
kubectl -n argocd get application project-gitops-dev-k3s
kubectl -n argocd get appproject gitops-control-plane
kubectl -n argocd get application project-gitops-control-plane
kubectl -n argocd get applicationsets
```
이 명령이 수행하는 직접 cluster mutation은 Argo CD 설치와 root seed뿐입니다.
Child Application은 root가 생성합니다.
직접 cluster mutation은 pinned Argo CD 설치, 제한된
`gitops-control-plane` AppProject, root Application seed뿐입니다.
Bootstrap script는 이 순서를 지킵니다. Root가 네 AppProject와 네
ApplicationSet을 만들고, ApplicationSet이 child Application을 생성합니다.
초기 inventory에서 Sealed Secrets와 Vault만 `autoSync: "true"`입니다.
Vault Agent Injector, `auth-system`, `auth-server`, `api-server` gate는
닫힌 상태여야 합니다.
### Sealed Secrets key 준비
Checked-in `ghcr-regcred` ciphertext는 암호화에 사용한 controller private
key로만 복호화할 수 있습니다. 기존 key backup이 있으면 workload gate를
열기 전에 복원하고, 없으면 새 controller certificate와 원본 credential
authority를 사용해 두 SealedSecret을 다시 seal한 PR을 merge합니다.
[Sealed Secrets recovery runbook](sealed-secrets-recovery.md)을 따르며 평문
GHCR credential을 Git이나 log에 남기지 않습니다.
## 3. Dev Vault 초기화
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 forward합니다.
port-forward는 최초 dev bootstrap용이며 routine runner 모델이 아닙니다.
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서
port-forward합니다. 이는 최초 dev bootstrap용이며 routine runner 모델이
아닙니다.
```bash
kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
@@ -56,13 +81,14 @@ export VAULT_ADDR=http://127.0.0.1:8200
./hack/vault-init.sh init
```
`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody 복사합니다.
dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과
분리합니다.
`.local/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 core
## 4. Vault foundation
초기 root token을 shell history에 직접 적지 않습니다.
Foundation은 초기 root token으로 한 번 적용합니다.
```bash
export TF_VAR_vault_addr="$VAULT_ADDR"
@@ -71,24 +97,89 @@ export TF_VAR_vault_token="$(
)"
make terraform-plan \
TF_ROOT=vault-core \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.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
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-foundation
```
plan에 mount, auth backend, policy, role, JWT Transit key 이외 객체가
나오면 apply하지 않습니다.
Plan에 mounts, auth configuration, workloads/database automation
policy와, OIDC/JWT를 명시적으로 구성한 경우에만 분리된 CI JWT role이
있어야 합니다. Workload runtime policy, workload Kubernetes role,
application Transit key, database connection이 보이면 중단합니다.
## 5. Runtime secret seed
CI JWT를 구성할 때 workloads/database exact claim map은 repository와
protected ref를 묶고, 최소 한 공통 job discriminator key에 서로 다른 값을
가져야 합니다. 실제 issuer token payload로 그 claim을 확인하지 못하면
OIDC/JWT 입력을 비워 둡니다.
Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 terminal
session에만 유지합니다.
CI JWT auth를 구성했다면 Foundation이 만든 두 delegated identity에 실제
로그인해 허용/거부 capability를 확인합니다. 구성하지 않았다면 정책
capability를 검사하고 승인된 관리자가 발급한 bootstrap용 short-lived
token을 사용합니다.
- Workloads identity는 승인된 workload policy/role와
`transit/keys/project-auth-jwt`만 변경할 수 있어야 합니다.
- Database identity는 승인된 `database/config``database/roles` 경로만
변경할 수 있어야 합니다.
- 둘 다 mount, auth backend, 임의 policy, token 발급 경로를 변경할 수
없어야 합니다.
장기 token이나 임의 AppRole을 대신 만들지 않습니다. 실제 Gitea
OIDC/JWT가 검증되지 않았다면 승인된 관리자가 발급한 short-lived bootstrap
token을 사용합니다.
## 5. Vault workload access
`TF_VAR_vault_token`을 workloads 전용 short-lived token으로 교체한 뒤
적용합니다. 다음은 OIDC/JWT를 아직 구성하지 않은 빈 dev lab의 bootstrap
예시입니다. Root token의 child revocation에 묶이지 않도록 짧은 TTL orphan
token을 발급하며, routine automation에서는 사용하지 않습니다.
```bash
export VAULT_WORKLOADS_TOKEN="$(
VAULT_TOKEN="$TF_VAR_vault_token" \
vault token create \
-orphan \
-no-default-policy \
-renewable=false \
-explicit-max-ttl=2h \
-policy=vault-workloads-automation-dev \
-ttl=2h \
-format=json |
jq -r '.auth.client_token'
)"
export TF_VAR_vault_token="$VAULT_WORKLOADS_TOKEN"
make terraform-plan \
TF_ROOT=vault-workloads \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
make terraform-apply \
TF_ROOT=vault-workloads \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-workloads
```
Plan에는 workload ACL, Kubernetes auth role와 `project-auth-jwt` Transit
key만 있어야 합니다. `auth-system-dev``auth-dev`의 정확한
ServiceAccount binding과 audience `vault`를 검토합니다. Vault를 사용하지
않는 `api-dev`에는 role이 생성되지 않아야 합니다.
## 6. Runtime secret seed
Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 operator
terminal session에만 유지합니다. 빈 dev lab의 최초 seed는 initial root
token을 잠깐 사용하고 seed 직후 CLI 환경에서 제거합니다.
```bash
export VAULT_TOKEN="$(
jq -r '.root_token' .local/vault/dev-k3s-init.json
)"
read -r -s -p "PostgreSQL superuser password: " POSTGRES_SUPERUSER_PASSWORD
echo
read -r -s -p "Auth database password: " AUTH_DB_PASSWORD
@@ -106,47 +197,72 @@ 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"
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/platform/postgres/auth-server @"$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/platform/postgres/keycloak @"$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/platform/keycloak/bootstrap-admin @"$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/platform/keycloak/client-auth-server @"$secret_file"
vault kv put kv/dev/workloads/auth-server/keycloak-client @"$secret_file"
rm -f "$secret_file"
trap - EXIT
unset VAULT_TOKEN
```
PostgreSQL이 Vault Agent 주입 후 시작하는지 확인합니다.
## 7. Vault Agent Injector gate
Vault auth, workload policy/role와 secret metadata를 확인한 뒤 Git PR에서
`vault-agent-injector` element만 `autoSync: "true"`로 바꿉니다. Merge 후
injector Deployment와 webhook health를 확인합니다. 이 단계에서도
`auth-system`과 두 workload gate는 닫혀 있어야 합니다.
## 8. Auth system gate
Secret metadata와 workload policy를 확인한 뒤 Git PR에서 `auth-system`
inventory element만 `autoSync: "true"`로 바꿉니다. PR에는 현재 전체 Argo
diff를 첨부합니다. Child manifest를 직접 apply하거나 Argo UI에서 임의로
Sync하지 않습니다.
Merge 후 PostgreSQL과 Keycloak health를 확인합니다.
```bash
kubectl -n platform rollout status statefulset/postgres --timeout=600s
kubectl -n auth-system-dev rollout status statefulset/postgres --timeout=600s
kubectl -n auth-system-dev rollout status deployment/keycloak --timeout=600s
```
## 6. Vault database state
## 9. Vault database state
초기 root token으로 TTL이 짧은 database 전용 token을 발급합니다.
`TF_VAR_vault_token`을 database 전용 short-lived token으로 교체하고
PostgreSQL credential을 실행 시점에만 전달합니다.
```bash
export TF_VAR_vault_token="$(
vault token create \
-policy=vault-database-automation-dev \
-ttl=30m \
-format=json |
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
@@ -160,50 +276,66 @@ make terraform-apply \
APPROVE_APPLY=dev-k3s/vault-database
```
## 7. Root token 폐기
Plan에는 `database/config/auth-system-postgres-dev` connection과
`auth-db-migration-dev` dynamic role만 있어야 합니다. Dynamic credential
발급과 revoke를 검증합니다.
Kubernetes auth operator login이 동작하는지 먼저 검증합니다.
## 10. Root token 폐기
```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
```
Database state와 delegated login/recovery 절차를 검증한 뒤, application
gate를 열기 전에 initial root token을 폐기합니다.
Script는 두 replacement token을 반드시 요구하고 다음을 자동 검증합니다.
검증 후 initial root token을 폐기합니다.
- `VAULT_WORKLOADS_TOKEN``sys/policies/acl/auth-server-dev``update`
capability를 가짐
- `VAULT_DATABASE_TOKEN`
`database/config/auth-system-postgres-dev``update` capability를 가짐
- 어느 replacement token도 `root` policy를 갖지 않음
이후 foundation을 다시 적용할 routine administrator는 없습니다. Encrypted
unseal custody의 담당자와 승인된 Vault generated-root recovery 절차를
확인할 수 없으면 root를 폐기하지 않습니다.
```bash
./hack/vault-init.sh revoke-root
unset operator_jwt operator_token
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
```
encrypted custody로 옮긴 init material의 local working copy는 조직의
dev recovery 정책에 따라 제거합니다.
`revoke-root`는 local init JSON에서도 root token field를 제거합니다. 두
bootstrap orphan token도 검증 직후 self-revoke하며 routine credential로
재사용하지 않습니다.
## 8. 확인
## 11. Workload gates
Keycloak realm/client sync와 database migration credential이 준비된 것을
확인한 뒤 단계별 PR을 사용합니다.
1. `auth-server``autoSync: "true"`로 변경합니다.
2. `auth-dev/ghcr-regcred` Secret 생성, migration hook 성공과 Deployment
health를 확인합니다.
3. `api-server``autoSync: "true"`로 변경합니다.
4. `api-dev/ghcr-regcred` Secret 생성, north-south와 service-to-service
경로를 확인합니다.
한 PR에서 모든 gate를 동시에 열지 않습니다.
## 12. 최종 확인
```bash
kubectl -n argocd get applications
kubectl -n vault get pods
kubectl -n platform get pods
kubectl -n auth-system-dev 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을 사용합니다.
Encrypted custody로 옮긴 init material의 local working copy는 조직의 dev
recovery 정책에 따라 제거합니다. 실패를 고치기 위해 live child manifest를
직접 수정하지 말고 Git PR을 사용합니다.
+230 -74
View File
@@ -1,88 +1,244 @@
# Terraform v2 state migration
# Terraform state migration to three Vault states
새 클러스터에는 이 runbook이 필요하지 않습니다. legacy local state 또는
이전 `provider-foundation`, `workload-foundation`, `workload-config`,
`database-config` remote state가 실제로 존재할 때만 수행합니다.
새 클러스터에는 이 runbook이 필요하지 않습니다. Legacy `vault-core` 또는
더 오래된 `provider-foundation`, `workload-foundation`, `workload-config`,
`database-config` state가 실제로 존재할 때만 사용합니다.
State 이동은 live object 삭제보다 위험할 수 있습니다. maintenance window와
독립 backup 없이 진행하지 않습니다.
> 2026-07-26 리팩터링에서는 이 절차를 실제 backend나 cluster에 실행하지
> 않았습니다. Maintenance window, 독립 backup과 승인 없이 시작하지
> 않습니다.
## 목표
State 이동은 live object 삭제보다 위험할 수 있습니다. State split,
Terraform module refactor, Vault KV path/namespace cutover를 한 apply에
섞지 않습니다.
| 이전 state | 목표 |
## 목표 ownership
| Legacy ownership | 목표 state |
|---|---|
| provider Transit Vault state | archive 후 provider Vault 폐기 절차에서 별도 처리 |
| workload foundation | `vault-core`의 기준 state |
| workload config | 소유 객체를 `vault-core`로 이동 |
| database config | `vault-database`로 backend key migration |
| `vault-core`의 mounts/auth/delegation 객체 | `vault-foundation` |
| `vault-core` 또는 `workload-config`의 workload policy/role와 Transit key | `vault-workloads` |
| `vault-database` 또는 `database-config`의 auth-system connection과 migration role | `vault-database` |
| 별도 same-cluster Transit provider Vault | State archive 후 별도 decommission 절차 |
동일 클러스터 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를
사용합니다.
목표 backend key는 각각 달라야 합니다.
```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
dev-k3s/vault-foundation.tfstate
dev-k3s/vault-workloads.tfstate
dev-k3s/vault-database.tfstate
```
이미 다른 state에 address가 있으면 import하지 말고 state ownership을 먼저
동일 Vault API path가 두 state에 동시에 남아 있으면 cutover가 끝난 것이
아닙니다. State끼리 `terraform_remote_state`를 추가하지 않습니다.
## 1. Freeze, inventory, backup
모든 Terraform apply와 관련 image/config promotion을 중단합니다.
ApplicationSet의 injector, `auth-system`, `auth-server`, `api-server`
autoSync gate도 닫습니다. 이 gate는 Argo의 자동 sync만 멈추며 이미 실행
중인 Pod의 재시작, node drain 또는 controller 동작을 막지 않습니다.
Migration 중 legacy path를 병행 유지하고, 완전한 quiesce가 필요하면
workload별 scale/maintenance 절차를 별도로 승인합니다.
각 legacy backend에서 다음을 확보합니다.
- `terraform state pull` 원본
- state checksum
- `terraform state list`
- 이동할 각 address의 `terraform state show`
- 현재 Vault object ID/path와 provider version
Backup에는 credential과 secret data가 포함될 수 있으므로 encrypted
offline custody에 보관합니다. Backend lock이 작동하는지 확인하고 source
state를 수정할 runner를 하나로 제한합니다.
## 2. Migration code 준비
먼저 실제로 배포된 legacy Git revision에서 임시 migration branch를
만듭니다. 그 revision의 policy 문서, namespace binding, role payload를
그대로 유지한 `vault-workloads` root와 import block만 추가합니다. 현재
branch의 새 KV path와 `auth-system-dev` binding을 이 단계에 복사하면
ownership 이동과 live policy 변경이 섞이므로 사용할 수 없습니다.
Source와 destination은 같은 legacy object payload를 선언해야 합니다.
실제 import ID는 backup의 `state show`로 확정하며 이름을 추측하지
않습니다. Ownership split이 양쪽 no-op으로 끝난 뒤에만 현재 desired
revision을 별도 path/namespace cutover PR로 적용합니다.
일반적인 import ID 형식은 다음과 같습니다.
```text
vault_policy <policy-name>
vault_kubernetes_auth_backend_role auth/kubernetes/role/<role-name>
vault_transit_secret_backend_key transit/keys/project-auth-jwt
```
Legacy core/foundation source에는 이동 대상별 `removed` block을 둡니다.
```hcl
removed {
from = module.workload_policies
lifecycle {
destroy = false
}
}
removed {
from = module.workload_roles
lifecycle {
destroy = false
}
}
removed {
from = vault_transit_secret_backend_key.project_auth_jwt
lifecycle {
destroy = false
}
}
```
실제 address가 다르면 현재 state list를 사용합니다. 위 예를 그대로
복사하지 않습니다.
Module 구조 개선은 아직 하지 않습니다. 우선 기존 address/선언으로
ownership만 옮기고 후속 PR에서 `moved` block을 사용합니다.
## 3. 양쪽 plan 검토
Source와 destination을 같은 revision에서 plan합니다.
| Plan | 허용 결과 |
|---|---|
| Source core/foundation | 대상 object를 state에서만 제거, live destroy `0` |
| Destination workloads | 기존 live object import, create/change/destroy `0` |
둘 중 하나라도 live create, update, delete를 제안하면 중단합니다. Policy
내용이나 Vault path rename은 이 단계에 포함하지 않습니다.
## 4. Workload ownership split
승인된 maintenance window에서 다음 순서로 진행합니다.
1. Source의 `removed { destroy = false }` apply
2. 즉시 destination import apply
3. 양쪽 `state list`에서 object가 정확히 한 번만 나타나는지 확인
4. 양쪽 plan이 no-op인지 확인
Manual remote `state push`나 offline `state mv -state/-state-out`을 primary
절차로 사용하지 않습니다. Source 제거와 destination import 사이에 문제가
생기면 다른 apply를 진행하지 말고 backup과 승인된 rollback 절차를
사용합니다.
## 5. Foundation backend key 전환
Workload object를 분리한 뒤 남은 legacy `vault-core` state 전체를
`vault-foundation` backend key로 `terraform init -migrate-state` 합니다.
Migration 전후의 state list와 serial/lineage, checksum을 기록합니다.
`vault-database`의 state 경계는 유지하지만 connection address와 live
object 이름은 모두 바뀝니다.
```text
vault_database_secret_backend_connection.platform_postgres
-> vault_database_secret_backend_connection.auth_system_postgres
database/config/platform-postgres-dev
-> database/config/auth-system-postgres-dev
```
Checked-in `moved` block은 Terraform address만 이관합니다. Vault connection
이름 변경은 replacement이며 `create_before_destroy`도 생성/삭제 사이에
operator 승인 대기 시간을 만들지 않습니다. 기존 cluster에서는 final
configuration을 바로 apply하지 말고 별도 blue/green cutover를 준비합니다.
1. 임시 migration revision에서 old connection을 유지하고 new connection을
별도 resource로 추가합니다.
2. Database runner policy가 maintenance window 동안 old/new 두 exact
`database/config` path를 모두 허용하게 합니다.
3. New connection 검증 뒤 `auth-db-migration-dev` role을 new connection으로
전환하고 credential issue/revoke를 시험합니다.
4. Old connection에 연결된 lease를 inventory하고 만료 또는 명시적 revoke를
확인합니다.
5. 후속 승인에서 old connection과 임시 policy path를 제거합니다.
기존 이름이 `database-config`이거나 backend key가 다를 때는 이 cutover와
분리해 `vault-database` backend key로 migration합니다.
Database object를 새로 import해야 하는 경우의 target address와 ID는
다음과 같습니다.
```text
vault_database_secret_backend_connection.auth_system_postgres
database/config/auth-system-postgres-dev
vault_database_secret_backend_role.auth_db_migration
database/roles/auth-db-migration-dev
```
이미 다른 state에 address가 있으면 import하지 말고 ownership을 먼저
이동합니다.
## 5. Cutover 완료 조건
## 6. Retired broad/unused access
- 두 목표 state가 remote backend와 locking을 사용
- 동일 Vault path가 두 state list에 나타나지 않음
- plan에 예상하지 않은 create/delete가 없음
- legacy state와 backup은 immutable archive
- repo와 runner에 local state/provider directory가 없음
다음 legacy object는 새 state의 desired ownership이 아닙니다.
검증이 끝나기 전 legacy backend를 삭제하지 않습니다.
- Broad `platform-admin-dev` policy와 이를 사용한 `vault-operator-dev` role
- Legacy 단일 `project-gitops-dev` CI JWT role
- 미사용 `keycloak-operator-dev`, `postgres-operator-dev` policies
- 미사용 `database/roles/postgres-operator-dev` dynamic role
State split 중 자동 destroy하지 않습니다. 우선 `removed { destroy = false }`
로 legacy source ownership에서 분리하고, Vault audit/consumer inventory로
사용자가 없음을 확인합니다. Token/lease revoke와 live object 삭제는
별도 보안 decommission 승인으로 수행합니다.
기존 `jwt-ci` auth mount가 state에 있으면 ownership 이동 동안 실제
issuer/discovery 입력을 유지합니다. 입력을 누락해 `count = 0`이 되어도
`prevent_destroy`가 mount 삭제를 차단해야 합니다. Mount와 그 하위 role을
제거할 때만 token/accessor와 consumer를 확인한 별도 decommission
revision에서 명시적으로 보호를 해제합니다.
## 7. Vault path와 namespace cutover
State split이 no-op인 것을 확인한 뒤 별도 PR/maintenance window에서
다음 legacy path를 새 owner path로 이관합니다.
```text
kv/dev/platform/postgres/* -> kv/dev/systems/auth-system/postgres/*
kv/dev/platform/keycloak/bootstrap-admin
-> kv/dev/systems/auth-system/keycloak/bootstrap-admin
kv/dev/platform/keycloak/client-auth-server
-> kv/dev/workloads/auth-server/keycloak-client
```
KV payload는 Terraform으로 이동하지 않습니다. 승인된 operator가 값을
노출하지 않는 secret procedure로 새 path에 기록하고 metadata/version을
확인합니다. Policy, Kubernetes role, Agent annotation, namespace/DNS
변경을 render와 Vault capability test로 검증합니다.
`platform`에서 `auth-system-dev`로의 live namespace 이동은 Kubernetes
state/data migration입니다. Terraform state split과 별도로 backup,
non-cascading ownership transfer, rollback 계획을 가져야 합니다.
새 consumer가 정상 동작하고 rollback 기간이 끝날 때까지 legacy KV value와
policy를 삭제하지 않습니다. 삭제는 별도 승인 작업입니다.
## 8. 완료 조건
- `vault-foundation`, `vault-workloads`, `vault-database`가 서로 다른 remote
backend와 lock을 사용
- 각 Vault API path가 정확히 한 state list에만 존재
- 세 plan에 예상하지 않은 create/change/delete가 없음
- Delegated state가 자신의 automation policy/login role을 소유하지 않음
- Workload/database identity의 허용·거부 capability test 통과
- Legacy state와 backup이 immutable archive에 있음
- Repository와 runner working directory에 local state, plan, provider
directory가 없음
- New KV path와 `auth-system-dev` cutover 전에는 관련 autoSync gate가 닫힘
검증이 끝나기 전 legacy backend, Vault path, namespace 또는 PVC를
삭제하지 않습니다.
+3 -1
View File
@@ -53,7 +53,9 @@ kubectl --context "$expected_context" -n argocd \
rollout status deployment/argocd-server --timeout=300s
kubectl --context "$expected_context" -n argocd \
rollout status deployment/argocd-repo-server --timeout=300s
kubectl --context "$expected_context" apply \
-f "${REPO_ROOT}/bootstrap/argocd/control-plane-project.yaml"
kubectl --context "$expected_context" apply \
-f "${REPO_ROOT}/bootstrap/argocd/root-application.yaml"
echo "Argo CD ${ARGOCD_VERSION} and the dev-k3s root Application are installed."
echo "Argo CD ${ARGOCD_VERSION}, its control-plane project, and the root Application are installed."
+152 -13
View File
@@ -15,7 +15,7 @@ done
cd "$REPO_ROOT"
forbidden_files="$(
git ls-files |
git ls-files --cached --others --exclude-standard |
rg '(^|/)(\\.terraform|\\.terraform-state|\\.local)(/|$)|(^|/)terraform\\.tfstate($|\\.)|\\.tfplan$' ||
true
)"
@@ -36,19 +36,106 @@ while IFS= read -r script; do
fi
done < <(rg --files hack -g '*.sh')
jq empty platform/auth-system/base/files/keycloak/project-auth-realm.json
jq empty renovate.json
jq empty systems/auth-system/base/files/keycloak/project-auth-realm.json
overlays=(
clusters/dev-k3s
clusters/dev-k3s/manifests/api-server
clusters/dev-k3s/manifests/auth-server
clusters/dev-k3s/manifests/auth-system
clusters/dev-k3s/manifests/vault
bootstrap/argocd
platform/control-plane/argocd
clusters/dev-k3s/overlays/platform/vault
clusters/dev-k3s/overlays/systems/auth-system
clusters/dev-k3s/overlays/workloads/auth-server
clusters/dev-k3s/overlays/workloads/api-server
)
for overlay in "${overlays[@]}"; do
kubectl kustomize "$overlay" >/dev/null
done
control_plane_render="$(kubectl kustomize platform/control-plane/argocd)"
if [[ "$(rg -c '^kind: AppProject$' <<<"$control_plane_render")" -ne 4 ||
"$(rg -c '^kind: ApplicationSet$' <<<"$control_plane_render")" -ne 4 ]]; then
echo "The control plane must render exactly four AppProjects and four ApplicationSets." >&2
exit 1
fi
application_manifests="$(
rg -l '^kind:[[:space:]]+Application$' \
--glob '*.yaml' \
--glob '*.yml' \
. |
sort
)"
if [[ "$application_manifests" != "./bootstrap/argocd/root-application.yaml" ]]; then
echo "Only the bootstrap root may be an explicit Argo CD Application:" >&2
echo "$application_manifests" >&2
exit 1
fi
if ! rg -q '^[[:space:]]+project:[[:space:]]+gitops-control-plane$' \
bootstrap/argocd/root-application.yaml ||
rg -n '^[[:space:]]+project:[[:space:]]+default$' bootstrap platform/control-plane; then
echo "The root and generated Applications must use explicit least-privilege AppProjects." >&2
exit 1
fi
applicationsets=(
platform/control-plane/argocd/application-sets/platform-addons.yaml
platform/control-plane/argocd/application-sets/platform-services.yaml
platform/control-plane/argocd/application-sets/systems.yaml
platform/control-plane/argocd/application-sets/workloads.yaml
)
for applicationset in "${applicationsets[@]}"; do
for safety_setting in \
'missingkey=error' \
'applicationsSync: create-update' \
'preserveResourcesOnDeletion: true' \
'Prune=confirm,Delete=confirm'; do
if ! rg -q "$safety_setting" "$applicationset"; then
echo "${applicationset} is missing ApplicationSet safety setting: ${safety_setting}" >&2
exit 1
fi
done
done
if ! rg -q '^ - Prune=confirm$' \
platform/control-plane/argocd/application-sets/platform-addons.yaml; then
echo "Platform addons must require approval before pruning chart resources." >&2
exit 1
fi
if rg -n 'project:[[:space:]]+"?\{\{' platform/control-plane/argocd/application-sets; then
echo "ApplicationSet projects are privilege boundaries and must never be templated." >&2
exit 1
fi
git_applicationsets=(
platform/control-plane/argocd/application-sets/platform-services.yaml
platform/control-plane/argocd/application-sets/systems.yaml
platform/control-plane/argocd/application-sets/workloads.yaml
)
for applicationset in "${git_applicationsets[@]}"; do
if ! rg -q '^[[:space:]]+targetRevision:[[:space:]]+main$' "$applicationset" ||
rg -q '^[[:space:]]+(repoURL|targetRevision):[[:space:]]+"?\{\{' "$applicationset"; then
echo "${applicationset} must pin the canonical repository main branch in its template." >&2
exit 1
fi
done
enabled_gates="$(
rg -o 'autoSync: "true"' platform/control-plane/argocd/application-sets |
wc -l |
tr -d ' '
)"
disabled_gates="$(
rg -o 'autoSync: "false"' platform/control-plane/argocd/application-sets |
wc -l |
tr -d ' '
)"
if [[ "$enabled_gates" -ne 2 || "$disabled_gates" -ne 4 ]]; then
echo "Initial sync gates must enable only Sealed Secrets and Vault." >&2
exit 1
fi
sealed_secrets_render="$(
helm template sealed-secrets sealed-secrets \
--repo https://bitnami.github.io/sealed-secrets \
@@ -85,8 +172,8 @@ fi
rendered_images="$(
{
kubectl kustomize clusters/dev-k3s/manifests/auth-system
kubectl kustomize clusters/dev-k3s/manifests/vault
kubectl kustomize clusters/dev-k3s/overlays/systems/auth-system
kubectl kustomize clusters/dev-k3s/overlays/platform/vault
} | rg '^[[:space:]]+image: (hashicorp/vault|postgres|quay\\.io/keycloak)'
)"
if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then
@@ -94,9 +181,26 @@ if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then
exit 1
fi
workload_kustomizations=(
clusters/dev-k3s/overlays/workloads/auth-server/kustomization.yaml
clusters/dev-k3s/overlays/workloads/api-server/kustomization.yaml
)
for workload_kustomization in "${workload_kustomizations[@]}"; do
if rg -q '^[[:space:]]+digest:[[:space:]]+sha256:[a-f0-9]{64}$' \
"$workload_kustomization"; then
continue
fi
if ! rg -q '^[[:space:]]+newTag:[[:space:]]+[a-f0-9]{7,40}$' \
"$workload_kustomization"; then
echo "${workload_kustomization} must use a verified digest or a temporary commit-shaped tag." >&2
exit 1
fi
done
terraform fmt -check -recursive iac/terraform
terraform_roots=(
iac/terraform/live/dev-k3s/vault-core
iac/terraform/live/dev-k3s/vault-foundation
iac/terraform/live/dev-k3s/vault-workloads
iac/terraform/live/dev-k3s/vault-database
)
for root in "${terraform_roots[@]}"; do
@@ -110,12 +214,47 @@ for root in "${terraform_roots[@]}"; do
done
if rg -n \
'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)' \
'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)|clusters/dev-k3s/manifests|platform/auth-system|platform/security/vault|platform-config|postgres\\.platform\\.svc|keycloak(-public)?\\.platform\\.svc|kv/data/dev/platform' \
--glob '!docs/archive/**' \
--glob '!docs/runbooks/terraform-state-migration.md' \
--glob '!policies/legacy/**' \
--glob '!hack/validate.sh' \
.; then
echo "Current files contain a legacy repository URL, Helm URL, absolute path, or Terraform root." >&2
echo "Current files contain a legacy URL, path, namespace, or Vault secret path." >&2
exit 1
fi
if rg -n '^path[[:space:]]+"[^"]*[+*]' policies/vault/dev-k3s/workloads; then
echo "Workload Vault policies must use exact paths; wildcard paths require a security review." >&2
exit 1
fi
automation_policies=(
policies/vault/dev-k3s/platform/vault-workloads-automation-dev.hcl
policies/vault/dev-k3s/platform/vault-database-automation-dev.hcl
)
for policy in "${automation_policies[@]}"; do
for self_path in \
'sys/capabilities-self' \
'auth/token/lookup-self' \
'auth/token/revoke-self'; do
if ! rg -q "^path \"${self_path}\"" "$policy"; then
echo "${policy} is missing required no-default-policy self service path: ${self_path}" >&2
exit 1
fi
done
done
if rg -n \
'^path[[:space:]]+"[^"]*[+*]|capabilities[[:space:]]*=.*"(sudo|list)"|^path[[:space:]]+"auth/token/(create|roles)' \
policies/vault/dev-k3s/platform; then
echo "Delegated automation policies must not use wildcards, sudo/list, or token issuance paths." >&2
exit 1
fi
if rg -n '^path[[:space:]]+"(sys/|auth/|database/config|database/roles)' \
policies/vault/dev-k3s/workloads; then
echo "Runtime workload policies may not configure Vault control-plane objects." >&2
exit 1
fi
@@ -124,7 +263,7 @@ if rg -n 'uses:[[:space:]]+[^#[:space:]]+@v[0-9]' .gitea/workflows; then
exit 1
fi
if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters; then
if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters platform/control-plane; then
echo "ApplyOutOfSyncOnly is incompatible with hook-based migrations and must not be enabled." >&2
exit 1
fi
+44 -1
View File
@@ -7,7 +7,7 @@ REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
VAULT_INIT_OUTPUT="${VAULT_INIT_OUTPUT:-${REPO_ROOT}/.local/vault/dev-k3s-init.json}"
for cmd in jq vault; do
for cmd in jq rg vault; do
if ! command -v "$cmd" >/dev/null 2>&1; then
echo "$cmd is required" >&2
exit 1
@@ -78,13 +78,56 @@ init() {
}
revoke_root() {
local database_lookup=""
local database_token="${VAULT_DATABASE_TOKEN:-}"
local root_token=""
local temporary=""
local workloads_lookup=""
local workloads_token="${VAULT_WORKLOADS_TOKEN:-}"
if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
exit 1
fi
if [[ -z "$workloads_token" || -z "$database_token" ]]; then
echo "Refusing root revocation: VAULT_WORKLOADS_TOKEN and VAULT_DATABASE_TOKEN are required." >&2
exit 1
fi
if ! VAULT_TOKEN="$workloads_token" \
vault token capabilities sys/policies/acl/auth-server-dev |
rg -q '(^|,|[[:space:]])update($|,|[[:space:]])'; then
echo "Refusing root revocation: the workloads replacement token failed its capability check." >&2
exit 1
fi
if ! VAULT_TOKEN="$database_token" \
vault token capabilities database/config/auth-system-postgres-dev |
rg -q '(^|,|[[:space:]])update($|,|[[:space:]])'; then
echo "Refusing root revocation: the database replacement token failed its capability check." >&2
exit 1
fi
if ! workloads_lookup="$(
VAULT_TOKEN="$workloads_token" vault token lookup -format=json
)"; then
echo "Refusing root revocation: the workloads replacement token cannot look itself up." >&2
exit 1
fi
if ! database_lookup="$(
VAULT_TOKEN="$database_token" vault token lookup -format=json
)"; then
echo "Refusing root revocation: the database replacement token cannot look itself up." >&2
exit 1
fi
if ! jq -e '.data.policies | type == "array"' <<<"$workloads_lookup" >/dev/null ||
! jq -e '.data.policies | type == "array"' <<<"$database_lookup" >/dev/null; then
echo "Refusing root revocation: a replacement token returned an invalid lookup response." >&2
exit 1
fi
if jq -e '.data.policies | index("root") != null' <<<"$workloads_lookup" >/dev/null ||
jq -e '.data.policies | index("root") != null' <<<"$database_lookup" >/dev/null; then
echo "Refusing root revocation: a replacement token unexpectedly carries the root policy." >&2
exit 1
fi
root_token="$(jq -er '.root_token' "$VAULT_INIT_OUTPUT")"
VAULT_TOKEN="$root_token" vault token revoke -self
@@ -0,0 +1,5 @@
bucket = "project-gitops-terraform-state"
key = "dev-k3s/vault-foundation.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
@@ -1,5 +1,5 @@
bucket = "project-gitops-terraform-state"
key = "dev-k3s/vault-core.tfstate"
key = "dev-k3s/vault-workloads.tfstate"
region = "us-east-1"
encrypt = true
use_lockfile = true
@@ -1,171 +0,0 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
backend "s3" {}
}
provider "vault" {
address = var.vault_addr
skip_child_token = true
token = var.vault_token
}
locals {
policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s"
workload_policies = {
auth-server-dev = file("${local.policy_dir}/auth-server-dev.hcl")
auth-db-migration-dev = file("${local.policy_dir}/auth-db-migration-dev.hcl")
postgres-dev = file("${local.policy_dir}/postgres-dev.hcl")
keycloak-dev = file("${local.policy_dir}/keycloak-dev.hcl")
keycloak-client-sync-dev = file("${local.policy_dir}/keycloak-client-sync-dev.hcl")
postgres-operator-dev = file("${local.policy_dir}/postgres-operator-dev.hcl")
keycloak-operator-dev = file("${local.policy_dir}/keycloak-operator-dev.hcl")
}
}
resource "vault_mount" "kv" {
path = var.kv_mount_path
type = "kv"
options = {
version = "2"
}
lifecycle {
prevent_destroy = true
}
}
resource "vault_mount" "database" {
path = var.database_mount_path
type = "database"
lifecycle {
prevent_destroy = true
}
}
resource "vault_mount" "transit" {
path = var.transit_mount_path
type = "transit"
lifecycle {
prevent_destroy = true
}
}
resource "vault_auth_backend" "kubernetes" {
path = var.kubernetes_auth_path
type = "kubernetes"
}
resource "vault_kubernetes_auth_backend_config" "cluster" {
backend = vault_auth_backend.kubernetes.path
disable_iss_validation = true
disable_local_ca_jwt = false
kubernetes_host = var.kubernetes_host
}
resource "vault_transit_secret_backend_key" "project_auth_jwt" {
backend = vault_mount.transit.path
name = var.jwt_transit_key_name
type = "rsa-2048"
}
resource "vault_policy" "platform_admin" {
name = var.platform_admin_policy_name
policy = file("${local.policy_dir}/platform-admin-dev.hcl")
}
resource "vault_policy" "database_automation" {
name = var.database_automation_policy_name
policy = file("${local.policy_dir}/vault-database-automation-dev.hcl")
}
module "workload_policies" {
source = "../../../modules/vault-policy-set"
policies = local.workload_policies
}
resource "vault_kubernetes_auth_backend_role" "operator" {
audience = var.kubernetes_token_audience
backend = vault_auth_backend.kubernetes.path
bound_service_account_names = [var.operator_service_account_name]
bound_service_account_namespaces = [var.operator_service_account_namespace]
role_name = var.operator_role_name
token_policies = [vault_policy.platform_admin.name]
token_ttl = var.operator_token_ttl_seconds
}
module "workload_roles" {
source = "../../../modules/vault-kubernetes-roles"
backend = vault_auth_backend.kubernetes.path
roles = {
auth-server-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["auth-server"]
service_account_namespaces = ["auth-dev"]
token_policies = [module.workload_policies.names["auth-server-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
auth-db-migration-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["auth-db-migration"]
service_account_namespaces = ["auth-dev"]
token_policies = [module.workload_policies.names["auth-db-migration-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
postgres-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["postgres"]
service_account_namespaces = ["platform"]
token_policies = [module.workload_policies.names["postgres-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
keycloak-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["keycloak"]
service_account_namespaces = ["platform"]
token_policies = [module.workload_policies.names["keycloak-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
keycloak-client-sync-dev = {
audiences = [var.kubernetes_token_audience]
service_account_names = ["keycloak-client-sync"]
service_account_namespaces = ["platform"]
token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
}
}
resource "vault_jwt_auth_backend" "ci" {
count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
bound_issuer = var.ci_jwt_bound_issuer
oidc_discovery_url = var.ci_jwt_oidc_discovery_url
path = var.ci_jwt_auth_path
}
resource "vault_jwt_auth_backend_role" "ci" {
count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
backend = vault_jwt_auth_backend.ci[0].path
bound_audiences = var.ci_jwt_bound_audiences
bound_claims = var.ci_jwt_bound_claims
bound_claims_type = "glob"
role_name = var.ci_jwt_role_name
role_type = "jwt"
token_explicit_max_ttl = var.ci_token_ttl_seconds
token_policies = [vault_policy.database_automation.name]
user_claim = var.ci_jwt_user_claim
}
@@ -1,146 +0,0 @@
variable "database_automation_policy_name" {
description = "Least-privilege policy used by the approved Vault database runner."
type = string
default = "vault-database-automation-dev"
}
variable "ci_jwt_auth_path" {
description = "JWT auth mount used by external CI."
type = string
default = "jwt-ci"
}
variable "ci_jwt_bound_audiences" {
description = "Accepted CI JWT audiences."
type = set(string)
default = []
}
variable "ci_jwt_bound_claims" {
description = "Claims that bind CI JWTs to the canonical repository and protected branch."
type = map(string)
default = {}
}
variable "ci_jwt_bound_issuer" {
description = "Expected issuer for CI JWTs."
type = string
default = null
nullable = true
}
variable "ci_jwt_oidc_discovery_url" {
description = "CI OIDC discovery URL. Null keeps JWT auth disabled until the issuer is confirmed."
type = string
default = null
nullable = true
}
variable "ci_jwt_role_name" {
description = "Vault role used by the GitOps configuration workflow."
type = string
default = "project-gitops-dev"
}
variable "ci_jwt_user_claim" {
description = "JWT claim used as the Vault identity alias."
type = string
default = "sub"
}
variable "ci_token_ttl_seconds" {
description = "Maximum lifetime for a CI Vault token."
type = number
default = 3600
}
variable "database_mount_path" {
description = "Workload Vault database secrets mount path."
type = string
default = "database"
}
variable "jwt_transit_key_name" {
description = "Transit key used for application JWT signing."
type = string
default = "project-auth-jwt"
}
variable "kubernetes_auth_path" {
description = "Kubernetes auth backend path."
type = string
default = "kubernetes"
}
variable "kubernetes_host" {
description = "Kubernetes TokenReview API address."
type = string
default = "https://kubernetes.default.svc.cluster.local:443"
}
variable "kubernetes_role_ttl_seconds" {
description = "TTL for workload Kubernetes auth tokens."
type = number
default = 3600
}
variable "kubernetes_token_audience" {
description = "Audience used by projected service account tokens."
type = string
default = "vault"
}
variable "kv_mount_path" {
description = "Workload Vault KV-v2 mount path."
type = string
default = "kv"
}
variable "operator_role_name" {
description = "Workload Vault Kubernetes auth role for human operators."
type = string
default = "vault-operator-dev"
}
variable "operator_service_account_name" {
description = "Service account authorized to open workload Vault operator sessions."
type = string
default = "vault-operator"
}
variable "operator_service_account_namespace" {
description = "Namespace of the workload Vault operator service account."
type = string
default = "vault"
}
variable "operator_token_ttl_seconds" {
description = "TTL for workload Vault operator sessions."
type = number
default = 1800
}
variable "platform_admin_policy_name" {
description = "Policy used only for short-lived break-glass administration."
type = string
default = "platform-admin-dev"
}
variable "transit_mount_path" {
description = "Workload Vault Transit mount path for application cryptography."
type = string
default = "transit"
}
variable "vault_addr" {
description = "Workload Vault API address reachable by the approved runner."
type = string
default = "http://127.0.0.1:8200"
}
variable "vault_token" {
description = "Short-lived token used only for this Terraform run."
type = string
sensitive = true
ephemeral = true
}
@@ -17,9 +17,23 @@ provider "vault" {
token = var.vault_token
}
moved {
from = vault_database_secret_backend_connection.platform_postgres
to = vault_database_secret_backend_connection.auth_system_postgres
}
removed {
from = vault_database_secret_backend_role.postgres_operator
lifecycle {
destroy = false
}
}
locals {
migration_role_name = "auth-db-migration-dev"
operator_role_name = "postgres-operator-dev"
database_config_name = "auth-system-postgres-dev"
database_mount_path = "database"
migration_role_name = "auth-db-migration-dev"
creation_statements = [
<<-EOT
@@ -38,10 +52,10 @@ locals {
]
}
resource "vault_database_secret_backend_connection" "platform_postgres" {
allowed_roles = [local.migration_role_name, local.operator_role_name]
backend = var.database_mount_path
name = var.database_config_name
resource "vault_database_secret_backend_connection" "auth_system_postgres" {
allowed_roles = [local.migration_role_name]
backend = local.database_mount_path
name = local.database_config_name
plugin_name = "postgresql-database-plugin"
verify_connection = true
@@ -52,24 +66,18 @@ resource "vault_database_secret_backend_connection" "platform_postgres" {
password_wo_version = var.postgres_admin_password_version
username = var.postgres_admin_username
}
lifecycle {
create_before_destroy = true
}
}
resource "vault_database_secret_backend_role" "auth_db_migration" {
backend = var.database_mount_path
backend = local.database_mount_path
creation_statements = local.creation_statements
db_name = vault_database_secret_backend_connection.platform_postgres.name
db_name = vault_database_secret_backend_connection.auth_system_postgres.name
default_ttl = var.auth_db_migration_default_ttl_seconds
max_ttl = var.auth_db_migration_max_ttl_seconds
name = local.migration_role_name
revocation_statements = local.revocation_statements
}
resource "vault_database_secret_backend_role" "postgres_operator" {
backend = var.database_mount_path
creation_statements = local.creation_statements
db_name = vault_database_secret_backend_connection.platform_postgres.name
default_ttl = var.postgres_operator_default_ttl_seconds
max_ttl = var.postgres_operator_max_ttl_seconds
name = local.operator_role_name
revocation_statements = local.revocation_statements
}
@@ -16,18 +16,6 @@ variable "auth_db_role" {
default = "project_auth"
}
variable "database_config_name" {
description = "Vault database connection name."
type = string
default = "platform-postgres-dev"
}
variable "database_mount_path" {
description = "Foundation-owned database secrets mount path."
type = string
default = "database"
}
variable "postgres_admin_password" {
description = "PostgreSQL admin password passed only through a write-only provider field."
type = string
@@ -53,25 +41,13 @@ variable "postgres_database" {
}
variable "postgres_host" {
description = "Platform PostgreSQL service DNS name."
description = "Auth system PostgreSQL service DNS name."
type = string
default = "postgres.platform.svc.cluster.local"
}
variable "postgres_operator_default_ttl_seconds" {
description = "Default TTL for operator database credentials."
type = number
default = 3600
}
variable "postgres_operator_max_ttl_seconds" {
description = "Maximum TTL for operator database credentials."
type = number
default = 28800
default = "postgres.auth-system-dev.svc.cluster.local"
}
variable "postgres_port" {
description = "Platform PostgreSQL service port."
description = "Auth system PostgreSQL service port."
type = number
default = 5432
}
@@ -0,0 +1,189 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
backend "s3" {}
}
provider "vault" {
address = var.vault_addr
token = var.vault_token
}
removed {
from = module.workload_policies
lifecycle {
destroy = false
}
}
removed {
from = module.workload_roles
lifecycle {
destroy = false
}
}
removed {
from = vault_transit_secret_backend_key.project_auth_jwt
lifecycle {
destroy = false
}
}
removed {
from = vault_policy.platform_admin
lifecycle {
destroy = false
}
}
removed {
from = vault_kubernetes_auth_backend_role.operator
lifecycle {
destroy = false
}
}
removed {
from = vault_jwt_auth_backend_role.ci
lifecycle {
destroy = false
}
}
locals {
platform_policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s/platform"
database_automation_policy_name = "vault-database-automation-dev"
database_mount_path = "database"
ci_database_role_name = "project-gitops-vault-database-dev"
ci_jwt_auth_path = "jwt-ci"
ci_workloads_role_name = "project-gitops-vault-workloads-dev"
kubernetes_auth_path = "kubernetes"
kv_mount_path = "kv"
transit_mount_path = "transit"
workloads_automation_policy_name = "vault-workloads-automation-dev"
automation_roles = var.ci_jwt_oidc_discovery_url == null ? {} : {
workloads = {
bound_claims = var.ci_workloads_bound_claims
name = local.ci_workloads_role_name
policy = vault_policy.workloads_automation.name
}
database = {
bound_claims = var.ci_database_bound_claims
name = local.ci_database_role_name
policy = vault_policy.database_automation.name
}
}
}
resource "vault_mount" "kv" {
path = local.kv_mount_path
type = "kv"
options = {
version = "2"
}
lifecycle {
prevent_destroy = true
}
}
resource "vault_mount" "database" {
path = local.database_mount_path
type = "database"
lifecycle {
prevent_destroy = true
}
}
resource "vault_mount" "transit" {
path = local.transit_mount_path
type = "transit"
lifecycle {
prevent_destroy = true
}
}
resource "vault_auth_backend" "kubernetes" {
path = local.kubernetes_auth_path
type = "kubernetes"
lifecycle {
prevent_destroy = true
}
}
resource "vault_kubernetes_auth_backend_config" "cluster" {
backend = vault_auth_backend.kubernetes.path
disable_iss_validation = true
disable_local_ca_jwt = false
kubernetes_host = var.kubernetes_host
}
resource "vault_policy" "workloads_automation" {
name = local.workloads_automation_policy_name
policy = file("${local.platform_policy_dir}/vault-workloads-automation-dev.hcl")
}
resource "vault_policy" "database_automation" {
name = local.database_automation_policy_name
policy = file("${local.platform_policy_dir}/vault-database-automation-dev.hcl")
}
resource "vault_jwt_auth_backend" "ci" {
count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
bound_issuer = var.ci_jwt_bound_issuer
oidc_discovery_url = var.ci_jwt_oidc_discovery_url
path = local.ci_jwt_auth_path
lifecycle {
prevent_destroy = true
precondition {
condition = (
var.ci_jwt_bound_issuer != null &&
length(var.ci_jwt_bound_audiences) > 0 &&
length(var.ci_workloads_bound_claims) > 0 &&
length(var.ci_database_bound_claims) > 0 &&
length([
for claim, value in var.ci_workloads_bound_claims : claim
if lookup(var.ci_database_bound_claims, claim, value) != value
]) > 0
)
error_message = "Enabled CI JWT auth requires issuer/audience constraints and workload/database claim maps with at least one shared discriminator key carrying different values."
}
}
}
resource "vault_jwt_auth_backend_role" "automation" {
for_each = local.automation_roles
backend = vault_jwt_auth_backend.ci[0].path
bound_audiences = var.ci_jwt_bound_audiences
bound_claims = each.value.bound_claims
bound_claims_type = "string"
role_name = each.value.name
role_type = "jwt"
token_explicit_max_ttl = var.ci_token_ttl_seconds
token_no_default_policy = true
token_policies = [each.value.policy]
user_claim = var.ci_jwt_user_claim
}
@@ -0,0 +1,71 @@
variable "ci_database_bound_claims" {
description = "Exact repository, protected-ref, and database-job claims for the database role."
type = map(string)
default = {}
}
variable "ci_jwt_bound_audiences" {
description = "Exact CI JWT audiences."
type = set(string)
default = []
}
variable "ci_jwt_bound_issuer" {
description = "Expected CI JWT issuer."
type = string
default = null
nullable = true
}
variable "ci_jwt_oidc_discovery_url" {
description = "CI OIDC discovery URL. Null leaves external CI authentication disabled."
type = string
default = null
nullable = true
}
variable "ci_jwt_user_claim" {
description = "JWT claim used as the Vault identity alias."
type = string
default = "sub"
}
variable "ci_token_ttl_seconds" {
description = "Maximum lifetime for delegated CI tokens."
type = number
default = 1800
validation {
condition = (
var.ci_token_ttl_seconds >= 60 &&
var.ci_token_ttl_seconds <= 3600 &&
floor(var.ci_token_ttl_seconds) == var.ci_token_ttl_seconds
)
error_message = "ci_token_ttl_seconds must be a whole number between 60 and 3600."
}
}
variable "ci_workloads_bound_claims" {
description = "Exact repository, protected-ref, and workloads-job claims for the workloads role."
type = map(string)
default = {}
}
variable "kubernetes_host" {
description = "Kubernetes TokenReview API address."
type = string
default = "https://kubernetes.default.svc.cluster.local:443"
}
variable "vault_addr" {
description = "Vault API address reachable by the foundation operator."
type = string
default = "http://127.0.0.1:8200"
}
variable "vault_token" {
description = "Short-lived bootstrap or security-administrator token."
type = string
sensitive = true
ephemeral = true
}
@@ -0,0 +1,22 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/vault" {
version = "5.7.0"
constraints = "~> 5.7.0"
hashes = [
"h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
"zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
"zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
"zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
"zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
"zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
"zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
"zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
"zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
"zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
"zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
]
}
@@ -0,0 +1,95 @@
terraform {
required_version = ">= 1.11.0"
required_providers {
vault = {
source = "hashicorp/vault"
version = "~> 5.7.0"
}
}
backend "s3" {}
}
provider "vault" {
address = var.vault_addr
skip_child_token = true
token = var.vault_token
}
locals {
workload_policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s/workloads"
jwt_transit_key_name = "project-auth-jwt"
kubernetes_auth_path = "kubernetes"
kubernetes_token_audience = "vault"
transit_mount_path = "transit"
workload_policies = {
auth-server-dev = file("${local.workload_policy_dir}/auth-server-dev.hcl")
auth-db-migration-dev = file("${local.workload_policy_dir}/auth-db-migration-dev.hcl")
postgres-dev = file("${local.workload_policy_dir}/postgres-dev.hcl")
keycloak-dev = file("${local.workload_policy_dir}/keycloak-dev.hcl")
keycloak-client-sync-dev = file("${local.workload_policy_dir}/keycloak-client-sync-dev.hcl")
}
}
module "workload_policies" {
source = "../../../modules/vault-policy-set"
policies = local.workload_policies
}
module "workload_roles" {
source = "../../../modules/vault-kubernetes-roles"
backend = local.kubernetes_auth_path
roles = {
auth-server-dev = {
audiences = [local.kubernetes_token_audience]
service_account_names = ["auth-server"]
service_account_namespaces = ["auth-dev"]
token_policies = [module.workload_policies.names["auth-server-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
auth-db-migration-dev = {
audiences = [local.kubernetes_token_audience]
service_account_names = ["auth-db-migration"]
service_account_namespaces = ["auth-dev"]
token_policies = [module.workload_policies.names["auth-db-migration-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
postgres-dev = {
audiences = [local.kubernetes_token_audience]
service_account_names = ["postgres"]
service_account_namespaces = ["auth-system-dev"]
token_policies = [module.workload_policies.names["postgres-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
keycloak-dev = {
audiences = [local.kubernetes_token_audience]
service_account_names = ["keycloak"]
service_account_namespaces = ["auth-system-dev"]
token_policies = [module.workload_policies.names["keycloak-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
keycloak-client-sync-dev = {
audiences = [local.kubernetes_token_audience]
service_account_names = ["keycloak-client-sync"]
service_account_namespaces = ["auth-system-dev"]
token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]]
token_ttl = var.kubernetes_role_ttl_seconds
}
}
}
resource "vault_transit_secret_backend_key" "project_auth_jwt" {
backend = local.transit_mount_path
deletion_allowed = false
name = local.jwt_transit_key_name
type = "rsa-2048"
lifecycle {
prevent_destroy = true
}
}
@@ -0,0 +1,18 @@
variable "kubernetes_role_ttl_seconds" {
description = "TTL for workload Kubernetes auth tokens."
type = number
default = 3600
}
variable "vault_addr" {
description = "Vault API address reachable by the delegated runner."
type = string
default = "http://127.0.0.1:8200"
}
variable "vault_token" {
description = "Short-lived token carrying only vault-workloads-automation-dev."
type = string
sensitive = true
ephemeral = true
}
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- platform-addons.yaml
- platform-services.yaml
- systems.yaml
- workloads.yaml
@@ -0,0 +1,122 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-addons
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-5"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- list:
elements:
- component: sealed-secrets
cluster: dev-k3s
server: https://kubernetes.default.svc
namespace: kube-system
repoURL: https://bitnami.github.io/sealed-secrets
chart: sealed-secrets
# renovate: datasource=helm depName=sealed-secrets registryUrl=https://bitnami.github.io/sealed-secrets
revision: 2.17.9
autoSync: "true"
helmValues: |
fullnameOverride: sealed-secrets-controller
keyrenewperiod: 720h
image:
# renovate: datasource=docker depName=bitnami/sealed-secrets-controller
repository: bitnami/sealed-secrets-controller
tag: "0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc"
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 250m
memory: 256Mi
- component: vault-agent-injector
cluster: dev-k3s
server: https://kubernetes.default.svc
namespace: vault
repoURL: https://helm.releases.hashicorp.com
chart: vault
# renovate: datasource=helm depName=vault registryUrl=https://helm.releases.hashicorp.com
revision: 0.32.0
autoSync: "false"
helmValues: |
global:
externalVaultAddr: http://vault.vault.svc.cluster.local:8200
tlsDisable: true
server:
enabled: false
injector:
enabled: true
authPath: auth/kubernetes
image:
# renovate: datasource=docker depName=hashicorp/vault-k8s
repository: hashicorp/vault-k8s
tag: "1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"
webhook:
failurePolicy: Fail
namespaceSelector:
matchLabels:
vault-injection: enabled
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
agentImage:
# renovate: datasource=docker depName=hashicorp/vault
repository: hashicorp/vault
tag: "1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"
template:
metadata:
name: "{{ .component }}-{{ .cluster }}"
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
project-gitops.io/auto-sync-gate: "{{ .autoSync }}"
labels:
project-gitops.io/cluster: "{{ .cluster }}"
project-gitops.io/owner: platform
spec:
project: platform-addons
source:
repoURL: "{{ .repoURL }}"
chart: "{{ .chart }}"
targetRevision: "{{ .revision }}"
helm:
values: "{{ .helmValues }}"
destination:
server: "{{ .server }}"
namespace: "{{ .namespace }}"
syncPolicy:
syncOptions:
- CreateNamespace=true
- Prune=confirm
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
templatePatch: |
{{- if eq .autoSync "true" }}
spec:
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
{{- end }}
syncPolicy:
applicationsSync: create-update
preserveResourcesOnDeletion: true
@@ -0,0 +1,64 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: platform-services
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-5"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- list:
elements:
- component: vault
cluster: dev-k3s
server: https://kubernetes.default.svc
namespace: vault
path: clusters/dev-k3s/overlays/platform/vault
autoSync: "true"
template:
metadata:
name: "{{ .component }}-{{ .cluster }}"
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
project-gitops.io/auto-sync-gate: "{{ .autoSync }}"
labels:
project-gitops.io/cluster: "{{ .cluster }}"
project-gitops.io/owner: platform
spec:
project: platform-services
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: "{{ .path }}"
destination:
server: "{{ .server }}"
namespace: "{{ .namespace }}"
syncPolicy:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
templatePatch: |
{{- if eq .autoSync "true" }}
spec:
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
{{- end }}
syncPolicy:
applicationsSync: create-update
preserveResourcesOnDeletion: true
@@ -0,0 +1,64 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: systems
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-5"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- list:
elements:
- component: auth-system
cluster: dev-k3s
server: https://kubernetes.default.svc
namespace: auth-system-dev
path: clusters/dev-k3s/overlays/systems/auth-system
autoSync: "false"
template:
metadata:
name: "{{ .component }}-{{ .cluster }}"
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
project-gitops.io/auto-sync-gate: "{{ .autoSync }}"
labels:
project-gitops.io/cluster: "{{ .cluster }}"
project-gitops.io/owner: systems
spec:
project: systems
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: "{{ .path }}"
destination:
server: "{{ .server }}"
namespace: "{{ .namespace }}"
syncPolicy:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
templatePatch: |
{{- if eq .autoSync "true" }}
spec:
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
{{- end }}
syncPolicy:
applicationsSync: create-update
preserveResourcesOnDeletion: true
@@ -0,0 +1,70 @@
apiVersion: argoproj.io/v1alpha1
kind: ApplicationSet
metadata:
name: workloads
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-5"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
goTemplate: true
goTemplateOptions:
- missingkey=error
generators:
- list:
elements:
- component: auth-server
cluster: dev-k3s
server: https://kubernetes.default.svc
namespace: auth-dev
path: clusters/dev-k3s/overlays/workloads/auth-server
autoSync: "false"
- component: api-server
cluster: dev-k3s
server: https://kubernetes.default.svc
namespace: api-dev
path: clusters/dev-k3s/overlays/workloads/api-server
autoSync: "false"
template:
metadata:
name: "{{ .component }}-{{ .cluster }}"
annotations:
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
project-gitops.io/auto-sync-gate: "{{ .autoSync }}"
labels:
project-gitops.io/cluster: "{{ .cluster }}"
project-gitops.io/owner: workloads
spec:
project: workloads
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: "{{ .path }}"
destination:
server: "{{ .server }}"
namespace: "{{ .namespace }}"
syncPolicy:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- FailOnSharedResource=true
retry:
limit: 5
refresh: true
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
templatePatch: |
{{- if eq .autoSync "true" }}
spec:
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
{{- end }}
syncPolicy:
applicationsSync: create-update
preserveResourcesOnDeletion: true
@@ -3,4 +3,4 @@ kind: Kustomization
resources:
- projects
- applications
- application-sets
@@ -0,0 +1,8 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- platform-addons.yaml
- platform-services.yaml
- systems.yaml
- workloads.yaml
@@ -1,7 +1,7 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: cluster-addons-dev
name: platform-addons
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
@@ -9,7 +9,7 @@ metadata:
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Cluster-scoped controllers required by the dev GitOps stack
description: Cluster-scoped controllers installed from approved Helm repositories
sourceRepos:
- https://bitnami.github.io/sealed-secrets
- https://helm.releases.hashicorp.com
@@ -40,6 +40,8 @@ spec:
kind: ServiceAccount
- group: apps
kind: Deployment
- group: policy
kind: PodDisruptionBudget
- group: rbac.authorization.k8s.io
kind: Role
- group: rbac.authorization.k8s.io
@@ -1,7 +1,7 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: platform-dev
name: platform-services
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
@@ -9,43 +9,29 @@ metadata:
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Dev shared infrastructure managed by Argo CD
description: Shared services owned by this standalone reference platform
sourceRepos:
- https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- namespace: platform
server: https://kubernetes.default.svc
- namespace: vault
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "rbac.authorization.k8s.io"
kind: ClusterRole
- group: "rbac.authorization.k8s.io"
- group: rbac.authorization.k8s.io
kind: ClusterRoleBinding
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: PersistentVolumeClaim
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "rbac.authorization.k8s.io"
kind: Role
- group: "rbac.authorization.k8s.io"
kind: RoleBinding
- group: "apps"
- group: apps
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
- group: networking.k8s.io
kind: NetworkPolicy
orphanedResources:
warn: true
@@ -0,0 +1,39 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: systems
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Backing systems dedicated to an example product boundary
sourceRepos:
- https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- namespace: auth-system-dev
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: apps
kind: Deployment
- group: apps
kind: StatefulSet
- group: batch
kind: Job
- group: networking.k8s.io
kind: Ingress
- group: networking.k8s.io
kind: NetworkPolicy
orphanedResources:
warn: true
@@ -1,7 +1,7 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: apps-dev
name: workloads
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "-10"
@@ -9,7 +9,7 @@ metadata:
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Dev application workloads managed by Argo CD
description: First-party application workloads
sourceRepos:
- https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
@@ -27,15 +27,15 @@ spec:
kind: Service
- group: ""
kind: ServiceAccount
- group: "bitnami.com"
- group: bitnami.com
kind: SealedSecret
- group: "apps"
- group: apps
kind: Deployment
- group: "batch"
- group: batch
kind: Job
- group: "networking.k8s.io"
- group: networking.k8s.io
kind: Ingress
- group: "networking.k8s.io"
- group: networking.k8s.io
kind: NetworkPolicy
orphanedResources:
warn: true
@@ -1,5 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-operator
automountServiceAccountToken: false
@@ -1,7 +1,5 @@
ui = true
disable_mlock = true
api_addr = "http://vault.vault.svc.cluster.local:8200"
cluster_addr = "http://vault.vault.svc.cluster.local:8201"
listener "tcp" {
address = "0.0.0.0:8200"
@@ -11,5 +9,5 @@ listener "tcp" {
storage "raft" {
path = "/vault/data"
node_id = "vault-dev-0"
node_id = "vault-0"
}
@@ -3,7 +3,6 @@ kind: Kustomization
resources:
- vault-serviceaccount.yaml
- vault-operator-serviceaccount.yaml
- vault-auth-delegator.clusterrolebinding.yaml
- vault-pvc.yaml
- vault-service.yaml
@@ -24,6 +24,14 @@ spec:
env:
- name: VAULT_ADDR
value: http://127.0.0.1:8200
- name: POD_IP
valueFrom:
fieldRef:
fieldPath: status.podIP
- name: VAULT_API_ADDR
value: http://$(POD_IP):8200
- name: VAULT_CLUSTER_ADDR
value: http://$(POD_IP):8201
args:
- |
cp /vault/config/vault.hcl /tmp/vault.hcl
@@ -3,7 +3,7 @@ kind: PersistentVolumeClaim
metadata:
name: vault-data
annotations:
argocd.argoproj.io/sync-options: Prune=false
argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
accessModes:
- ReadWriteOnce
@@ -1,7 +0,0 @@
path "kv/data/dev/platform/keycloak/bootstrap-admin" {
capabilities = ["read"]
}
path "kv/data/dev/platform/keycloak/client-auth-server" {
capabilities = ["read"]
}
-7
View File
@@ -1,7 +0,0 @@
path "kv/data/dev/platform/postgres/keycloak" {
capabilities = ["read"]
}
path "kv/data/dev/platform/keycloak/bootstrap-admin" {
capabilities = ["read"]
}
@@ -1,3 +0,0 @@
path "kv/data/dev/platform/keycloak/bootstrap-admin" {
capabilities = ["read"]
}
@@ -1,43 +0,0 @@
path "kv/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "auth/kubernetes/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "sys/policies/acl/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "database/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "transit/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "auth/token/create-orphan" {
capabilities = ["update"]
}
path "auth/token/lookup" {
capabilities = ["update"]
}
path "sys/storage/raft/snapshot" {
capabilities = ["read", "update", "sudo"]
}
path "sys/storage/raft/snapshot-force" {
capabilities = ["update", "sudo"]
}
@@ -0,0 +1,24 @@
# Managed by vault-foundation. The database runner may reconcile only the named
# PostgreSQL connection, dynamic role, and minimal self-service token endpoints.
path "database/config/auth-system-postgres-dev" {
capabilities = ["create", "read", "update", "delete"]
}
path "database/roles/auth-db-migration-dev" {
capabilities = ["create", "read", "update", "delete"]
}
# no-default-policy runner tokens retain only the self-service operations used
# for capability checks, identity verification, and explicit revocation.
path "sys/capabilities-self" {
capabilities = ["update"]
}
path "auth/token/lookup-self" {
capabilities = ["read"]
}
path "auth/token/revoke-self" {
capabilities = ["update"]
}

Some files were not shown because too many files have changed in this diff Show More