Compare commits
3
Commits
d507ac6ee9
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8626946b1 | ||
|
|
a6f6c663e0 | ||
|
|
293ee6fc97 |
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -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` 진입점은 제공하지 않습니다.
|
||||
@@ -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)"
|
||||
+46
@@ -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
|
||||
@@ -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
|
||||
@@ -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 +0,0 @@
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
# Bootstrap
|
||||
|
||||
선언형 인프라와 GitOps가 스스로 동작하기 전에 한 번 또는 매우 드물게 수행하는
|
||||
최소 초기화만 둡니다.
|
||||
|
||||
- `foundation`: remote state, locking, 최초 identity 같은 선행 조건
|
||||
- `gitops`: 선택한 controller 설치와 cluster root 연결
|
||||
|
||||
일반 네트워크, Kubernetes cluster, addon과 application을 이곳에 두지 않습니다.
|
||||
부트스트랩 절차는 반복 실행 가능하고 감사 가능해야 하며, 장기 수동 운영 경로가
|
||||
되어서는 안 됩니다.
|
||||
@@ -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와 복구 절차만 기록합니다.
|
||||
@@ -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
|
||||
+2
-4
@@ -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
|
||||
@@ -0,0 +1,2 @@
|
||||
ARGOCD_VERSION=v3.4.2
|
||||
ARGOCD_INSTALL_SHA256=69114b8c9eb48a1d08598e6f654a0869b10ae902456ea4b70796cb563760f5ec
|
||||
@@ -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는 이 리팩터링 리뷰 범위에 포함되지 않습니다.
|
||||
@@ -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로
|
||||
교체합니다.
|
||||
@@ -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/)
|
||||
@@ -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에 먼저
|
||||
기록합니다.
|
||||
@@ -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에서 허용되지 않습니다.
|
||||
@@ -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
@@ -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에서 제거합니다.
|
||||
@@ -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`를 사용하지 않습니다.
|
||||
@@ -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은 유지하지 않습니다.
|
||||
@@ -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를 변경하지 않습니다.
|
||||
@@ -0,0 +1,26 @@
|
||||
# Architecture Decision Records
|
||||
|
||||
프로젝트의 장기 구조에 영향을 주는 선택은 ADR로 남깁니다.
|
||||
|
||||
파일명은 `NNNN-kebab-case-title.md`를 사용하고 다음 형식을 따릅니다.
|
||||
|
||||
```markdown
|
||||
# NNNN. 제목
|
||||
|
||||
- 상태: 제안 | 승인 | 폐기 | 대체
|
||||
- 날짜: YYYY-MM-DD
|
||||
- 결정자: 팀 또는 역할
|
||||
|
||||
## 배경
|
||||
|
||||
## 결정
|
||||
|
||||
## 결과
|
||||
|
||||
## 대안
|
||||
```
|
||||
|
||||
기존 결정을 바꿀 때 문서를 지우지 말고 새 ADR에서 이전 ADR을 대체했다고
|
||||
표시합니다.
|
||||
|
||||
현재 프로젝트 결정은 이 디렉터리의 `0001`부터 순서대로 관리합니다.
|
||||
@@ -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에 재사용하지 않습니다.
|
||||
@@ -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을 연습 과제로 실행하지 않습니다.
|
||||
@@ -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 장애 대응
|
||||
|
||||
각 문서는 `목적`, `사전 조건`, `영향`, `절차`, `검증`, `롤백`,
|
||||
`에스컬레이션` 섹션을 포함해야 합니다.
|
||||
@@ -0,0 +1,15 @@
|
||||
# Runbook 제목
|
||||
|
||||
## 목적
|
||||
|
||||
## 사전 조건
|
||||
|
||||
## 영향
|
||||
|
||||
## 절차
|
||||
|
||||
## 검증
|
||||
|
||||
## 롤백
|
||||
|
||||
## 에스컬레이션
|
||||
@@ -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과 별도 승인 없이는
|
||||
사용하지 않습니다.
|
||||
@@ -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을 사용합니다.
|
||||
@@ -0,0 +1,46 @@
|
||||
# Sealed Secrets backup and recovery
|
||||
|
||||
Existing SealedSecrets are decryptable only with a controller private key.
|
||||
Back up every key after first install and after rotation.
|
||||
|
||||
## Backup
|
||||
|
||||
Use an encrypted operator workstation:
|
||||
|
||||
```bash
|
||||
umask 077
|
||||
kubectl -n kube-system get secret \
|
||||
-l sealedsecrets.bitnami.com/sealed-secrets-key \
|
||||
-o json > .local/sealed-secrets-keys.json
|
||||
chmod 0600 .local/sealed-secrets-keys.json
|
||||
```
|
||||
|
||||
Encrypt the file with the organization's recovery mechanism, store at least
|
||||
two independently controlled copies, and delete the plaintext working copy.
|
||||
Record cluster, date and checksum without recording private key data.
|
||||
|
||||
## Restore
|
||||
|
||||
Restore keys before application SealedSecrets sync:
|
||||
|
||||
```bash
|
||||
kubectl -n kube-system apply -f .local/sealed-secrets-keys.json
|
||||
kubectl -n kube-system rollout restart deployment/sealed-secrets-controller
|
||||
kubectl -n kube-system rollout status deployment/sealed-secrets-controller
|
||||
```
|
||||
|
||||
Verify both `auth-dev/ghcr-regcred` and `api-dev/ghcr-regcred` are created.
|
||||
|
||||
If no key backup exists, old ciphertext cannot be recovered. Create a new
|
||||
controller key and reseal every Secret from the original credential source.
|
||||
|
||||
## Rotation
|
||||
|
||||
The chart requests periodic key renewal. Old keys must remain until all
|
||||
ciphertext has been resealed and verified. GHCR credential rotation requires:
|
||||
|
||||
1. issue an organization-owned read-only package credential;
|
||||
2. create namespace-scoped SealedSecrets with the current controller cert;
|
||||
3. merge and verify image pulls;
|
||||
4. revoke the previous credential;
|
||||
5. update the encrypted key backup.
|
||||
@@ -0,0 +1,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를
|
||||
삭제하지 않습니다.
|
||||
@@ -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가
|
||||
필요합니다.
|
||||
@@ -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 기본값으로 재사용하지 않습니다.
|
||||
@@ -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
|
||||
-3
@@ -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 엔진을 선택하지 않았기 때문에 실행 코드를
|
||||
포함하지 않습니다.
|
||||
@@ -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 분리는 폴더 수가 아니라 소유권과 권한 경계가 달라질 때 수행합니다.
|
||||
@@ -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에
|
||||
문서화합니다.
|
||||
@@ -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를
|
||||
사용합니다.
|
||||
@@ -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 같은 참조 또는 승인된 암호화 형식으로만 추가합니다.
|
||||
@@ -0,0 +1,4 @@
|
||||
# Base
|
||||
|
||||
환경을 모르는 application의 공통 manifest를 둡니다. namespace 자체의 소유권이
|
||||
tenant catalog에 있다면 이곳에서 중복 생성하지 않습니다.
|
||||
@@ -0,0 +1,4 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
|
||||
resources: []
|
||||
@@ -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
|
||||
+38
-11
@@ -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
|
||||
+4
-4
@@ -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:
|
||||
-3
@@ -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:
|
||||
+12
-7
@@ -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
@@ -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
Reference in New Issue
Block a user