Compare commits

...
3 Commits
276 changed files with 8133 additions and 6479 deletions
+17
View File
@@ -0,0 +1,17 @@
root = true
[*]
charset = utf-8
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true
[*.{yaml,yml,json,tf,tofu,hcl}]
indent_style = space
indent_size = 2
[*.md]
trim_trailing_whitespace = false
[Makefile]
indent_style = tab
+15
View File
@@ -0,0 +1,15 @@
* text=auto eol=lf
*.sh text eol=lf
*.tf text eol=lf
*.tofu text eol=lf
*.yaml text eol=lf
*.yml text eol=lf
*.gif binary
*.ico binary
*.jpeg binary
*.jpg binary
*.pdf binary
*.png binary
*.webp binary
+114
View File
@@ -0,0 +1,114 @@
name: Promote Dev Image by Pull Request
on:
workflow_dispatch:
inputs:
service:
description: Workload to promote
required: true
type: choice
options:
- auth-server
- api-server
image_digest:
description: Immutable OCI digest including the sha256 prefix
required: true
type: string
concurrency:
group: promote-dev-${{ inputs.service }}
cancel-in-progress: false
jobs:
promote:
runs-on:
- self-hosted
- linux
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: 0
token: ${{ secrets.GITOPS_BOT_TOKEN }}
- name: Update immutable digest
env:
SERVICE: ${{ inputs.service }}
IMAGE_DIGEST: ${{ inputs.image_digest }}
run: |
case "$SERVICE" in
auth-server)
image_name="ghcr.io/donghyeonka/project-auth-server"
;;
api-server)
image_name="ghcr.io/donghyeonka/project-api-server"
;;
*)
echo "Unsupported service: $SERVICE" >&2
exit 1
;;
esac
if [[ ! "$IMAGE_DIGEST" =~ ^sha256:[a-f0-9]{64}$ ]]; then
echo "image_digest must be an immutable sha256 digest" >&2
exit 1
fi
manifest="gitops/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) }
target && $1 == "newTag:" {
match($0, /^[[:space:]]*/)
print substr($0, RSTART, RLENGTH) "digest: " image_digest
target = 0
updated = 1
next
}
target && $1 == "digest:" {
match($0, /^[[:space:]]*/)
print substr($0, RSTART, RLENGTH) "digest: " image_digest
target = 0
updated = 1
next
}
{ print }
END { if (!updated) exit 42 }
' "$manifest" >"$temporary"
mv "$temporary" "$manifest"
kubectl kustomize "$(dirname "$manifest")" >/dev/null
- name: Create promotion branch
env:
SERVICE: ${{ inputs.service }}
RUN_NUMBER: ${{ gitea.run_number }}
run: |
branch="gitops/promote-${SERVICE}-${RUN_NUMBER}"
git config user.name "gitops-bot"
git config user.email "gitops-bot@hyeonworks.local"
git switch -c "$branch"
git add "gitops/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"
- name: Open Gitea pull request
env:
GITEA_TOKEN: ${{ secrets.GITOPS_BOT_TOKEN }}
GITEA_API_URL: ${{ gitea.api_url }}
REPOSITORY: ${{ gitea.repository }}
DEFAULT_BRANCH: ${{ gitea.event.repository.default_branch }}
SERVICE: ${{ inputs.service }}
IMAGE_DIGEST: ${{ inputs.image_digest }}
run: |
payload="$(
jq -n \
--arg base "$DEFAULT_BRANCH" \
--arg head "$PROMOTION_BRANCH" \
--arg title "chore(gitops): promote ${SERVICE} dev digest" \
--arg body "Promotes ${SERVICE} to immutable digest ${IMAGE_DIGEST}." \
'{base: $base, head: $head, title: $title, body: $body}'
)"
curl -fsS \
-H "Authorization: token ${GITEA_TOKEN}" \
-H "Content-Type: application/json" \
-d "$payload" \
"${GITEA_API_URL}/repos/${REPOSITORY}/pulls"
+23
View File
@@ -0,0 +1,23 @@
name: Validate GitOps Repository
on:
pull_request:
push:
branches:
- main
concurrency:
group: validate-gitops-${{ gitea.ref }}
cancel-in-progress: true
jobs:
validate:
runs-on:
- self-hosted
- linux
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
- name: Validate
run: make check
+66
View File
@@ -0,0 +1,66 @@
# OS and editors
.DS_Store
.idea/
.vscode/
*.swp
*.swo
*.tmp
# Local environment and credentials
.env
.env.*
!.env.example
*.key
*.pem
*.p12
*.pfx
*.jks
credentials.json
id_dsa
id_ecdsa
id_ed25519
id_rsa
# Kubernetes local access
kubeconfig
kubeconfig.*
*.kubeconfig
# Terraform / OpenTofu working directories and local state
**/.terraform/*
**/.terraform-state/*
**/.terragrunt-cache/*
*.tfstate
*.tfstate.*
*.tfplan
*.tfvars
!*.tfvars.example
plan.out
.terraform.tfstate.lock.info
crash.log
crash.*.log
override.tf
override.tf.json
*_override.tf
*_override.tf.json
*.local.tfvars
*.secret.tfvars
# Bootstrap material and local operator files
.local/*
!.local/.gitkeep
*init*.json
# Decrypted or generated secret material
.decrypted/
*.decrypted.yaml
*.dec.yaml
# Build, render, and test output
.build/
.cache/
dist/
rendered/
tmp/
coverage/
*.log
+25
View File
@@ -0,0 +1,25 @@
# Repository operating rules
- Treat the Gitea `origin` as the canonical deployment repository.
- Production is disabled; do not create or enable production Applications
without a complete production design and explicit approval.
- Never commit Terraform state, provider directories, plan files, tfvars,
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 `gitops/platform`, bounded-context
backing services under `gitops/apps/systems`, and first-party runtimes under
`gitops/apps/workloads`.
- Manage child Argo CD Applications through the permission-scoped
ApplicationSets in `gitops/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.
- Do not apply to a live cluster unless the user explicitly requests live
deployment and the kube context has been verified.
+72
View File
@@ -0,0 +1,72 @@
# Changelog
## 2026-07-26
- Adopted the `k8s-template` lifecycle layout: bootstrap under
`bootstrap/gitops`, Terraform under `infrastructure`, all Kubernetes desired
state under `gitops`, automation under `scripts`, and ADRs under
`docs/decisions`.
- Made `gitops/clusters/dev-k3s` the root Application source while preserving
permission-scoped ApplicationSets and their staged `autoSync` gates.
- Co-located reusable Terraform modules in `infrastructure/components`,
backend examples with each live root, and Vault ACLs with their owning
Terraform state.
- Added the template catalogs, `_template` directories, examples, security
checks, and common validation ahead of project-specific validation.
- 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.
- Replaced staged Argo roots with one bootstrap seed and one cluster-owned
root Application.
- Reorganized the repository around `clusters/dev-k3s`, environment-neutral
`platform`/`workloads` bases and separate `iac/terraform`.
- Corrected the Sealed Secrets Helm repository and added controller resources
and key renewal configuration.
- Removed incomplete production skeletons; production remains unsupported.
- Changed application promotion to a Gitea pull request carrying an immutable
OCI digest and removed direct writes to `main`.
- Pinned Vault, PostgreSQL, Keycloak, Vault Injector and Sealed Secrets images
to verified multi-architecture digests.
- Removed routine `kubectl apply`, port-forward orchestration and reusable
Vault operator-token scripts.
- Added a guarded, dev-only Vault init/unseal/root-revoke entrypoint.
- Fixed auth migration and Keycloak sync Job lifecycle and ordering.
- Removed selective sync from Applications containing Sync hooks.
- Enabled ConfigMap hash rollouts and projected Vault authentication tokens.
- Removed the same-cluster Transit Vault and its credential rotation cycle.
- Consolidated Terraform into `vault-core` and `vault-database` remote states
with write-only/ephemeral credential inputs.
- Removed tracked Terraform providers and backend metadata.
- Moved machine-consumed Vault policies out of runbooks and narrowed routine
automation permissions.
- Recorded Gateway API first and deferred Istio ambient adoption criteria.
- Reduced shell entrypoints from 18 files/1,205 lines to 3 files/300 lines.
- Split current documentation from archived historical material.
+32
View File
@@ -0,0 +1,32 @@
# 기여 가이드
## 변경 원칙
1. 변경 대상의 소유 디렉터리를 먼저 확인합니다.
2. 재사용 가능한 구현은 catalog 영역에, 환경별 조합과 차이만 live/cluster
진입점에 둡니다.
3. 하나의 리소스는 하나의 도구와 하나의 디렉터리만 소유합니다.
4. 운영 변경에는 영향 범위, 롤백 방법, 검증 결과를 함께 기록합니다.
5. `make check`를 통과한 변경만 리뷰를 요청합니다.
## 이름 규칙
- 폴더와 리소스: 소문자 `kebab-case`
- 환경: `dev`, `staging`, `prod`처럼 조직에서 합의한 고정 어휘
- 클러스터: 환경과 위치를 식별할 수 있는 안정적인 이름
- 임시 이름, 사람 이름, 티켓 번호를 장기 리소스 이름에 사용하지 않음
## Pull request 체크리스트
- [ ] 변경이 올바른 소유권 경계에 위치한다.
- [ ] 비밀, kubeconfig, state, plan 파일이 포함되지 않았다.
- [ ] 공급자, module, chart, image 버전 변경의 영향을 확인했다.
- [ ] `make check` 결과를 확인했다.
- [ ] 운영 영향이 있으면 rollback/runbook을 갱신했다.
- [ ] 구조적 선택이 바뀌면 ADR을 추가하거나 갱신했다.
## 배포
Kubernetes routine 변경은 Git PR로만 반영합니다. Terraform apply는
`TF_ROOT`, backend config, 검토된 saved plan과 `APPROVE_APPLY`가 모두
준비된 경우에만 실행합니다. `destroy` 진입점은 제공하지 않습니다.
+67
View File
@@ -0,0 +1,67 @@
SHELL := /bin/bash
.DEFAULT_GOAL := help
.PHONY: help doctor validate check tree bootstrap vault-init terraform-init terraform-plan terraform-apply check-terraform-inputs
KUBE_CONTEXT ?=
TF_ROOT ?=
BACKEND_CONFIG ?=
TF_DIR = infrastructure/live/dev-k3s/$(TF_ROOT)
PLAN_FILE ?= $(CURDIR)/.local/terraform-plans/dev-k3s-$(TF_ROOT).tfplan
help: ## 사용 가능한 명령을 표시합니다.
@awk 'BEGIN {FS = ":.*## "; print "Usage: make <target>"} /^[a-zA-Z_-]+:.*## / {printf " %-20s %s\n", $$1, $$2}' $(MAKEFILE_LIST)
doctor: ## 현재 로컬 도구 상태를 확인합니다.
@./scripts/doctor.sh
validate: ## 템플릿 구조와 프로젝트별 GitOps/IaC 계약을 검증합니다.
@./scripts/validate.sh
check: validate ## CI와 동일한 전체 검증을 실행합니다.
tree: ## 생성물과 Git 메타데이터를 제외한 구조를 표시합니다.
@find . \
\( \
-name .build -o \
-name .cache -o \
-name .git -o \
-name .terraform -o \
-name .terragrunt-cache -o \
-name dist -o \
-name rendered -o \
-name tmp \
\) -prune -o \
-print | sort
bootstrap: ## 확인된 kube context에 Argo CD와 root Application을 설치합니다.
@test -n "$(KUBE_CONTEXT)" || (echo "KUBE_CONTEXT is required" >&2; exit 1)
./scripts/bootstrap-argocd.sh --context "$(KUBE_CONTEXT)"
vault-init: ## 개발 Vault를 최초 초기화합니다.
./scripts/vault-init.sh init
check-terraform-inputs:
@case "$(TF_ROOT)" in \
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)
terraform-init: check-terraform-inputs ## 선택한 Vault Terraform root를 초기화합니다.
terraform -chdir="$(TF_DIR)" init -input=false -reconfigure -backend-config="$(abspath $(BACKEND_CONFIG))"
terraform-plan: terraform-init ## 검토 가능한 saved Terraform plan을 만듭니다.
@mkdir -p "$(dir $(PLAN_FILE))"
@umask 077; terraform -chdir="$(TF_DIR)" plan \
-input=false \
-lock-timeout=5m \
-out="$(PLAN_FILE)"
terraform-apply: terraform-init ## 명시적으로 승인한 saved Terraform plan을 적용합니다.
@test "$(APPROVE_APPLY)" = "dev-k3s/$(TF_ROOT)" || \
(echo "Set APPROVE_APPLY=dev-k3s/$(TF_ROOT) to continue" >&2; exit 1)
@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)"
+134 -1573
View File
File diff suppressed because it is too large Load Diff
+46
View File
@@ -0,0 +1,46 @@
# Security Policy
## 저장소에 둘 수 없는 항목
- 평문 비밀번호, token, access key, private key
- base64로만 인코딩한 Kubernetes `Secret`
- kubeconfig와 클라우드 provider credential
- Terraform/OpenTofu state, plan, 복호화 산출물
- 실제 비밀이 포함된 `tfvars`, Helm values, `.env`
`.gitignore`는 실수 완화 장치일 뿐 보안 통제가 아닙니다. 비밀이 한 번이라도
커밋되었다면 history 삭제 여부와 관계없이 즉시 폐기하고 회전합니다.
## 허용하는 비밀 관리 방식
프로젝트마다 다음 중 하나를 ADR로 선택하고 CI와 운영 절차를 함께 정의합니다.
- External Secrets 계열 리소스로 외부 secret manager를 참조
- SOPS와 KMS/age를 사용해 암호화한 파일만 저장
- 조직에서 승인한 동등한 GitOps 비밀 관리 방식
SOPS로 암호화한 Kubernetes Secret은 검증 가능한 규칙을 위해
`*.sops.yaml`, `*.sops.yml` 또는 `*.sops.json` 이름을 사용합니다.
암호화 키, 복호화 권한과 secret manager 접근은 workload identity/OIDC와
최소 권한 원칙으로 부여합니다. 장기 cloud access key를 CI secret으로
사용하지 않습니다.
## State와 CI
- remote state는 암호화, locking, versioning을 활성화합니다.
- 환경과 독립 장애 영역은 별도 state로 분리합니다.
- production apply에는 승인, 직렬화와 감사 로그를 적용합니다.
- fork 또는 신뢰하지 않는 PR 코드에 privileged credential을 제공하지 않습니다.
- CI action, provider, module, chart와 image 버전을 검토 가능한 방식으로 고정합니다.
## 노출 사고 대응
1. 노출된 credential과 파생 token을 폐기하고 회전합니다.
2. 영향받은 시스템의 접근 로그와 변경 이력을 확인합니다.
3. 저장소 history와 artifact/cache에서 민감 데이터를 제거합니다.
4. 원인과 영향 범위, 재발 방지 조치를 incident 문서에 기록합니다.
5. 조직의 보안 연락 채널로 보고합니다.
보안 이슈는 공개 issue 대신 canonical Gitea 저장소의 비공개 보안 채널로
보고합니다. 대응 우선순위와 SLA는 운영 환경 도입 전에 별도로 승인합니다.
@@ -1,7 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: api-server-config
data:
APP_SERVER_PORT: "8082"
APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI: http://auth-public.auth-dev.svc.cluster.local
@@ -1,13 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: api-prod
resources:
- ../../base
- namespace.yaml
images:
- name: ghcr.io/donghyeonka/project-api-server
newName: ghcr.io/donghyeonka/project-api-server
newTag: fd097c9
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: api-prod
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
@@ -1,26 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-server-config
data:
SPRING_PROFILES_ACTIVE: dev
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_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_CLIENT_ID: project-auth-server
APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google
APP_SECURITY_OAUTH2_GOOGLE_IDP_HINT: google
APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID: keycloak-github
APP_SECURITY_OAUTH2_GITHUB_IDP_HINT: github
APP_SECURITY_JWT_ISSUER: http://auth-public.auth-dev.svc.cluster.local
APP_SECURITY_JWT_ACTIVE_KEY_ID: dev-vault-rsa-1
APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP: "false"
APP_SECURITY_JWT_ACCESS_TOKEN_EXPIRATION: PT30M
APP_SECURITY_JWT_VAULT_ENABLED: "true"
APP_SECURITY_JWT_VAULT_ADDRESS: http://vault.vault.svc.cluster.local:8200
APP_SECURITY_JWT_VAULT_MOUNT_PATH: transit
APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME: project-auth-jwt
@@ -1,13 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: auth-prod
resources:
- ../../base
- namespace.yaml
images:
- name: ghcr.io/donghyeonka/project-auth-server
newName: ghcr.io/donghyeonka/project-auth-server
newTag: 5648fd2
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: auth-prod
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
-18
View File
@@ -1,18 +0,0 @@
## Argo CD Structure
`argocd/` 디렉터리는 환경(`dev`, `prod`)과 성격(`apps`, `infra`) 기준으로 나눠 관리합니다.
- `applications/<env>/apps`: 서비스 애플리케이션 선언
- `applications/<env>/infra`: 공용 인프라/컨트롤러 선언
- `projects/<env>/apps-project.yaml`: 서비스 애플리케이션용 AppProject
- `projects/<env>/infra-project.yaml`: 공용 인프라용 AppProject
현재 `dev`에는 실제 선언을 두고, `prod`는 이후 운영 확장을 위한 구조와 프로젝트 골격을 먼저 유지합니다.
현재 dev `infra`에는 대표적으로 아래 Application이 포함됩니다.
- `vault-transit`: workload Vault transit auto-unseal provider
- `vault`: workload Vault
- `platform`: postgres, keycloak
- `vault-agent-injector`: workload secret injection
- `sealed-secrets`: image pull secret 같은 예외 secret 처리
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: api-server-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "40"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
targetRevision: main
path: apps/api-server/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: api-dev
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: auth-server-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "30"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
targetRevision: main
path: apps/auth-server/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: auth-dev
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
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: "20"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
targetRevision: main
path: infra/platform/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: platform
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,35 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: sealed-secrets-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
source:
repoURL: https://bitnami-labs.github.io/sealed-secrets
chart: sealed-secrets
targetRevision: 2.17.9
helm:
values: |
fullnameOverride: sealed-secrets-controller
destination:
server: https://kubernetes.default.svc
namespace: kube-system
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,59 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault-agent-injector-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-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
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"
destination:
server: https://kubernetes.default.svc
namespace: vault
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault-transit-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
targetRevision: main
path: infra/vault-transit/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: vault-transit
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
-34
View File
@@ -1,34 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: vault-dev
namespace: argocd
annotations:
argocd.argoproj.io/sync-wave: "10"
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: infra-dev
source:
repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
targetRevision: main
path: infra/vault/overlays/dev
destination:
server: https://kubernetes.default.svc
namespace: vault
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- CreateNamespace=true
- PruneLast=true
- ApplyOutOfSyncOnly=true
retry:
limit: 5
backoff:
duration: 5s
factor: 2
maxDuration: 3m
revisionHistoryLimit: 5
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
-68
View File
@@ -1,68 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: infra-dev
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Dev shared infrastructure managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
- https://bitnami-labs.github.io/sealed-secrets
- https://helm.releases.hashicorp.com
destinations:
- namespace: platform
server: https://kubernetes.default.svc
- namespace: vault
server: https://kubernetes.default.svc
- namespace: vault-transit
server: https://kubernetes.default.svc
- namespace: kube-system
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "apiextensions.k8s.io"
kind: CustomResourceDefinition
- group: "rbac.authorization.k8s.io"
kind: ClusterRole
- group: "rbac.authorization.k8s.io"
kind: ClusterRoleBinding
- group: "admissionregistration.k8s.io"
kind: MutatingWebhookConfiguration
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "rbac.authorization.k8s.io"
kind: Role
- group: "rbac.authorization.k8s.io"
kind: RoleBinding
- group: "apps"
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
-50
View File
@@ -1,50 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: apps-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Prod application workloads managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
destinations:
- namespace: auth-prod
server: https://kubernetes.default.svc
- namespace: api-prod
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "apps"
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
-61
View File
@@ -1,61 +0,0 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
name: infra-prod
namespace: argocd
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Prod shared infrastructure managed by Argo CD
sourceRepos:
- https://github.com/DongHyeonka/Project-Auth-GitOps
- https://bitnami-labs.github.io/sealed-secrets
destinations:
- namespace: platform-prod
server: https://kubernetes.default.svc
- namespace: kube-system
server: https://kubernetes.default.svc
clusterResourceWhitelist:
- group: ""
kind: Namespace
- group: "apiextensions.k8s.io"
kind: CustomResourceDefinition
- group: "rbac.authorization.k8s.io"
kind: ClusterRole
- group: "rbac.authorization.k8s.io"
kind: ClusterRoleBinding
namespaceResourceWhitelist:
- group: ""
kind: ConfigMap
- group: ""
kind: Secret
- group: ""
kind: Service
- group: ""
kind: ServiceAccount
- group: ""
kind: PersistentVolumeClaim
- group: "bitnami.com"
kind: SealedSecret
- group: "rbac.authorization.k8s.io"
kind: Role
- group: "rbac.authorization.k8s.io"
kind: RoleBinding
- group: "apps"
kind: Deployment
- group: "apps"
kind: StatefulSet
- group: "apps"
kind: ReplicaSet
- group: "autoscaling"
kind: HorizontalPodAutoscaler
- group: "batch"
kind: Job
- group: "networking.k8s.io"
kind: Ingress
- group: "networking.k8s.io"
kind: NetworkPolicy
- group: "policy"
kind: PodDisruptionBudget
orphanedResources:
warn: true
+11
View File
@@ -0,0 +1,11 @@
# Bootstrap
선언형 인프라와 GitOps가 스스로 동작하기 전에 한 번 또는 매우 드물게 수행하는
최소 초기화만 둡니다.
- `foundation`: remote state, locking, 최초 identity 같은 선행 조건
- `gitops`: 선택한 controller 설치와 cluster root 연결
일반 네트워크, Kubernetes cluster, addon과 application을 이곳에 두지 않습니다.
부트스트랩 절차는 반복 실행 가능하고 감사 가능해야 하며, 장기 수동 운영 경로가
되어서는 안 됩니다.
+21
View File
@@ -0,0 +1,21 @@
# Foundation Bootstrap
다른 IaC state가 의존하는 최소 선행 조건을 관리합니다.
가능한 대상:
- remote state storage와 locking
- state 암호화 key
- CI의 최초 workload identity/OIDC trust
- 조직 공통 account/project 초기 설정
## 계약
- 일반 infrastructure state와 분리합니다.
- 변경 권한과 실행 빈도를 최소화합니다.
- local state를 장기간 유지하지 않습니다.
- output과 후속 `infrastructure/live`가 값을 소비하는 방식을 문서화합니다.
- provider credential이나 실제 backend secret을 커밋하지 않습니다.
조직 공통 foundation이 외부 저장소에 이미 있다면 이 폴더에는 소유 팀, output
contract와 복구 절차만 기록합니다.
+19
View File
@@ -0,0 +1,19 @@
# GitOps Bootstrap
Flux, Argo CD 등 **하나의** GitOps controller를 선택해 설치하고 cluster root에
연결합니다. 선택하지 않은 controller의 병렬 구조를 만들지 않습니다.
포함 범위:
- controller 설치 또는 설치 manifest
- source repository/OCI 연결
- `gitops/clusters/<...>` root reconcile 선언
- controller용 최소 identity와 secret manager 접근 연결
제외 범위:
- ingress, certificate, DNS, storage, observability addon
- application workload
- controller 설치 후 Git으로 관리할 수 있는 일반 Kubernetes 리소스
bootstrap credential과 recovery 절차는 `docs/runbooks`에 문서화합니다.
@@ -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
@@ -1,8 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: vault-transit
resources:
- ../../base
- namespace.yaml
- control-plane-project.yaml
- root-application.yaml
@@ -0,0 +1,23 @@
apiVersion: argoproj.io/v1alpha1
kind: Application
metadata:
name: project-gitops-control-plane
namespace: argocd
spec:
project: gitops-control-plane
source:
repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
path: gitops/clusters/dev-k3s
destination:
server: https://kubernetes.default.svc
namespace: argocd
syncPolicy:
automated:
enabled: true
prune: true
selfHeal: true
syncOptions:
- PruneLast=true
- FailOnSharedResource=true
revisionHistoryLimit: 10
+2
View File
@@ -0,0 +1,2 @@
ARGOCD_VERSION=v3.4.2
ARGOCD_INSTALL_SHA256=69114b8c9eb48a1d08598e6f654a0869b10ae902456ea4b70796cb563760f5ec
+159
View File
@@ -0,0 +1,159 @@
# Argo CD architecture
## Control-plane ownership
Controller 설치 후 다음 두 bootstrap object를 순서대로 수동 seed합니다.
1. `bootstrap/gitops/argocd/control-plane-project.yaml`
2. `bootstrap/gitops/argocd/root-application.yaml`
`gitops-control-plane` AppProject는 canonical Gitea repository와 in-cluster
`argocd` namespace, AppProject/ApplicationSet kind만 허용합니다. 단일 root
Application은 이 Project를 사용하고 `gitops/clusters/dev-k3s`를 source로
사용합니다. Cluster root는 다음 control-plane 구성을 참조합니다.
```text
gitops/platform/control-plane/argocd
├── projects
│ ├── platform-addons.yaml
│ ├── platform-services.yaml
│ ├── systems.yaml
│ └── workloads.yaml
└── application-sets
├── platform-addons.yaml
├── platform-services.yaml
├── systems.yaml
└── workloads.yaml
```
Cluster root는 AppProject와 ApplicationSet까지만 직접 소유합니다. 각
ApplicationSet의 list inventory가 실제 child Application을 생성합니다.
Routine 변경에 category별 root나 직접 `kubectl apply`를 추가하지 않습니다.
## AppProject boundary
| 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` |
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에서 반복하게 될 때 `gitops/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는 이 리팩터링 리뷰 범위에 포함되지 않습니다.
+119
View File
@@ -0,0 +1,119 @@
# Deployment architecture
## Reconciliation boundaries
```text
Gitea main
|
+-- Argo CD root
| -> AppProjects + ApplicationSets
| -> generated Applications
| -> Kubernetes
|
+-- approved Terraform runner
-> one of three Vault states
-> Vault API
```
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가 아닙니다.
## Kubernetes ownership
| Layer | 역할 |
|---|---|
| `gitops/clusters/dev-k3s` | bootstrap root가 읽는 유일한 cluster entrypoint |
| `gitops/platform/control-plane/argocd` | AppProject와 ApplicationSet control plane |
| `gitops/platform/shared-services/*/base` | 환경 중립을 목표로 하는 공유 cluster service base |
| `gitops/apps/systems/*/base` | 환경 중립을 목표로 하는 bounded-context backing system base |
| `gitops/apps/workloads/*/base` | first-party 애플리케이션 base |
| `gitops/clusters/dev-k3s/overlays/*` | dev namespace, host, digest, Vault role/path, NetworkPolicy를 합친 최종 구성 |
현재 concrete ownership은 Vault가 platform shared service,
PostgreSQL/Keycloak이 `gitops/apps/systems/auth-system`, 두 서버가
`gitops/apps/workloads`의 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 안에서는 다음 ordering을 사용합니다.
- generated ConfigMap과 일반 리소스: wave `0`
- database migration Sync hook: wave `5`
- Deployment: wave `10`
- north-south route: wave `20`
`auth-system`의 Keycloak client sync도 idempotent Sync hook이며 deadline,
backoff, `BeforeHookCreation,HookSucceeded` cleanup을 사용합니다.
Application 간 준비 순서와 Application 내부 hook 순서를 혼동하지
않습니다.
## Stateful lifecycle
Vault PVC에는 `Prune=confirm,Delete=confirm`이 명시되어 있습니다.
PostgreSQL PVC는 StatefulSet `volumeClaimTemplates`가 생성하며 현재
manifest에 별도 Argo prune annotation이 없습니다. Namespace와 generated
Application 삭제 보호만 믿지 말고 PostgreSQL retention/backup을 직접
확인해야 합니다. Path, namespace, Application 이름을 이동할 때는 다음을
별도 migration으로 다룹니다.
1. 기존 live object와 owner를 inventory합니다.
2. 새 owner가 같은 object를 안전하게 추적할 수 있는지 render/diff로
확인합니다.
3. Stateful data backup과 rollback 지점을 확보합니다.
4. 기존 owner를 non-cascading 방식으로 제거한 뒤 새 owner를 연결합니다.
이번 리팩터링에서는 `platform` namespace의 auth-system을
`auth-system-dev`로 옮기는 live 작업을 실행하지 않았습니다.
## 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로
교체합니다.
+150
View File
@@ -0,0 +1,150 @@
# Repository Structure
## 설계 목표
이 구조는 특정 제품의 파일 배치보다 변경 주기와 소유권을 우선합니다.
- 한 리소스에는 한 명확한 소유자가 있다.
- 재사용 구현과 실제 배포 진입점을 분리한다.
- 소규모 구성은 선택 영역을 생략할 수 있다.
- 규모가 커져도 기존 경계를 바꾸지 않고 같은 종류의 leaf를 추가한다.
- 사람이 실행하는 명령과 CI 검증이 같은 진입점을 사용한다.
## 소유권 매트릭스
| 대상 | 소유 경로 | 직접 실행 여부 | 변경 주기 |
|---|---|---:|---|
| state backend, 초기 identity | `bootstrap/foundation` | 예 | 매우 낮음 |
| 네트워크, IAM, DNS, 클러스터 | `infrastructure/live` | 예 | 낮음 |
| 재사용 IaC 단위 | `infrastructure/components` | 아니요 | 중간 |
| 재사용 IaC 조합 | `infrastructure/stacks` | 아니요 | 중간 |
| GitOps 컨트롤러와 root 연결 | `bootstrap/gitops` | 예 | 낮음 |
| 클러스터 desired state | `gitops/clusters` | reconcile 진입점 | 지속적 |
| cluster-wide addon | `gitops/platform` | 아니요 | 중간 |
| 애플리케이션 배포 정의 | `gitops/apps` | 아니요 | 높음 |
| 정책과 tenant 정의 | `gitops/policies`, `gitops/tenants` | 아니요 | 중간 |
Catalog 영역(`components`, `stacks`, `platform`, `policies`, `tenants`, `apps`)은
직접 배포하지 않습니다. 실제 진입점이 필요한 항목만 조합해서 참조합니다.
## 의존 방향
```text
bootstrap/foundation
│ output
infrastructure/components ◀── infrastructure/stacks
▲ ▲
└──────── infrastructure/live ─┘
│ cluster endpoint/identity
bootstrap/gitops
│ root reference
platform ─┐
policies ─┼────────▶ gitops/clusters
tenants ─┤
apps ─┘
```
역방향 의존은 만들지 않습니다. 예를 들어 reusable component가 특정
`live/prod` 값을 읽거나, app base가 특정 cluster overlay를 참조하면 안 됩니다.
## Infrastructure 경계
### `components`
네트워크, identity, registry, Kubernetes cluster처럼 작고 응집된 재사용
단위입니다. Terraform/OpenTofu를 선택했다면 일반적으로 backend가 없는 child
module에 해당합니다.
### `stacks`
여러 component를 반복해서 같은 방식으로 조합할 때만 사용합니다. 작은 프로젝트는
이 계층 없이 `live`가 component를 직접 호출할 수 있습니다. stack이 다른 stack을
깊게 중첩하기보다는 live root에서 평평하게 조합하는 방식을 권장합니다.
### `live`
실제로 plan/apply하는 root입니다. leaf 하나는 다음을 만족해야 합니다.
- 독립된 state와 locking
- 명시적인 provider와 backend 설정
- 고정된 component/module/chart 버전
- 비밀이 아닌 환경 입력만 저장소에 커밋
- 출력값과 downstream contract 문서화
작은 구성은 `live/dev/cluster`로 충분합니다. 계정과 리전이 늘어나면
`live/<provider>/<account>/<region>/<environment>/<stack>`처럼 경로를 확장합니다.
자동화는 경로의 고정 깊이에 의존하지 말고 실행 가능한 root 파일을 기준으로
대상을 찾도록 작성합니다.
서로 다른 환경의 root가 상대 경로로 다른 환경 구현을 import하면 안 됩니다.
공유가 필요하면 versioned component나 명시적인 remote output/data contract를
사용합니다.
## GitOps 경계
### `clusters`
클러스터가 reconcile하는 유일한 진입점입니다. 공통 리소스를 복사하지 않고
platform, policy, tenant, app catalog에서 필요한 항목만 참조합니다.
작은 구성은 `clusters/dev/main`, 다중 리전 구성은
`clusters/<environment>/<region>/<cluster>` 형태를 사용할 수 있습니다. 여기에도
고정된 경로 깊이를 강제하지 않습니다.
### `platform`
cluster-wide controller와 addon을 둡니다. 예시는 다음과 같습니다.
- ingress/gateway, external DNS, certificate
- storage class/CSI, autoscaling
- metrics, logs, traces, alerting
- secret operator와 delivery controller
각 component는 `base`와 필요한 `overlays`를 같은 디렉터리 안에 응집시킵니다.
환경 차이는 전체 파일 복사 대신 Kustomize patch 또는 별도 values로 표현합니다.
### `policies`, `tenants`, `apps`
- `policies`: cluster-wide admission 규칙, 거버넌스와 예외
- `tenants`: 구체적인 namespace, RBAC, quota, limit range, NetworkPolicy
- `apps`: application source code가 아닌 배포 정의
애플리케이션 팀이 별도 저장소를 소유하면 `apps`에는 그 저장소/OCI artifact를
참조하는 GitOps 리소스만 둘 수 있습니다.
Cloud DNS zone/delegation과 cloud IAM role은 `infrastructure`가 소유합니다.
External DNS controller, 동적 record 요청과 Kubernetes ServiceAccount binding은
`gitops/platform`이 소유합니다. 두 계층 사이에는 zone ID, role ARN 같은
명시적인 output contract만 전달합니다.
## Bootstrap 경계
Bootstrap은 선언형 관리가 스스로 시작될 수 없는 최소 범위만 담당합니다.
- `foundation`: state backend, 최초 CI identity와 같은 선행 조건
- `gitops`: Flux 또는 Argo CD 중 선택한 컨트롤러 설치와 root reference
ingress, cert-manager, observability 같은 addon은 bootstrap이 아니라 GitOps가
소유합니다. bootstrap 이후의 변경을 계속 수동 명령으로 누적하지 않습니다.
## 규모 확장 기준
| 단계 | 추가하는 것 | 그대로 유지하는 것 |
|---|---|---|
| 소형 | 단일 live root, 단일 cluster root, 최소 platform | lifecycle/ownership 경계 |
| 중형 | reusable stack, staging/prod, 정책, 관측성 | component와 entrypoint 분리 |
| 대형 | 계정·리전별 state, 다중 cluster, tenants, CODEOWNERS | 한 리소스 한 소유자 |
| 조직 분리 | lifecycle/team별 repository 분리 가능 | 각 repository 내부의 동일한 계약 |
repository를 분리하는 시점은 폴더 수가 아니라 권한, 배포 주기와 소유 팀이
실제로 달라졌을 때입니다.
## 설계 참고 자료
- [Kubernetes: Kustomize를 이용한 선언형 객체 관리](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/)
- [Flux: GitOps repository 구조](https://fluxcd.io/flux/guides/repository-structure/)
- [OpenTofu: reusable module](https://opentofu.org/docs/language/modules/)
- [OpenTofu: 평평한 module composition](https://opentofu.org/docs/language/modules/develop/composition/)
+159
View File
@@ -0,0 +1,159 @@
# Repository taxonomy
이 문서는 새 리소스를 어느 디렉터리에 둘지 결정하는 기준입니다. 이
저장소는 Project Auth를 예제로 삼는 독립 reference lab이며, 디렉터리
이름은 조직의 중요도나 설치 순서가 아니라 소유권을 표현합니다.
## 분류 기준
| 분류 | 판단 질문 | 현재 예 |
|---|---|---|
| `gitops/platform` | GitOps control plane이거나, 둘 이상의 system이 독립 lifecycle로 소비할 cluster capability인가? | Argo inventory, Vault shared service |
| `gitops/apps/systems` | 하나의 bounded context가 함께 소유하는 backing system인가? | Project Auth의 PostgreSQL, Keycloak, realm/client sync |
| `gitops/apps/workloads` | 별도 source repository에서 빌드하는 first-party 실행 단위인가? | `auth-server`, `api-server` |
| `gitops/clusters` | 특정 클러스터의 최종 composition 값인가? | namespace, host, digest, Vault role, NetworkPolicy |
| `infrastructure` | 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를 갖게 되면
실행 서비스는 `gitops/platform/`으로 이동할 수 있습니다. 그래도 Project
Auth realm/client 구성은 `gitops/apps/systems/auth-system/`에 남습니다.
현재 Keycloak과 PostgreSQL은 Project Auth 전용이므로 모두 system
소유입니다.
## Path contract
환경 중립 base와 cluster-specific overlay를 분리하는 것이 목표
contract입니다.
```text
gitops/platform/shared-services/<name>/base
gitops/apps/systems/<system>/base
gitops/apps/workloads/<workload>/base
gitops/clusters/<cluster>/overlays/platform/<name>
gitops/clusters/<cluster>/overlays/systems/<system>
gitops/clusters/<cluster>/overlays/workloads/<workload>
gitops/platform/control-plane/argocd/projects
gitops/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 가정은 남아 있습니다.
- `gitops/apps/systems/auth-system/base`의 Keycloak 실행 command가
`start-dev`입니다.
- `gitops/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는
`gitops/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만 `gitops/platform/shared-services/`에 둡니다.
`foundation`은 소유권 분류가 아닙니다. Bootstrap 때 먼저 필요하다는 뜻은
분리된 ApplicationSet category, `autoSync` gate와 runbook 순서로
표현합니다. 따라서 새로운 `foundation/` business directory를 만들지
않습니다.
## System와 workload의 경계
`gitops/apps/systems/auth-system`은 인증 bounded context가 함께 책임지는
데이터와 identity backing services입니다.
- PostgreSQL StatefulSet와 초기 database contract
- Keycloak server와 Project Auth realm
- Keycloak client synchronization
`gitops/apps/workloads/auth-server``gitops/apps/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 | `gitops/platform/control-plane/argocd/application-sets/platform-addons.yaml` |
| 공용 object storage service base | `gitops/platform/shared-services/object-storage/base` |
| Project Auth 전용 Redis | `gitops/apps/systems/auth-system/base` |
| 새 first-party worker | `gitops/apps/workloads/<worker>/base` |
| dev worker digest/secret annotation | `gitops/clusters/dev-k3s/overlays/workloads/<worker>` |
| Vault workload policy/role | `infrastructure/live/dev-k3s/vault-workloads`와 그 아래 `policies/` |
| Vault auth backend | `vault-foundation` Terraform state |
분류가 애매하면 설치 순서가 아니라 owner와 소비자 경계를 ADR에 먼저
기록합니다.
+127
View File
@@ -0,0 +1,127 @@
# Secret trust boundaries
## Dev Vault
`dev-k3s`는 workload 클러스터 안의 단일 self-hosted Vault를 사용합니다.
별도 Transit Vault는 두지 않습니다. Vault는 다음 API 객체를 제공합니다.
- KV-v2 runtime secret path
- Kubernetes auth와 workload role
- Dynamic PostgreSQL credential
- 애플리케이션 JWT signing용 Transit key
Dev Vault는 Shamir 1-of-1로 한 번 초기화하고 재시작 때 명시적으로
unseal합니다. 이는 폐기 가능한 개발 환경 전용입니다. Production에서는
managed Vault 또는 독립 failure domain의 HA integrated-Raft와 KMS/HSM
auto-unseal이 필요합니다.
## Three Terraform states
```text
vault-foundation
-> mounts/auth configuration
-> delegated automation policies
-> optional, separated CI JWT login roles
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가 사용하며
application container에 Kubernetes bearer token을 직접 노출하지 않습니다.
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은 다음 작업에만 사용합니다.
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
- 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
infrastructure/live/<cluster>/machine
infrastructure/live/<cluster>/vault-foundation
infrastructure/live/<cluster>/vault-workloads
infrastructure/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)
@@ -6,11 +6,11 @@
즉, 이 문서는 단순한 코드 해설서가 아니라 아래 3층 구조를 목표로 합니다.
1. 개념 레이어
1. 개념 레이어
이 프로젝트가 왜 Kubernetes, GitOps, Vault, Terraform, Keycloak 구조를 쓰는지 이해합니다.
2. 아키텍처 레이어
2. 아키텍처 레이어
현재 dev 환경에서 각 구성요소가 어떤 책임을 가지며 어떻게 연결되는지 이해합니다.
3. 코드 레이어
3. 코드 레이어
실제로 `apps/`, `infra/`, `scripts/`, `terraform/`, `runbooks/` 안의 파일을 읽고 수정할 수 있게 합니다.
이 문서를 다 읽고 나면 최소한 아래 질문에 스스로 답할 수 있어야 합니다.
@@ -113,7 +113,7 @@
## 5. 이 저장소를 이해하기 위한 핵심 개념
이 섹션은 "왜 이런 구조가 필요한가"를 설명합니다.
이 섹션은 "왜 이런 구조가 필요한가"를 설명합니다.
이 섹션을 먼저 이해해야 뒤에서 나오는 YAML, Bash, HCL, TF가 단순 문법이 아니라 **설계의 결과물**로 보입니다.
### 5-1. 인증, 인가, OAuth2, OIDC, JWT, Keycloak
@@ -181,7 +181,7 @@ sequenceDiagram
### 5-2. 리눅스, 컨테이너, 프로세스, 파일
이 저장소의 YAML을 읽을 때 사실상 리눅스 프로세스 개념을 알아야 합니다.
이 저장소의 YAML을 읽을 때 사실상 리눅스 프로세스 개념을 알아야 합니다.
특히 Vault patch를 읽을 때 이 이해가 없으면 `command`, `args`, `. /vault/secrets/runtime-env`, `exec java -jar ...` 같은 부분이 전부 주문처럼 보입니다.
꼭 이해해야 하는 개념은 아래와 같습니다.
@@ -211,7 +211,7 @@ flowchart TD
A["/bin/sh -ec 시작<br/>PID 1 = 셸 프로세스"] --> B[". /vault/secrets/runtime-env<br/>export 명령들이 현재 셸에 적용<br/>→ 환경변수가 셸 메모리에 올라감"]
B --> C["exec java -jar /app/application.jar<br/>셸 프로세스가 Java 프로세스로 교체<br/>→ PID 1 = Java (셸은 사라짐)"]
end
subgraph 만약_exec_없이["만약 exec를 안 쓰면?"]
direction TB
D["PID 1 = 셸 (계속 살아있음)"] --> E["PID 2 = Java (자식 프로세스)"]
@@ -221,7 +221,7 @@ flowchart TD
`exec`가 왜 중요한지 이 그림이 보여줍니다. K8s가 Pod를 종료할 때 **PID 1에게** SIGTERM 신호를 보냅니다. `exec` 없이 셸이 PID 1이면, 셸은 이 신호를 Java에게 전달하지 않을 수 있습니다. 결과적으로 Java가 연결을 정리하지 못한 채 강제 종료(SIGKILL)됩니다. `exec`를 쓰면 Java가 PID 1이 되어 직접 SIGTERM을 받고, 연결을 정리한 뒤 깔끔하게 종료합니다.
즉, 여기서 중요한 것은 "Vault가 비밀값을 준다"는 사실만이 아닙니다.
즉, 여기서 중요한 것은 "Vault가 비밀값을 준다"는 사실만이 아닙니다.
**비밀값을 파일로 렌더링하고, 셸이 그 파일을 읽고, 마지막에 앱 프로세스로 넘어간다**는 실행 모델 전체를 이해해야 합니다.
이 개념이 없으면 아래 같은 질문에 답하기 어렵습니다.
@@ -282,7 +282,7 @@ flowchart LR
### 5-4. GitOps, Kustomize, Argo CD
이 프로젝트는 "좋은 YAML을 써놨다"에서 끝나지 않습니다.
이 프로젝트는 "좋은 YAML을 써놨다"에서 끝나지 않습니다.
이 YAML을 **누가**, **어떤 기준으로**, **반복적으로** 적용하느냐가 중요합니다.
#### GitOps
@@ -322,7 +322,7 @@ Argo CD는 Git에 있는 선언을 실제 클러스터와 맞추는 실행 주
- 해당 경로의 manifest를 dev 클러스터에 동기화
- 드리프트가 생기면 다시 선언 상태로 되돌리려 함
즉, `apps/auth-server/overlays/dev`를 수정한다는 것은 단순히 파일을 고치는 것이 아니라
즉, `apps/auth-server/overlays/dev`를 수정한다는 것은 단순히 파일을 고치는 것이 아니라
**Argo CD가 나중에 실제 클러스터 상태를 바꾸게 될 선언을 수정하는 것**입니다.
이 세 가지(GitOps, Kustomize, Argo CD)가 맞물리는 전체 흐름을 그림으로 보면 이렇습니다.
@@ -332,20 +332,20 @@ flowchart LR
subgraph Developer["개발자"]
A["base/deployment.yaml 수정<br/>또는 overlay/configmap.yaml 수정"]
end
subgraph Git["Git 저장소 (Source of Truth)"]
B["base/ + overlay/<br/>= 최종 선언"]
end
subgraph ArgoCD["Argo CD"]
C["Git 감시<br/>변경 감지"] --> D["Kustomize로<br/>base + overlay 합성"]
D --> E["합성 결과와<br/>현재 클러스터 비교"]
end
subgraph Cluster["K8s 클러스터"]
F["실제 리소스<br/>Deployment, Service 등"]
end
A -->|"git push"| B
B -->|"Watch"| C
E -->|"차이 있으면<br/>kubectl apply"| F
@@ -360,7 +360,7 @@ flowchart LR
#### Kubernetes Secret
Kubernetes Secret은 Kubernetes 안에서 secret을 다루기 위한 기본 기능입니다.
Kubernetes Secret은 Kubernetes 안에서 secret을 다루기 위한 기본 기능입니다.
하지만 이 프로젝트에서는 runtime secret의 최종 해답으로 보지 않습니다.
이유:
@@ -419,7 +419,7 @@ flowchart TB
P2 --> P3["Vault가 K8s API에<br/>'이 JWT 진짜야?' 확인"]
P3 --> P4["Secret 발급"]
end
subgraph CI_경로["경로 2: CI가 Vault에 접근할 때"]
direction LR
C1["GitHub Actions<br/>Secrets에 저장된<br/>Role ID + Secret ID"] --> C2["AppRole 로그인"]
@@ -461,14 +461,14 @@ flowchart TB
KA["K8s Auth<br/>(Pod 로그인)"]
AR["AppRole Auth<br/>(CI 로그인)"]
end
AUTH["auth-server Pod"] -->|"K8s Auth로 로그인"| KA
AUTH -->|"DB 비밀번호 읽기"| KV
AUTH -->|"JWT 서명 요청"| TR
MIG["migration Job"] -->|"K8s Auth로 로그인"| KA
MIG -->|"임시 DB 계정 발급"| DB
CI["GitHub Actions"] -->|"AppRole로 로그인"| AR
CI -->|"Terraform으로<br/>policy/role/secret 설정"| KV
```
@@ -479,7 +479,7 @@ auth-server는 K8s Auth로 로그인해서 KV(고정 비밀번호)와 Transit(JW
### 5-7. Terraform, State, 멱등성, Bootstrap vs Reconcile
Terraform은 단순히 "리소스를 만드는 도구"가 아닙니다.
Terraform은 단순히 "리소스를 만드는 도구"가 아닙니다.
핵심은 **현재 상태와 원하는 상태의 차이를 계산한다**는 점입니다.
#### State
@@ -535,13 +535,13 @@ flowchart LR
B1["vault/dev<br/>mount 생성, auth backend 활성화<br/>root 수준 권한 필요"]
B2["vault-transit/dev<br/>transit key 생성, AppRole 생성<br/>root 수준 권한 필요"]
end
subgraph Reconcile["Reconcile (반복, CI가 실행)"]
direction TB
R1["vault/reconcile<br/>policy 업데이트, role 업데이트<br/>secret 복사, DB role 설정<br/>제한된 권한으로 충분"]
R2["vault-transit/reconcile<br/>policy 업데이트, role 업데이트<br/>제한된 권한으로 충분"]
end
Bootstrap -->|"구조가 세워진 뒤<br/>이후는 reconcile만"| Reconcile
```
@@ -648,12 +648,12 @@ sequenceDiagram
Note over OP,PV: 1~2단계: Seed 입력
OP->>PV: populate-workload-seeds.sh로<br/>DB 비밀번호 입력
Note over TF,WV: 3~6단계: Terraform Reconcile
TF->>PV: provider Vault에서 seed 읽기
TF->>WV: workload Vault에 secret 복사
TF->>WV: policy 생성 + K8s auth role 생성
Note over API,POD: 7~10단계: Pod 기동
API->>INJ: "vault 어노테이션 있는 Pod 정의 와슸"
INJ->>INJ: Pod에 Vault Agent 사이드카 추가
@@ -735,11 +735,11 @@ flowchart LR
MC -->|"성공 ✅"| NEXT["다음 단계로"]
MC -->|"실패 ❌"| STOP["전체 sync 중단<br/>앱 배포 안 함"]
end
subgraph Sync["② Sync 단계"]
D["auth-server Deployment<br/>앱 배포"]
end
NEXT --> D
```
@@ -802,7 +802,7 @@ sequenceDiagram
Note over Vault: 내부의 RSA 개인키로 서명<br/>개인키는 Vault 밖으로 절대 안 나감
Vault-->>Auth: 서명된 JWT 반환
Auth-->>User: JWT 토큰 전달
User->>API: JWT를 담아 API 호출
API->>Vault: "transit/keys/project-auth-jwt로<br/>공개키 읽기"
Vault-->>API: RSA 공개키 반환
@@ -866,18 +866,18 @@ flowchart TD
B2["service.yaml"]
B3["serviceaccount.yaml"]
end
subgraph Overlay["overlays/dev/ (환경별 차이)"]
O1["deployment.vault-patch.yaml<br/>automountServiceAccountToken: true<br/>command/args: 추가"]
O2["configmap.yaml (새로 추가)"]
O3["networkpolicy.yaml (새로 추가)"]
O4["namespace: auth-dev"]
end
subgraph Result["최종 배포 결과 (Kustomize 합성)"]
R1["deployment.yaml<br/>automountServiceAccountToken: true ← patch로 변경<br/>command/args: Vault 시작 명령 추가"]
end
B1 --> R1
O1 -->|"패치 적용<br/>(strategic merge)"| R1
```
@@ -975,7 +975,7 @@ base에서 `automountServiceAccountToken: false`이지만, overlay patch가 이
- `vault-injection: enabled` 라벨을 준다
- Pod Security 관련 라벨도 같이 준다
여기서 중요한 것은 `vault-injection: enabled`입니다.
여기서 중요한 것은 `vault-injection: enabled`입니다.
`argocd/applications/dev/infra/vault-agent-injector.yaml`를 보면 injector webhook은 **이 라벨이 있는 namespace에만** 동작합니다.
즉, auth-server가 Vault injection을 받는 이유는 단순히 Deployment patch 때문만이 아니라, **namespace도 injector 대상 조건을 만족**하기 때문입니다.
@@ -1034,7 +1034,7 @@ flowchart LR
AUTH -->|"JWT 서명"| VAULT["Vault Transit"]
AUTH -->|"DB R/W"| PG["Postgres"]
API -->|"공개키 읽기<br/>(issuer URI 경유)"| AUTH
style API fill:#e8f5e9
style AUTH fill:#fff3e0
```
@@ -1083,12 +1083,12 @@ flowchart TB
VAULT --> KC["Keycloak<br/>(Deployment)"]
PG --> KC_SYNC["Keycloak Client<br/>Sync Job"]
KC --> KC_SYNC
PG --> AUTH["auth-server"]
KC --> AUTH
VAULT --> AUTH
KC_SYNC -.->|"client secret 설정<br/>redirect URI 설정"| AUTH
AUTH -->|"JWT issuer"| API["api-server"]
```
@@ -1133,7 +1133,7 @@ flowchart LR
W10["wave 10<br/>Vault Transit<br/>Vault<br/>Agent Injector"] --> W20["wave 20<br/>Platform<br/>(Postgres, Keycloak)"]
W20 --> W30["wave 30<br/>auth-server"]
W30 --> W40["wave 40<br/>api-server"]
W10 -.->|"이것 없이 다음 단계로 가면<br/>secret 주입 실패"| W20
W20 -.->|"이것 없이 다음 단계로 가면<br/>DB 연결 실패"| W30
W30 -.->|"이것 없이 다음 단계로 가면<br/>JWT 검증 실패"| W40
@@ -1305,20 +1305,20 @@ flowchart TB
T_JWT["Transit Key<br/>project-auth-jwt<br/>(JWT 서명용)"]
SEED["Seed Secrets<br/>(원본 비밀번호)"]
end
subgraph Workload["Workload Vault"]
KV["KV Engine<br/>(복사된 runtime secret)"]
DB_ENG["Database Engine<br/>(동적 계정 발급)"]
K8S_AUTH["K8s Auth<br/>(Pod 인증)"]
end
T_UNSEAL -->|"Auto-Unseal<br/>마스터키 복호화"| Workload
SEED -->|"Terraform이<br/>seed를 복사"| KV
AUTH_POD["auth-server"] --> K8S_AUTH
AUTH_POD --> KV
AUTH_POD -->|"JWT 서명 요청"| T_JWT
MIG_POD["migration Job"] --> K8S_AUTH
MIG_POD --> DB_ENG
```
@@ -1483,12 +1483,12 @@ flowchart TB
subgraph 우리_PC["우리 개발 PC (Windows)"]
direction TB
WIN[Windows NT 커널<br/>바탕화면, 마우스, VS Code 등 실행]
subgraph HYPERV["Hyper-V 가상화 층"]
direction TB
LINUX[진짜 Linux 커널<br/>WSL2가 제공하는 경량 가상머신]
end
subgraph DOCKER["Docker Desktop"]
direction TB
ENGINE[Docker Engine 데몬<br/>Linux 커널 위에서 실행됨]
@@ -1497,7 +1497,7 @@ flowchart TB
ENGINE --> C3[vault 컨테이너]
end
end
WIN -->|"Hyper-V를 통해<br/>리눅스 커널 호스팅"| LINUX
LINUX -->|"커널 기능 제공<br/>(cgroups, namespaces)"| ENGINE
```
@@ -1706,17 +1706,17 @@ flowchart TB
direction TB
A["Spring Security Filter<br/>Spring MVC Controller<br/>JPA Repository<br/>DB 드라이버"]
end
subgraph 중간["중간: Application / UseCase"]
direction TB
B["로그인 UseCase<br/>회원가입 UseCase<br/>토큰 발급 UseCase"]
end
subgraph 가장_안쪽["가장 안쪽: Domain"]
direction TB
C["User 엔티티<br/>비즈니스 규칙<br/>순수 Java 코드"]
end
A -->|"의존 가능 ✅"| B
B -->|"의존 가능 ✅"| C
C -.-x|"의존 불가 ❌<br/>Domain은 Security를<br/>몰라야 함"| A
@@ -1740,14 +1740,14 @@ flowchart LR
S2["JWT 서명 검증<br/>토큰이 위조되었는지 확인"]
S3["RBAC 권한 체크<br/>일반 유저인지, 관리자인지<br/>API 접근 허용/차단"]
end
subgraph Service_역할["비즈니스 로직이 하는 일 (UseCase)"]
direction TB
U1["로그인 검증<br/>DB에서 사용자 조회"]
U2["회원 가입 처리<br/>DB에 사용자 저장"]
U3["토큰 발급<br/>Vault Transit으로 서명"]
end
Security_역할 -->|"Argument Resolver로<br/>토큰 정보만 넘겨줌"| Service_역할
```
@@ -1816,7 +1816,7 @@ sequenceDiagram
User->>Google: 구글 로그인 페이지에서 직접 로그인
Google->>User: 로그인 성공! 인가 코드를 들고<br/>우리 서버의 콜백 URL로 돌아가거라
User->>Spring: 인가 코드(임시 교환권)를 들고 콜백 URL로 돌아옴
Note over Spring,Google: 여기서부터는 서버 대 서버 통신 (사용자 브라우저를 거치지 않음)
Spring->>Google: "이 인가 코드, 진짜 너네가 준 거 맞지?<br/>Access Token으로 바꿔줘"
Google-->>Spring: Access Token + ID Token 반환
@@ -1966,7 +1966,7 @@ flowchart TB
S3[auth-server Pod 3] -->|"Flyway: ALTER TABLE users..."| DB
S4["... Pod 4~10도 동시에"] -->|"Flyway: ALTER TABLE users..."| DB
end
DB -->|"💥 Lock 경합!<br/>누가 먼저야?<br/>DDL Lock 충돌!"| DEAD[배포 데드락<br/>일부 Pod는 마이그레이션 성공<br/>일부 Pod는 Lock 대기 중 타임아웃]
```
@@ -1980,17 +1980,17 @@ Flyway는 내부적으로 DB Lock을 사용해서 중복 실행을 방지하려
flowchart TD
subgraph 해결_구조["이 프로젝트의 배포 흐름"]
direction TB
subgraph PreSync["1단계: PreSync (앱 배포 전)"]
JOB[auth-db-migration Job<br/>Flyway 실행<br/>딱 1개만 실행됨] -->|"스키마 변경 완료"| DB2[(PostgreSQL)]
end
subgraph MainSync["2단계: Main Sync (스키마 준비 완료 후)"]
S1b[auth-server Pod 1<br/>Flyway 안 함] --> DB2
S2b[auth-server Pod 2<br/>Flyway 안 함] --> DB2
S3b[auth-server Pod 3<br/>Flyway 안 함] --> DB2
end
PreSync -->|"Job 성공해야<br/>다음 단계 진행"| MainSync
end
```
@@ -2097,21 +2097,21 @@ flowchart TB
ETCD["etcd / SQLite<br/>클러스터의 모든 상태를<br/>저장하는 데이터베이스"]
SCHED["Scheduler<br/>새 Pod를 어느 노드에<br/>배치할지 결정"]
CM["Controller Manager<br/>선언된 상태와 현재 상태의<br/>차이를 감지하고 조정"]
API <--> ETCD
SCHED --> API
CM --> API
end
subgraph W1["Worker Node 1 (공장)"]
direction TB
KL1["kubelet<br/>API Server의 지시를 받아<br/>컨테이너를 생성/삭제"]
KP1["kube-proxy<br/>네트워크 규칙을 관리<br/>(Service → Pod 라우팅)"]
CR1["containerd<br/>실제 컨테이너를 실행하는<br/>런타임 엔진"]
KL1 --> CR1
end
subgraph W2["Worker Node 2 (공장)"]
direction TB
KL2["kubelet"]
@@ -2119,7 +2119,7 @@ flowchart TB
CR2["containerd"]
KL2 --> CR2
end
API -->|"Watch 스트림으로<br/>이벤트 전달"| KL1
API -->|"Watch 스트림으로<br/>이벤트 전달"| KL2
```
@@ -2151,11 +2151,11 @@ Kubernetes는 **Watch 방식**을 씁니다. 왜냐하면 Worker Node가 100대,
sequenceDiagram
participant KL as kubelet (Worker Node)
participant API as API Server (Control Plane)
KL->>API: "내 노드에 관련된 변경사항을<br/>Watch 스트림으로 구독합니다"
Note over KL,API: HTTP Long-Poll 연결이 유지됨
API-->>KL: (아무 일 없으면 조용)
Note over API: 사용자가 kubectl apply로<br/>새 Deployment 생성
API->>API: Scheduler가 "Worker Node 1에 배치" 결정
API-->>KL: "새 Pod를 실행하세요" 이벤트 푸시
@@ -2179,14 +2179,14 @@ flowchart LR
C1["Cloud Controller Manager<br/>AWS, GCP 연동 코드"]
S1["Storage Driver<br/>다양한 CSI 드라이버 포함"]
end
subgraph K3S["K3s (경량)"]
direction TB
E2["SQLite<br/>단일 파일 DB<br/>단일 바이너리 내장<br/>수 MB 메모리"]
C2["없음<br/>클라우드 연동 코드 제거"]
S2["Local Path Provisioner<br/>기본 탑재, 간단한 로컬 볼륨"]
end
K8S -->|"이것이 100MB 바이너리 하나로<br/>압축된 것이 K3s"| K3S
```
@@ -2227,7 +2227,7 @@ sequenceDiagram
participant APP as auth-server Pod<br/>(다른 Pod에서 vault를 호출)
participant IPTABLES as iptables 규칙<br/>(kube-proxy가 관리)
participant POD as vault Pod<br/>(실제 컨테이너)
Note over APP: configmap에 적힌 주소:<br/>vault.vault.svc.cluster.local:8200
APP->>APP: DNS 조회: vault.vault.svc.cluster.local<br/>→ CoreDNS가 10.43.x.x (Service의 ClusterIP) 반환
APP->>IPTABLES: 10.43.x.x:8200으로 패킷 전송
@@ -2283,14 +2283,14 @@ flowchart TB
P2["migration Job Pod<br/>Pod IP: 10.42.0.6"]
F1["Flannel<br/>VXLAN 터널 엔드포인트"]
end
subgraph Node2["Worker Node 2 (물리 IP: 192.168.1.11)"]
direction TB
P3["vault Pod<br/>Pod IP: 10.42.1.3"]
P4["postgres Pod<br/>Pod IP: 10.42.1.4"]
F2["Flannel<br/>VXLAN 터널 엔드포인트"]
end
F1 <-->|"VXLAN 터널<br/>Pod 패킷을 캡슐화해서<br/>물리 네트워크 위로 전달"| F2
P1 -.-|"10.42.0.5 → 10.42.1.3<br/>다른 노드지만<br/>직접 통신 가능"| P3
```
@@ -2370,16 +2370,16 @@ flowchart LR
subgraph 사용자_요청["개발자가 선언하는 것"]
PVC["PVC<br/>(PersistentVolumeClaim)<br/>'5GB 볼륨 하나 주세요'"]
end
subgraph 중간_매개["K8s가 처리하는 것"]
SC["StorageClass<br/>'볼륨을 어떤 방식으로<br/>만들지 정의한 템플릿'"]
end
subgraph 실제_저장소["실제로 생성되는 것"]
PV["PV<br/>(PersistentVolume)<br/>'실제 5GB 디스크 공간'"]
DISK["노드의 로컬 디스크<br/>또는 NFS/클라우드 EBS"]
end
PVC -->|"① '5GB 주세요'<br/>StorageClass 참조"| SC
SC -->|"② Provisioner가<br/>실제 볼륨 생성"| PV
PV -->|"③ 바인딩 완료"| PVC
@@ -2453,19 +2453,19 @@ flowchart LR
S2["메모리: 마스터 키 없음<br/>❌ 복호화 불가"]
S3["모든 API 요청 → 503 거부"]
end
subgraph UNSEAL["Unseal 과정"]
direction TB
U1["마스터 키를<br/>메모리에 올림"]
end
subgraph UNSEALED["Unsealed 상태"]
direction TB
US1["디스크: 여전히 암호화 상태"]
US2["메모리: 마스터 키 보유<br/>✅ 요청 시 복호화 가능"]
US3["모든 API 요청 → 정상 처리"]
end
SEALED -->|"Unseal 과정<br/>(키 제공)"| UNSEAL
UNSEAL --> UNSEALED
```
@@ -2482,7 +2482,7 @@ flowchart LR
sequenceDiagram
participant WV as Workload Vault<br/>(우리가 쓰는 Vault)
participant PV as Provider Vault<br/>(vault-transit)
Note over WV: Pod 기동됨 → Sealed 상태
WV->>WV: 디스크에서 암호화된<br/>마스터 키를 읽음
WV->>PV: "이 암호화된 마스터 키를<br/>Transit 엔진으로 복호화해줘"
@@ -2563,26 +2563,26 @@ sequenceDiagram
participant KUBELET as kubelet
participant AGENT as Vault Agent<br/>(사이드카 컨테이너)
participant VAULT as Vault Server
USER->>API: "auth-server Pod를 만들어줘"
API->>API: 어노테이션 확인:<br/>vault.hashicorp.com/agent-inject: "true"
API->>INJECTOR: "이 Pod 정의를 보내는데,<br/>수정할 게 있으면 수정해줘"
Note over INJECTOR: Pod 정의를 분석<br/>vault 관련 어노테이션 발견
INJECTOR->>INJECTOR: Pod 정의에 사이드카 컨테이너<br/>(Vault Agent) 추가
INJECTOR->>INJECTOR: 공유 볼륨<br/>(/vault/secrets) 추가
INJECTOR-->>API: 수정된 Pod 정의 반환
API->>KUBELET: 수정된 Pod를 생성하라
KUBELET->>KUBELET: 원래 컨테이너 + Vault Agent 사이드카 함께 실행
AGENT->>VAULT: K8s ServiceAccount JWT로 인증 요청
VAULT->>VAULT: K8s API에 "이 JWT 진짜야?" 확인
VAULT-->>AGENT: Vault 토큰 발급
AGENT->>VAULT: 토큰으로 secret 요청<br/>"kv/data/dev/platform/postgres/auth-server"
VAULT-->>AGENT: secret 데이터 반환
AGENT->>AGENT: Go 템플릿으로 렌더링<br/>→ /vault/secrets/runtime-env 파일 생성
Note over KUBELET: auth-server 컨테이너가<br/>/vault/secrets/runtime-env 파일을<br/>읽어서 환경변수로 로드
```
@@ -2657,7 +2657,7 @@ flowchart LR
subgraph 위험한_방식["일반적인 방식 (키 유출 위험)"]
A1["앱이 키를 다운로드"] --> A2["앱 메모리에 키 올림"] --> A3["앱이 직접 서명"]
end
subgraph 안전한_방식["Transit Engine 방식"]
B1["앱이 Vault API 호출<br/>'이 데이터에 서명해줘'"] --> B2["Vault가 내부에서<br/>키로 서명 수행"] --> B3["서명 결과만 반환<br/>키는 Vault 밖으로 안 나감"]
end
@@ -2697,7 +2697,7 @@ flowchart TB
K1["키: project-auth-jwt<br/>(RSA 키, JWT 서명용)"]
K2["키: workload-vault-dev-unseal<br/>(AES 키, Unseal용)"]
end
AUTH["auth-server Pod"] -->|"'이 JWT에 서명해줘'<br/>transit/sign/project-auth-jwt"| K1
VAULT["Workload Vault"] -->|"'이 마스터키 복호화해줘'<br/>transit/decrypt/workload-vault-dev-unseal"| K2
```
@@ -2748,15 +2748,15 @@ flowchart TD
subgraph 선언["개발자가 작성한 것 (main.tf)"]
TF["'auth-server-dev policy를 만들어라'<br/>'auth-server K8s auth role을 만들어라'<br/>'postgres secret을 복사해라'"]
end
subgraph state["State 파일 (.tfstate)"]
ST["'auth-server-dev policy: 만들었음 ✅'<br/>'auth-server K8s auth role: 만들었음 ✅'<br/>'postgres secret: 만들었음 ✅'"]
end
subgraph 실제["실제 인프라 (Vault)"]
REAL["auth-server-dev policy 존재<br/>auth-server K8s auth role 존재<br/>postgres secret 존재"]
end
TF -->|"terraform plan<br/>선언 vs State 비교"| ST
ST -->|"terraform apply<br/>차이만 실제에 적용"| REAL
REAL -->|"적용 결과를<br/>State에 기록"| ST
@@ -3038,7 +3038,3 @@ sleep 60 # 60초면 되겠지...?
- `2>&1`의 의미는 무엇인가? `>&2`와는 무엇이 다른가? (힌트: 표준 에러 리다이렉션 방향의 차이)
- `terraform apply -auto-approve`는 plan 확인 없이 바로 적용한다. CI에서는 왜 이것을 쓰는가? 사람이 직접 실행할 때는 왜 위험한가?
- `reconcile-vault-dev.sh`에서 `transit_tf_token="$(transit_login)"`을 왜 두 번 호출하는가? 한 번이면 안 되는가? (힌트: policy 업데이트 후 새 토큰이 필요)
File diff suppressed because it is too large Load Diff
+6
View File
@@ -0,0 +1,6 @@
# Archived documentation
Files in this directory describe historical repository states. Paths,
workflows, credentials and operational commands may no longer exist.
Do not execute archived procedures. Use the root README and `docs/runbooks/`.
@@ -0,0 +1,8 @@
# Legacy policies
These policies were used by permanent AppRole credentials and could modify
mounts, auth backends and their own policies. They are retained only to make
the state and credential migration auditable.
No current Terraform root references files in this directory. Revoke the
legacy AppRoles after the JWT/Kubernetes-authenticated workflow is verified.
@@ -0,0 +1,12 @@
# ADR 0001: Internal Gitea is canonical
Status: accepted
The internal repository
`https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops` is the only
writable deployment source.
Argo CD and Gitea Actions use this URL. A GitHub copy may exist only as a
read-only mirror with monitored replication; it must never be an independent
deployment branch. GHCR remains an image registry and does not require GitHub
to host the GitOps source.
@@ -0,0 +1,55 @@
# ADR 0002: Terraform state ownership
Status: accepted
Updated: 2026-07-26
Terraform의 현재 범위는 Vault API 객체입니다. Kubernetes 리소스는 Argo
CD가 소유하며, machine/network provisioning은 provider와 운영 경계가
확정될 때 별도 root로 추가합니다.
`dev-k3s`는 정확히 세 state를 사용합니다.
| 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 |
Resource 또는 Vault API path 하나는 한 state에만 속합니다. State 사이는
이름 contract와 실행 순서만 공유하며 `terraform_remote_state`로 서로의
snapshot을 읽지 않습니다. Backend는 encryption, versioning, access
control, locking을 제공해야 합니다.
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에서 제거합니다.
+25
View File
@@ -0,0 +1,25 @@
# ADR 0003: Vault topology
Status: accepted for dev, production decision pending
동일한 단일 노드 K3s 안의 두 Vault는 failure domain을 분리하지 못하면서
초기화, Transit credential, rotation과 staged apply 절차를 추가했다.
따라서 `dev-k3s`는 단일 self-hosted Vault로 단순화한다.
Dev profile:
- single-node integrated Raft
- Shamir 1-of-1 init/unseal
- TLS 미적용
- 명시적 backup/recovery runbook
이 구성은 production에 사용할 수 없다. production은 다음 중 하나를
선택해야 한다.
- managed Vault
- workload cluster 밖의 독립 HA Vault
- 최소 3-node integrated-Raft + TLS + KMS/HSM auto-unseal + PDB,
anti-affinity와 정기 restore exercise
production Vault가 결정되기 전에는 production manifest와 Terraform root를
만들지 않는다.
@@ -0,0 +1,44 @@
# ADR 0004: Single Argo CD root and stage-gated ApplicationSets
Status: accepted
Updated: 2026-07-26
Argo CD 설치 후 bootstrap 전용 `gitops-control-plane` AppProject와 단일
root Application을 순서대로 수동 seed합니다. Root는
`gitops/clusters/dev-k3s`를 source로 사용합니다. 이 cluster root가
`gitops/platform/control-plane/argocd`의 AppProject와 ApplicationSet을 소유하고,
ApplicationSet이 platform addon/shared service, system, workload
Application을 생성합니다. Bootstrap Project는 canonical repository,
`argocd` namespace와 AppProject/ApplicationSet kind만 허용합니다.
반복 `kubectl apply`와 category별 root wrapper는 사용하지 않습니다.
Routine deployment는 Git merge만으로 시작합니다.
각 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`를 사용하지 않습니다.
+33
View File
@@ -0,0 +1,33 @@
# ADR 0005: Cluster-first repository layout
Status: accepted
Updated: 2026-07-26
현재는 단일 `dev-k3s`와 소수 workload를 다루므로 GitOps configuration
monorepo를 유지합니다. Application source repository와 deployment
configuration repository는 분리합니다. 이 저장소 자체는 독립 reference
lab이며 범용 platform product로 간주하지 않습니다.
- `gitops/platform`, `gitops/apps/systems`, `gitops/apps/workloads`:
ownership별 base; 환경 중립은 목표 contract
- `gitops/clusters/<cluster>`: GitOps controller가 읽는 cluster root
- `gitops/clusters/<cluster>/overlays`: cluster-specific final composition
- `gitops/platform/control-plane/argocd/projects`: Argo 권한 경계
- `gitops/platform/control-plane/argocd/application-sets`: reconciliation inventory
- `infrastructure`: Kubernetes manifest와 분리된 external API IaC
- `bootstrap`: controller가 존재하기 전의 최소 seed
`gitops/clusters`가 배포 가능한 최종 상태를 소유합니다. Argo CD는
catalog 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은 유지하지 않습니다.
+33
View File
@@ -0,0 +1,33 @@
# ADR 0006: Gateway API first, Istio deferred
Status: accepted
현재 단일 노드 K3s와 auth/api 중심 workload에는 service mesh 운영 비용을
정당화할 mTLS identity, L7 authorization, canary traffic policy 또는
multi-team 요구가 없다. 이번 개편에는 Istio를 설치하지 않는다.
선행 작업:
1. Traefik Gateway API provider와 GatewayClass 검증
2. Ingress를 Gateway/HTTPRoute로 이관
3. north-south TLS
4. 내부 호출의 ingress hairpin 제거
5. Vault/PostgreSQL native TLS
6. NetworkPolicy regression test와 observability/SLO
Istio 요구가 실제화되면 sidecar가 아니라 ambient mode로 제한 pilot한다.
초기 범위는 api-server와 auth-server이며 Vault, Vault injector,
PostgreSQL은 제외한다. ztunnel L4부터 시작하고 L7 정책이 필요할 때만
waypoint를 추가한다.
다음 기능 요구 중 두 개 이상과 운영 선행조건이 모두 충족될 때 ADR을
재검토한다.
- ServiceAccount identity 기반 east-west mTLS
- path/JWT 기반 L7 authorization
- canary traffic split/retry/timeout/outlier detection
- 지속적인 서비스·namespace·팀 증가
- application instrumentation만으로 해결하기 어려운 장애 분석
현재 Ingress를 즉시 제거하지 않는다. TLS, DNS, GatewayClass 계약이
확정되기 전 가상의 Gateway 설정을 배포하지 않기 위함이다.
@@ -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를 다음 소유권으로 분류합니다.
- `gitops/platform`: 여러 system이 사용할 수 있고 독립 lifecycle을 가진
cluster capability
- `gitops/apps/systems`: 특정 bounded context가 소유하는 backing services와
domain configuration
- `gitops/apps/workloads`: 별도 source repository와 release digest를 가진
first-party 실행 애플리케이션
- `gitops/clusters/<cluster>/overlays`: 위 base에 namespace, image, host,
secret reference, network boundary를 결합한 최종 구성
Vault는 `gitops/platform/shared-services/vault`에 둡니다. Sealed Secrets와 Vault
Agent Injector는 cluster addon inventory로 관리합니다. PostgreSQL,
Keycloak, realm/client sync는 Project Auth 전용이므로
`gitops/apps/systems/auth-system`으로 이동합니다. `auth-server`
`api-server``gitops/apps/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`를 따른다.
@@ -0,0 +1,45 @@
# ADR 0008: Adopt the Kubernetes template lifecycle layout
Status: accepted
Date: 2026-07-26
## Context
기존 저장소는 `clusters`, `platform`, `systems`, `workloads`, `iac/terraform`,
`policies``hack`을 각각 최상위에 두었습니다. 리소스 소유권은 구분했지만
bootstrap, infrastructure, GitOps desired state의 수명주기 경계가 저장소
최상위에서 일관되게 드러나지 않았습니다.
## Decision
`k8s-template`의 수명주기 구조를 canonical repository layout으로 채택합니다.
- Argo CD 최초 seed는 `bootstrap/gitops/argocd`에 둡니다.
- Vault Terraform component와 실행 root는 각각
`infrastructure/components``infrastructure/live`에 둡니다.
- Kubernetes desired state는 `gitops/platform`, `gitops/apps`,
`gitops/clusters` 아래에만 둡니다.
- 기존 system/workload 소유권 분류는 `gitops/apps/systems`
`gitops/apps/workloads` 하위에서 유지합니다.
- 환경별 Vault ACL은 이를 소비하는 `infrastructure/live` state root의
`policies`에 함께 둡니다. Kubernetes admission 정책용
`gitops/policies`와 혼합하지 않습니다.
- `gitops/clusters/dev-k3s`를 Argo CD bootstrap root의 유일한 source로
사용하고, 이 root가 permission-scoped ApplicationSet control plane을
조립합니다.
- 공통 `_template`, 예제, 구조/보안 검증을 유지하고 프로젝트별 검증을 그
위에 추가합니다.
## Consequences
- 최상위 경로만으로 bootstrap, infrastructure, GitOps lifecycle을 구분할
수 있습니다.
- 기존 AppProject, ApplicationSet, `autoSync` gate와 세 Vault state의
소유권은 유지됩니다.
- backend 예시는 각 live root에, Vault ACL은 소비 state에 가까이 위치합니다.
- Argo CD source path와 Terraform module source가 변경되므로 이미 연결된
live 시스템의 이관은 별도 diff, orphan/prune 검토와 승인 없이 실행하지
않습니다.
- 이 ADR의 구현은 Git 작업 트리에서만 수행하며 live cluster, Vault,
Terraform backend를 변경하지 않습니다.
+26
View File
@@ -0,0 +1,26 @@
# Architecture Decision Records
프로젝트의 장기 구조에 영향을 주는 선택은 ADR로 남깁니다.
파일명은 `NNNN-kebab-case-title.md`를 사용하고 다음 형식을 따릅니다.
```markdown
# NNNN. 제목
- 상태: 제안 | 승인 | 폐기 | 대체
- 날짜: YYYY-MM-DD
- 결정자: 팀 또는 역할
## 배경
## 결정
## 결과
## 대안
```
기존 결정을 바꿀 때 문서를 지우지 말고 새 ADR에서 이전 ADR을 대체했다고
표시합니다.
현재 프로젝트 결정은 이 디렉터리의 `0001`부터 순서대로 관리합니다.
+146
View File
@@ -0,0 +1,146 @@
# Getting Started
이 문서는 이 저장소가 채택한 공통 템플릿 구조와 새 catalog 항목 생성 규칙을
설명합니다. Project GitOps의 실제 입문 순서와 운영 gate는
[`intern-guide.md`](intern-guide.md)와 runbook을 우선합니다.
## 예제를 먼저 확인하기
실제 파일을 만들기 전에 두 예제를 설계 참고 자료로 사용합니다.
1. `examples/minimal`에서 단일 환경의 canonical 폴더와 조립 방식을 확인합니다.
2. 다음 명령으로 platform과 app이 cluster root에서 합쳐지는 결과를 확인합니다.
```bash
kubectl kustomize examples/minimal/gitops/clusters/dev/main
```
3. 다중 계정·리전·클러스터가 필요하면 `examples/scaled/README.md`에서 경로와
state 분리 기준을 선택합니다.
4. 실제 파일은 `examples`에서 복사하지 않고 각 책임 폴더의 `_template`에서
생성합니다.
```text
examples ──▶ 구조 선택 ──▶ _template 복사 ──▶ live/clusters 구현
```
## 1. 프로젝트 선택 기록
구현을 추가하기 전에 다음 항목을 결정하고 `docs/decisions`에 ADR을 작성합니다.
- cloud/on-prem provider와 account/project 구조
- Terraform, OpenTofu, Pulumi 등 IaC 엔진
- Kustomize 중심 또는 Helm 사용 범위
- Flux 또는 Argo CD 등 GitOps 컨트롤러
- External Secrets 또는 SOPS 등 비밀 관리 방식
- admission policy와 observability 운영 범위
선택하지 않은 도구의 빈 폴더를 모두 만들 필요는 없습니다.
Terraform/OpenTofu를 선택했다면 IaC 파일을 추가하기 전에 다음 선택 파일을
만들고 한 값만 활성화합니다.
```bash
cp infrastructure/.iac-engine.example infrastructure/.iac-engine
```
`.iac-engine`에는 주석을 제외하고 `tofu` 또는 `terraform` 한 줄만 남깁니다.
선택한 도구와 version을 CI에도 설치·고정하고 project-specific
`init -backend=false`/`validate` 검사를 추가합니다.
## 2. 프로젝트 메타데이터 설정
1. `README.md`의 제목과 프로젝트 범위를 바꿉니다.
2. `.github/CODEOWNERS.example`을 실제 소유자로 수정한 뒤 `CODEOWNERS`로
이름을 바꿉니다.
3. `SECURITY.md`에 조직의 보안 연락처와 SLA를 추가합니다.
4. 선택한 도구 버전을 프로젝트의 버전 관리 방식으로 고정합니다.
5. branch protection과 required check를 설정합니다.
## 3. Foundation bootstrap
`bootstrap/foundation` 아래에 remote state, locking, 초기 CI identity 등
다른 인프라가 의존하는 최소 구성을 작성합니다.
Foundation은 일반 infrastructure state와 분리하고 변경 권한을 좁게 유지합니다.
이미 조직 공통 foundation이 있다면 이 폴더에는 외부 의존 계약과 초기화 방법만
문서화해도 됩니다.
## 4. Infrastructure 작성
작은 프로젝트는 component와 live root만으로 시작합니다.
```bash
cp -R infrastructure/components/_template infrastructure/components/kubernetes-cluster
mkdir -p infrastructure/live/dev
cp -R infrastructure/live/_template infrastructure/live/dev/cluster
```
동일한 조합이 여러 환경에서 반복될 때만 stack을 추가합니다.
```bash
cp -R infrastructure/stacks/_template infrastructure/stacks/cluster
```
`live` root마다 backend/state를 분리하고, provider credential은 파일에 저장하지
않습니다.
## 5. Desired state 조립
필요한 catalog 템플릿을 복사합니다.
```bash
cp -R gitops/platform/_template gitops/platform/core
cp -R gitops/apps/_template gitops/apps/example-api
mkdir -p gitops/clusters/dev
cp -R gitops/clusters/_template gitops/clusters/dev/main
```
component의 `base`에 공통값을 두고, 환경 차이가 있을 때만 overlay를 추가합니다.
마지막으로 cluster `kustomization.yaml`이 사용할 component를 참조하게 합니다.
복사된 README의 `__REPLACE_ME_*__` 값을 모두 실제 메타데이터로 바꿉니다.
controller에 연결하기 전에 실제 cluster root를 로컬에서 렌더해 확인합니다.
## 6. GitOps bootstrap
desired-state root가 준비되고 클러스터가 생성되면 `bootstrap/gitops`에서 GitOps
컨트롤러 하나를 선택해 설치합니다. 이 단계에는 다음만 포함합니다.
- controller 설치 또는 설치 선언
- repository/OCI source 연결
- 검증된 `gitops/clusters/<...>` root reconcile 연결
- controller가 secret manager에 접근하는 최소 identity
일반 platform addon과 application은 이 단계에 넣지 않습니다.
## 7. 검증
```bash
make doctor
make check
kubectl kustomize examples/minimal/gitops/clusters/dev/main
kubectl kustomize gitops/clusters/dev/main
```
프로젝트에서 실제 IaC, Helm, policy 파일을 추가하면 필요한 validator를
`scripts/validate.sh`에 명시적으로 추가하고 CI에서도 같은 `make check`를
호출합니다. 도구가 없을 때 조용히 성공하도록 만들지 않습니다.
첫 환경은 다음 조건을 모두 만족하면 완료된 것으로 봅니다.
- 실제 `live` root의 대상, state, owner와 실행 절차가 작성되어 있다.
- 실제 cluster root가 필요한 catalog base/overlay를 참조하고 비어 있지 않다.
- cluster root 렌더 결과와 IaC plan이 리뷰 가능하다.
- GitOps bootstrap root가 `_template`이 아닌 실제 cluster root를 가리킨다.
- 비밀 관리, rollback과 담당자 연락 경로가 문서화되어 있다.
## 8. 운영 준비
- production apply 승인 및 concurrency lock
- backup/restore와 disaster recovery runbook
- cluster와 addon upgrade 정책
- secret rotation과 접근 감사
- alert routing과 담당자
- 비용, 용량, SLO 기준
운영 준비가 끝나기 전에는 예제 값을 production에 재사용하지 않습니다.
+117
View File
@@ -0,0 +1,117 @@
# Intern guide
## 이 저장소의 역할
이 저장소는 Project Auth를 예제로 한 독립 GitOps reference lab입니다.
애플리케이션 소스나 범용 production platform이 아닙니다. 현재 지원하는
환경은 `dev-k3s` 하나입니다.
서로 다른 세 reconciliation 경계를 먼저 구분합니다.
1. Gitea `main`이 승인된 desired-state revision을 저장합니다.
2. Argo CD가 그 revision의 Kubernetes 리소스를 지속적으로 맞춥니다.
3. Terraform이 승인된 실행 환경에서 Vault API 객체를 관리합니다.
GHCR은 빌드된 image를 보관할 뿐 desired-state source가 아닙니다. Argo
CD가 Terraform을 실행하지 않으며 CI가 routine deployment를 위해
`kubectl apply`를 호출하지 않습니다. Secret 값도 Git이나 Terraform을
통과하지 않습니다.
## 디렉터리를 고르는 법
- 여러 system이 공유하는 cluster capability: `gitops/platform`
- Project Auth bounded context 전용 backing service:
`gitops/apps/systems/auth-system`
- 별도 source repository에서 빌드하는 서버: `gitops/apps/workloads`
- Dev namespace, digest, host, Vault annotation: `gitops/clusters/dev-k3s/overlays`
- Vault API 객체: `infrastructure/components``infrastructure/live`
현재 Vault는 platform shared service, PostgreSQL과 Keycloak은 auth-system,
`auth-server``api-server`는 workload입니다. 설치 순서나 중요도로
`foundation`을 만들지 않습니다. 자세한 기준은
`docs/architecture/repository-taxonomy.md`에 있습니다.
## 안전한 변경 흐름
1. `refactor/...`, `feat/...`, `fix/...` branch에서 변경합니다.
2. `make validate`를 실행합니다.
3. Rendered manifest 또는 Terraform plan을 검토합니다.
4. 내부 Gitea에 PR을 생성합니다.
5. 승인 후 `main`에 merge합니다.
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에서 관리
- 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 gitops/clusters/dev-k3s/overlays/workloads/auth-server
kubectl kustomize gitops/clusters/dev-k3s/overlays/systems/auth-system
kubectl kustomize gitops/clusters/dev-k3s/overlays/platform/vault
```
전체 정적 검증:
```bash
make validate
```
Backend 없이 Terraform configuration 검증:
```bash
for terraform_root in vault-foundation vault-workloads vault-database; do
terraform_data_dir="$(mktemp -d)"
TF_DATA_DIR="$terraform_data_dir" \
terraform -chdir="infrastructure/live/dev-k3s/${terraform_root}" \
init -backend=false -input=false -lockfile=readonly
TF_DATA_DIR="$terraform_data_dir" \
terraform -chdir="infrastructure/live/dev-k3s/${terraform_root}" validate
rm -rf "$terraform_data_dir"
done
```
일반적으로는 같은 검사를 포함한 `make validate`를 사용합니다. 위 예는
provider data를 repository의 `.terraform`에 남기지 않습니다.
실제 plan은 승인된 backend와 identity를 준비한 뒤 수행합니다.
```bash
make terraform-plan \
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는 전용 branch와 PR을 만들며
`main`에 직접 쓰지 않습니다.
## 읽는 순서
1. `README.md`
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/decisions/`
8. 수행하려는 작업의 runbook
2026-07-26 리팩터링은 repository에서만 구현·검증했으며 실제 cluster에
적용하지 않았습니다. Live migration을 연습 과제로 실행하지 않습니다.
+16
View File
@@ -0,0 +1,16 @@
# Runbooks
운영자가 긴급 상황에서도 그대로 실행할 수 있는 절차를 둡니다. 프로젝트를
운영하기 전에 최소한 다음 runbook을 준비합니다.
- foundation/state 접근 복구
- 실패한 plan/apply 복구와 state lock 처리
- GitOps controller 복구와 reconciliation 중지/재개
- cluster 및 핵심 addon upgrade/rollback
- secret rotation과 credential 노출 대응
- backup restore와 disaster recovery
- 인증서, DNS, ingress 장애 대응
- 관측성 또는 alert pipeline 장애 대응
각 문서는 `목적`, `사전 조건`, `영향`, `절차`, `검증`, `롤백`,
`에스컬레이션` 섹션을 포함해야 합니다.
+15
View File
@@ -0,0 +1,15 @@
# Runbook 제목
## 목적
## 사전 조건
## 영향
## 절차
## 검증
## 롤백
## 에스컬레이션
+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과 별도 승인 없이는
사용하지 않습니다.
+341
View File
@@ -0,0 +1,341 @@
# Bootstrap an empty dev-k3s cluster
이 runbook은 폐기 가능한 빈 개발 클러스터만 대상으로 합니다. Production과
기존 live cluster migration에는 사용하지 않습니다.
> 2026-07-26 repository 리팩터링 중에는 아래 절차를 실행하지 않았습니다.
> 이 문서는 승인된 future bootstrap 절차이며 명령 예시는 자동 실행 대상이
> 아닙니다.
## 1. Preflight
```bash
kubectl config current-context
kubectl cluster-info
make validate
```
의도한 빈 dev cluster가 아니면 중단합니다. 내부 Gitea가 private이면 Argo
CD가 root repository를 읽을 수 있는 read-only credential을 외부 secret
authority에서 먼저 provision해야 합니다. Credential은 이 저장소에
commit하지 않습니다.
세 remote state backend 파일을 준비합니다.
```bash
mkdir -p .local/terraform-backend/dev-k3s
cp infrastructure/live/dev-k3s/vault-foundation/backend.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
cp infrastructure/live/dev-k3s/vault-workloads/backend.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-workloads.s3.hcl
cp infrastructure/live/dev-k3s/vault-database/backend.s3.hcl.example \
.local/terraform-backend/dev-k3s/vault-database.s3.hcl
```
실제 bucket, endpoint와 workload identity를 설정합니다. Backend
credential은 파일에 넣지 않습니다. 세 backend key가 서로 다르고 locking이
활성화됐는지 확인합니다.
## 2. Argo CD와 root Application
```bash
make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
kubectl -n argocd get appproject gitops-control-plane
kubectl -n argocd get application project-gitops-control-plane
kubectl -n argocd get applicationsets
```
직접 cluster mutation은 pinned Argo CD 설치, 제한된
`gitops-control-plane` AppProject, root Application seed뿐입니다.
Bootstrap script는 이 순서를 지킵니다. Root가 네 AppProject와 네
ApplicationSet을 만들고, ApplicationSet이 child Application을 생성합니다.
초기 inventory에서 Sealed Secrets와 Vault만 `autoSync: "true"`입니다.
Vault Agent Injector, `auth-system`, `auth-server`, `api-server` gate는
닫힌 상태여야 합니다.
### Sealed Secrets key 준비
Checked-in `ghcr-regcred` ciphertext는 암호화에 사용한 controller private
key로만 복호화할 수 있습니다. 기존 key backup이 있으면 workload gate를
열기 전에 복원하고, 없으면 새 controller certificate와 원본 credential
authority를 사용해 두 SealedSecret을 다시 seal한 PR을 merge합니다.
[Sealed Secrets recovery runbook](sealed-secrets-recovery.md)을 따르며 평문
GHCR credential을 Git이나 log에 남기지 않습니다.
## 3. Dev Vault 초기화
Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서
port-forward합니다. 이는 최초 dev bootstrap용이며 routine runner 모델이
아닙니다.
```bash
kubectl -n vault wait --for=create pod -l app=vault --timeout=300s
kubectl -n vault port-forward deployment/vault 8200:8200
```
별도 terminal:
```bash
export VAULT_ADDR=http://127.0.0.1:8200
./scripts/vault-init.sh init
```
`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody에 복사합니다.
Dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과
분리합니다. Local working copy는 root revoke 때까지 mode `0600`으로
유지하며 shell history나 CI log에 token을 출력하지 않습니다.
## 4. Vault foundation
Foundation은 초기 root token으로 한 번 적용합니다.
```bash
export TF_VAR_vault_addr="$VAULT_ADDR"
export TF_VAR_vault_token="$(
jq -r '.root_token' .local/vault/dev-k3s-init.json
)"
make terraform-plan \
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl
make terraform-apply \
TF_ROOT=vault-foundation \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-foundation.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-foundation
```
Plan에는 mounts, auth configuration, workloads/database automation
policy와, OIDC/JWT를 명시적으로 구성한 경우에만 분리된 CI JWT role이
있어야 합니다. Workload runtime policy, workload Kubernetes role,
application Transit key, database connection이 보이면 중단합니다.
CI JWT를 구성할 때 workloads/database exact claim map은 repository와
protected ref를 묶고, 최소 한 공통 job discriminator key에 서로 다른 값을
가져야 합니다. 실제 issuer token payload로 그 claim을 확인하지 못하면
OIDC/JWT 입력을 비워 둡니다.
CI JWT auth를 구성했다면 Foundation이 만든 두 delegated identity에 실제
로그인해 허용/거부 capability를 확인합니다. 구성하지 않았다면 정책
capability를 검사하고 승인된 관리자가 발급한 bootstrap용 short-lived
token을 사용합니다.
- Workloads identity는 승인된 workload policy/role와
`transit/keys/project-auth-jwt`만 변경할 수 있어야 합니다.
- Database identity는 승인된 `database/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
echo
read -r -s -p "Keycloak database password: " KEYCLOAK_DB_PASSWORD
echo
read -r -s -p "Keycloak bootstrap admin password: " KEYCLOAK_ADMIN_PASSWORD
echo
read -r -s -p "Auth-server Keycloak client secret: " KEYCLOAK_CLIENT_SECRET
echo
secret_file="$(mktemp)"
trap 'rm -f "$secret_file"' EXIT
chmod 0600 "$secret_file"
jq -n --arg password "$POSTGRES_SUPERUSER_PASSWORD" \
'{POSTGRES_SUPERUSER_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/systems/auth-system/postgres/superuser @"$secret_file"
jq -n \
--arg password "$AUTH_DB_PASSWORD" \
'{AUTH_DB_PASSWORD: $password, APP_DATASOURCE_USERNAME: "project_auth", APP_DATASOURCE_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/systems/auth-system/postgres/auth-server @"$secret_file"
jq -n --arg password "$KEYCLOAK_DB_PASSWORD" \
'{KEYCLOAK_DB_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/systems/auth-system/postgres/keycloak @"$secret_file"
jq -n --arg password "$KEYCLOAK_ADMIN_PASSWORD" \
'{KC_BOOTSTRAP_ADMIN_PASSWORD: $password}' >"$secret_file"
vault kv put kv/dev/systems/auth-system/keycloak/bootstrap-admin @"$secret_file"
jq -n --arg secret "$KEYCLOAK_CLIENT_SECRET" \
'{KEYCLOAK_CLIENT_SECRET: $secret, APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET: $secret}' >"$secret_file"
vault kv put kv/dev/workloads/auth-server/keycloak-client @"$secret_file"
rm -f "$secret_file"
trap - EXIT
unset VAULT_TOKEN
```
## 7. Vault Agent Injector gate
Vault auth, workload policy/role와 secret metadata를 확인한 뒤 Git PR에서
`vault-agent-injector` element만 `autoSync: "true"`로 바꿉니다. Merge 후
injector Deployment와 webhook health를 확인합니다. 이 단계에서도
`auth-system`과 두 workload gate는 닫혀 있어야 합니다.
## 8. Auth system gate
Secret metadata와 workload policy를 확인한 뒤 Git PR에서 `auth-system`
inventory element만 `autoSync: "true"`로 바꿉니다. PR에는 현재 전체 Argo
diff를 첨부합니다. Child manifest를 직접 apply하거나 Argo UI에서 임의로
Sync하지 않습니다.
Merge 후 PostgreSQL과 Keycloak health를 확인합니다.
```bash
kubectl -n auth-system-dev rollout status statefulset/postgres --timeout=600s
kubectl -n auth-system-dev rollout status deployment/keycloak --timeout=600s
```
## 9. Vault database state
`TF_VAR_vault_token`을 database 전용 short-lived token으로 교체하고
PostgreSQL credential을 실행 시점에만 전달합니다.
```bash
export VAULT_DATABASE_TOKEN="$(
VAULT_TOKEN="$(
jq -r '.root_token' .local/vault/dev-k3s-init.json
)" \
vault token create \
-orphan \
-no-default-policy \
-renewable=false \
-explicit-max-ttl=2h \
-policy=vault-database-automation-dev \
-ttl=2h \
-format=json |
jq -r '.auth.client_token'
)"
export TF_VAR_vault_token="$VAULT_DATABASE_TOKEN"
export TF_VAR_postgres_admin_password="$POSTGRES_SUPERUSER_PASSWORD"
export TF_VAR_postgres_admin_password_version=1
make terraform-plan \
TF_ROOT=vault-database \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl
make terraform-apply \
TF_ROOT=vault-database \
BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-database.s3.hcl \
APPROVE_APPLY=dev-k3s/vault-database
```
Plan에는 `database/config/auth-system-postgres-dev` connection과
`auth-db-migration-dev` dynamic role만 있어야 합니다. Dynamic credential
발급과 revoke를 검증합니다.
## 10. Root token 폐기
Database state와 delegated login/recovery 절차를 검증한 뒤, application
gate를 열기 전에 initial root token을 폐기합니다.
Script는 두 replacement token을 반드시 요구하고 다음을 자동 검증합니다.
- `VAULT_WORKLOADS_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
./scripts/vault-init.sh revoke-root
VAULT_TOKEN="$VAULT_WORKLOADS_TOKEN" vault token revoke -self
VAULT_TOKEN="$VAULT_DATABASE_TOKEN" vault token revoke -self
unset TF_VAR_vault_token TF_VAR_postgres_admin_password
unset VAULT_WORKLOADS_TOKEN VAULT_DATABASE_TOKEN
unset POSTGRES_SUPERUSER_PASSWORD AUTH_DB_PASSWORD KEYCLOAK_DB_PASSWORD
unset KEYCLOAK_ADMIN_PASSWORD KEYCLOAK_CLIENT_SECRET
```
`revoke-root`는 local init JSON에서도 root token field를 제거합니다. 두
bootstrap orphan token도 검증 직후 self-revoke하며 routine credential로
재사용하지 않습니다.
## 11. Workload gates
Keycloak realm/client sync와 database migration credential이 준비된 것을
확인한 뒤 단계별 PR을 사용합니다.
1. `auth-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 auth-system-dev get pods
kubectl -n auth-dev get pods
kubectl -n api-dev get pods
```
Encrypted custody로 옮긴 init material의 local working copy는 조직의 dev
recovery 정책에 따라 제거합니다. 실패를 고치기 위해 live child manifest를
직접 수정하지 말고 Git PR을 사용합니다.
+46
View File
@@ -0,0 +1,46 @@
# Sealed Secrets backup and recovery
Existing SealedSecrets are decryptable only with a controller private key.
Back up every key after first install and after rotation.
## Backup
Use an encrypted operator workstation:
```bash
umask 077
kubectl -n kube-system get secret \
-l sealedsecrets.bitnami.com/sealed-secrets-key \
-o json > .local/sealed-secrets-keys.json
chmod 0600 .local/sealed-secrets-keys.json
```
Encrypt the file with the organization's recovery mechanism, store at least
two independently controlled copies, and delete the plaintext working copy.
Record cluster, date and checksum without recording private key data.
## Restore
Restore keys before application SealedSecrets sync:
```bash
kubectl -n kube-system apply -f .local/sealed-secrets-keys.json
kubectl -n kube-system rollout restart deployment/sealed-secrets-controller
kubectl -n kube-system rollout status deployment/sealed-secrets-controller
```
Verify both `auth-dev/ghcr-regcred` and `api-dev/ghcr-regcred` are created.
If no key backup exists, old ciphertext cannot be recovered. Create a new
controller key and reseal every Secret from the original credential source.
## Rotation
The chart requests periodic key renewal. Old keys must remain until all
ciphertext has been resealed and verified. GHCR credential rotation requires:
1. issue an organization-owned read-only package credential;
2. create namespace-scoped SealedSecrets with the current controller cert;
3. merge and verify image pulls;
4. revoke the previous credential;
5. update the encrypted key backup.
+244
View File
@@ -0,0 +1,244 @@
# Terraform state migration to three Vault states
새 클러스터에는 이 runbook이 필요하지 않습니다. Legacy `vault-core` 또는
더 오래된 `provider-foundation`, `workload-foundation`, `workload-config`,
`database-config` state가 실제로 존재할 때만 사용합니다.
> 2026-07-26 리팩터링에서는 이 절차를 실제 backend나 cluster에 실행하지
> 않았습니다. Maintenance window, 독립 backup과 승인 없이 시작하지
> 않습니다.
State 이동은 live object 삭제보다 위험할 수 있습니다. State split,
Terraform module refactor, Vault KV path/namespace cutover를 한 apply에
섞지 않습니다.
## 목표 ownership
| Legacy ownership | 목표 state |
|---|---|
| `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 절차 |
목표 backend key는 각각 달라야 합니다.
```text
dev-k3s/vault-foundation.tfstate
dev-k3s/vault-workloads.tfstate
dev-k3s/vault-database.tfstate
```
동일 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을 먼저
이동합니다.
## 6. Retired broad/unused access
다음 legacy object는 새 state의 desired ownership이 아닙니다.
- 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를
삭제하지 않습니다.
+44
View File
@@ -0,0 +1,44 @@
# Vault backup and recovery
`dev-k3s` Vault는 single-node integrated Raft입니다. upgrade, Terraform
state migration, storage 작업 전에 snapshot을 생성합니다.
## Snapshot
short-lived authorized token과 Vault API 연결을 준비합니다.
```bash
umask 077
vault operator raft snapshot save .local/vault/dev-k3s.snap
sha256sum .local/vault/dev-k3s.snap
chmod 0600 .local/vault/dev-k3s.snap
```
즉시 암호화해 init/unseal material과 다른 위치에 보관합니다. snapshot에는
secret payload가 포함됩니다.
## Recovery material
- Raft snapshot
- unseal key
- 현재 manifest revision
- Terraform remote state backup
- Sealed Secrets controller key
Initial root token은 recovery material이 아닙니다. bootstrap 완료 후
폐기해야 합니다.
## Restore exercise
분기마다 isolated disposable cluster에서 다음을 검증합니다.
1. 동일 Vault version과 storage 설정 배포
2. Vault init 후 snapshot restore
3. 복구 key로 unseal
4. Kubernetes auth 재연결 확인
5. KV read, JWT signing, dynamic database lease issue/revoke 확인
6. recovery time과 누락된 의존성 기록
현재 dev 구성은 production recovery 설계가 아닙니다. production은
independent HA Vault, TLS, auto-unseal과 더 엄격한 backup custody가
필요합니다.
+28
View File
@@ -0,0 +1,28 @@
# Examples
예제는 구조와 조립 방식을 설명하기 위한 설계 참고 자료이며 실제 reconcile
대상이 아닙니다.
- [`minimal`](minimal/README.md): `kubectl kustomize`로 렌더 가능한 단일
환경·단일 클러스터의 canonical 구조
- [`scaled`](scaled/README.md): 여러 계정·환경·리전·클러스터로 확장할 때의
경로와 state 분리 기준
## 활용 순서
1. `minimal`에서 `infrastructure/live`, catalog `base`,
`gitops/clusters`의 관계를 확인합니다.
2. 규모가 커질 가능성이 있으면 `scaled`에서 account/region/environment
segment와 state 분리 기준을 선택합니다.
3. 실제 구현은 예제가 아니라 다음 `_template`을 복사해 시작합니다.
| 영역 | 복사 원본 |
|---|---|
| Infrastructure | `../infrastructure/components/_template`, `../infrastructure/stacks/_template`, `../infrastructure/live/_template` |
| GitOps | `../gitops/clusters/_template`, `../gitops/platform/_template`, `../gitops/apps/_template`, `../gitops/policies/_template`, `../gitops/tenants/_template` |
4. 실제 `live`와 cluster root가 예제와 같은 소유권·조립 경계를 유지하는지
비교하고 `make check`로 검증합니다.
예제 디렉터리를 GitOps controller root로 연결하거나 예제의 이름, namespace,
값을 production 기본값으로 재사용하지 않습니다.
+20
View File
@@ -0,0 +1,20 @@
# Minimal Example
단일 환경·단일 클러스터가 같은 경계를 어떻게 사용하는지 보여 주는 예입니다.
```text
minimal/
├── infrastructure/
│ └── live/dev/cluster/README.md
└── gitops/
├── platform/core/base/
├── apps/hello-config/base/
└── clusters/dev/main/
```
실제 cloud 리소스를 만들지 않으며, GitOps 예제는 Namespace와 ConfigMap만
렌더합니다.
```bash
kubectl kustomize examples/minimal/gitops/clusters/dev/main
```
@@ -0,0 +1,11 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: hello-config
namespace: skeleton-demo
labels:
app.kubernetes.io/name: hello-config
app.kubernetes.io/part-of: skeleton-demo
app.kubernetes.io/managed-by: kustomize
data:
message: "replace this example with a real application definition"
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- config-map.yaml
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../../platform/core/base
- ../../../apps/hello-config/base
@@ -1,8 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: vault-prod
resources:
- ../../base
- namespace.yaml
@@ -0,0 +1,7 @@
apiVersion: v1
kind: Namespace
metadata:
name: skeleton-demo
labels:
app.kubernetes.io/part-of: skeleton-demo
app.kubernetes.io/managed-by: kustomize
@@ -0,0 +1,11 @@
# Example Live Root
실제 프로젝트에서는 이 위치가 독립 state를 갖는 실행 가능한 IaC root가 됩니다.
- Scope: local example
- Environment: dev
- Stack: cluster
- State: example에는 없음
이 예제에는 provider 또는 IaC 엔진을 선택하지 않았기 때문에 실행 코드를
포함하지 않습니다.
+48
View File
@@ -0,0 +1,48 @@
# Scaled Layout Example
계정·리전·환경·클러스터가 늘어나도 lifecycle과 entrypoint 계약은 바뀌지
않습니다.
```text
infrastructure/
├── components/
│ └── aws/
│ ├── network/
│ ├── identity/
│ └── eks/
├── stacks/
│ ├── regional-foundation/
│ └── kubernetes-cluster/
└── live/
└── aws/
├── platform-nonprod/
│ └── ap-northeast-2/
│ ├── dev/{network,cluster-a}/
│ └── staging/{network,cluster-a}/
└── platform-prod/
├── ap-northeast-2/prod/{network,cluster-a}/
└── ap-southeast-1/prod/{network,cluster-b}/
gitops/
├── platform/{core,networking,security,observability}/
├── policies/{baseline,production}/
├── tenants/{team-a,team-b}/
├── apps/{api,worker}/
└── clusters/
├── dev/ap-northeast-2/cluster-a/
├── staging/ap-northeast-2/cluster-a/
└── prod/
├── ap-northeast-2/cluster-a/
└── ap-southeast-1/cluster-b/
```
중괄호 표기는 설명을 줄이기 위한 것이며 실제 폴더명으로 사용하지 않습니다.
## 분리 기준
- `regional-foundation` network stack과 cluster는 파괴 영향이 달라 state를
분리합니다.
- production과 non-production은 account, credential과 state를 분리합니다.
- 공통 구현은 catalog에 한 번만 두고 cluster root는 선택과 patch만 가집니다.
- tenant/team별 권한이 다르면 CODEOWNERS와 repository 분리를 검토합니다.
- repository 분리는 폴더 수가 아니라 소유권과 권한 경계가 달라질 때 수행합니다.
+33
View File
@@ -0,0 +1,33 @@
# GitOps Desired State
Kubernetes API 안에서 지속적으로 reconcile할 desired state를 관리합니다.
GitOps controller를 사용하지 않는 초기 단계에도 `kubectl kustomize`로 같은
entrypoint를 렌더할 수 있습니다.
```text
platform ─┐
policies ─┼──▶ clusters/<...> ◀── GitOps controller root
tenants ─┤
apps ─┘
```
- `clusters`: 클러스터별 최종 조립점
- `platform`: cluster-wide addon과 controller
- `policies`: cluster-wide admission과 거버넌스 규칙
- `tenants`: 구체적인 namespace/RBAC/quota/NetworkPolicy
- `apps`: application 배포 정의
Catalog 디렉터리를 controller root로 직접 지정하지 않습니다. cluster entrypoint가
필요한 base/overlay를 선택하고 의존 순서를 명시합니다.
## 기본 규칙
- `base`는 환경을 모르며 재사용 가능한 기본값만 가집니다.
- `overlays`는 차이만 patch하고 전체 manifest를 복사하지 않습니다.
- CRD/controller가 필요한 리소스는 controller 이후에 reconcile합니다.
- resource namespace, ownership label과 버전을 명시합니다.
- raw `Secret` 또는 실제 비밀값을 커밋하지 않습니다.
- 원격 base/chart를 참조할 때 immutable version 또는 digest를 사용합니다.
Flux/Argo CD 고유 리소스와 sync ordering은 선택한 controller를 기록한 ADR에
문서화합니다.
+18
View File
@@ -0,0 +1,18 @@
# Application Deployment Catalog
애플리케이션의 source code가 아니라 Kubernetes 배포 정의를 둡니다. app 팀이
별도 source/deploy 저장소를 소유하면 이곳에는 immutable artifact를 참조하는
GitOps 리소스만 둘 수 있습니다.
```text
apps/
└── example-api/
├── base/
└── overlays/
├── dev/
└── prod/
```
base는 환경을 모르고, overlay에는 replica/resource/config처럼 필요한 차이만
둡니다. image는 mutable tag 대신 조직 정책에 따른 고정 tag 또는 digest를
사용합니다.
+22
View File
@@ -0,0 +1,22 @@
# __REPLACE_ME_APPLICATION_NAME__
## 소유자
Team: __REPLACE_ME_OWNER__
repository와 on-call 정보를 적습니다.
## 배포 계약
- Namespace:
- Image/artifact source:
- Ports/protocol:
- Dependency:
- SLO/alerts:
## 구성
- `base`: 공통 Kubernetes 배포 정의
- `overlays`: 환경별 replica, resource, config 차이
비밀은 ExternalSecret 같은 참조 또는 승인된 암호화 형식으로만 추가합니다.
+4
View File
@@ -0,0 +1,4 @@
# Base
환경을 모르는 application의 공통 manifest를 둡니다. namespace 자체의 소유권이
tenant catalog에 있다면 이곳에서 중복 생성하지 않습니다.
@@ -0,0 +1,4 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources: []
+6
View File
@@ -0,0 +1,6 @@
# Overlays
필요한 환경에만 overlay를 추가합니다. 각 overlay는 `../../base`를 참조하고
환경별 patch만 포함합니다.
비밀값, 임시 debug 설정과 수동 hotfix 결과를 overlay에 커밋하지 않습니다.
@@ -0,0 +1,8 @@
# Project Auth system
Project Auth bounded context가 소유하는 PostgreSQL, Keycloak과 client sync의
환경 중립 base입니다. `dev-k3s` namespace, host, Vault annotation과
NetworkPolicy는
`gitops/clusters/dev-k3s/overlays/systems/auth-system`에서 결합합니다.
Owner: Project Auth system
@@ -0,0 +1,19 @@
#!/bin/sh
set -eu
psql \
-v ON_ERROR_STOP=1 \
--set=auth_db_user="${AUTH_DB_USER}" \
--set=auth_db_password="${AUTH_DB_PASSWORD}" \
--set=auth_db_name="${AUTH_DB_NAME}" \
--set=keycloak_db_user="${KEYCLOAK_DB_USER}" \
--set=keycloak_db_password="${KEYCLOAK_DB_PASSWORD}" \
--set=keycloak_db_name="${KEYCLOAK_DB_NAME}" \
--username "${POSTGRES_USER}" \
--dbname "${POSTGRES_DB}" <<-'EOSQL'
CREATE USER :"auth_db_user" WITH PASSWORD :'auth_db_password';
CREATE DATABASE :"auth_db_name" OWNER :"auth_db_user";
CREATE USER :"keycloak_db_user" WITH PASSWORD :'keycloak_db_password';
CREATE DATABASE :"keycloak_db_name" OWNER :"keycloak_db_user";
EOSQL
@@ -3,32 +3,46 @@ kind: Job
metadata:
name: keycloak-client-sync
annotations:
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
argocd.argoproj.io/sync-wave: "4"
spec:
backoffLimit: 5
activeDeadlineSeconds: 600
backoffLimit: 1
template:
metadata:
labels:
app: keycloak-client-sync
spec:
serviceAccountName: keycloak-client-sync
restartPolicy: OnFailure
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
seccompProfile:
type: RuntimeDefault
containers:
- name: keycloak-client-sync
image: quay.io/keycloak/keycloak:26.5.5
image: quay.io/keycloak/keycloak:26.5.5@sha256:a7b0cb7a43a1235a61872883414d3f1d9a3ceac9df6e5907bd12202778a6265c
command:
- /bin/sh
- -c
- |
set -eu
until /opt/keycloak/bin/kcadm.sh config credentials \
--server http://keycloak.platform.svc.cluster.local \
--realm master \
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; do
ready=false
for _ in $(seq 1 60); do
if /opt/keycloak/bin/kcadm.sh config credentials \
--server http://keycloak \
--realm master \
--user "$KC_BOOTSTRAP_ADMIN_USERNAME" \
--password "$KC_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null 2>&1; then
ready=true
break
fi
sleep 5
done
test "$ready" = true
CLIENT_UUID=$(/opt/keycloak/bin/kcadm.sh get clients \
-r project-auth \
@@ -42,11 +56,24 @@ spec:
-s "baseUrl=$AUTH_SERVER_BASE_URL" \
-s 'redirectUris=["'"$AUTH_SERVER_BASE_URL"'/login/oauth2/code/keycloak-google","'"$AUTH_SERVER_BASE_URL"'/login/oauth2/code/keycloak-github"]' \
-s 'webOrigins=["'"$AUTH_SERVER_BASE_URL"'"]'
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
cpu: 250m
memory: 256Mi
env:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
@@ -56,7 +83,7 @@ spec:
- name: KEYCLOAK_CLIENT_ID
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_CLIENT_ID
- name: KEYCLOAK_CLIENT_SECRET
valueFrom:
@@ -66,5 +93,5 @@ spec:
- name: AUTH_SERVER_BASE_URL
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: AUTH_SERVER_BASE_URL
@@ -21,7 +21,7 @@ spec:
type: RuntimeDefault
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.5.5
image: quay.io/keycloak/keycloak:26.5.5@sha256:a7b0cb7a43a1235a61872883414d3f1d9a3ceac9df6e5907bd12202778a6265c
args:
- start-dev
- --import-realm
@@ -35,11 +35,11 @@ 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_DB_PASSWORD
valueFrom:
@@ -51,7 +51,7 @@ spec:
- name: KC_BOOTSTRAP_ADMIN_USERNAME
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME
- name: KC_BOOTSTRAP_ADMIN_PASSWORD
valueFrom:
@@ -10,9 +10,6 @@ resources:
- keycloak-service.yaml
- keycloak-deployment.yaml
- keycloak-client-sync-job.yaml
generatorOptions:
disableNameSuffixHash: true
configMapGenerator:
- name: postgres-init-script
files:
@@ -22,7 +22,7 @@ spec:
type: RuntimeDefault
containers:
- name: postgres
image: postgres:16-alpine
image: postgres:16-alpine@sha256:57c72fd2a128e416c7fcc499958864df5301e940bca0a56f58fddf30ffc07777
ports:
- containerPort: 5432
name: postgres
@@ -32,7 +32,7 @@ spec:
- name: POSTGRES_USER
valueFrom:
configMapKeyRef:
name: platform-config
name: auth-system-config
key: POSTGRES_SUPERUSER
- name: POSTGRES_PASSWORD
valueFrom:
@@ -42,17 +42,17 @@ spec:
- 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: AUTH_DB_PASSWORD
valueFrom:
@@ -62,12 +62,12 @@ spec:
- 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
- name: KEYCLOAK_DB_PASSWORD
valueFrom:
@@ -75,6 +75,11 @@ spec:
name: postgres-keycloak-credentials
key: KEYCLOAK_DB_PASSWORD
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
readOnlyRootFilesystem: false
volumeMounts:
- name: postgres-data
mountPath: /var/lib/postgresql/data
@@ -0,0 +1,8 @@
# API server
별도 source repository에서 빌드되는 API server의 환경 중립 Kubernetes
base입니다. Image digest, dev configuration, ingress와 pull credential
reference는 `gitops/clusters/dev-k3s/overlays/workloads/api-server`
소유합니다.
Owner: Project API workload
@@ -2,6 +2,8 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
annotations:
argocd.argoproj.io/sync-wave: "10"
spec:
replicas: 1
selector:

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