diff --git a/.gitea/workflows/promote-image.yaml b/.gitea/workflows/promote-image.yaml
new file mode 100644
index 0000000..be7bbf6
--- /dev/null
+++ b/.gitea/workflows/promote-image.yaml
@@ -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="clusters/dev-k3s/manifests/${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 "clusters/dev-k3s/manifests/${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"
diff --git a/.gitea/workflows/validate.yaml b/.gitea/workflows/validate.yaml
new file mode 100644
index 0000000..89deb85
--- /dev/null
+++ b/.gitea/workflows/validate.yaml
@@ -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: ./hack/validate.sh
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..733971d
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,22 @@
+# Terraform working directories and local state
+**/.terraform/*
+**/.terraform-state/*
+*.tfstate
+*.tfstate.*
+*.tfplan
+*.tfvars
+!*.tfvars.example
+crash.log
+crash.*.log
+
+# Bootstrap material and local operator files
+.local/*
+!.local/.gitkeep
+*init*.json
+
+# Tool and editor output
+.DS_Store
+.idea/
+.vscode/
+*.swp
+*.tmp
diff --git a/AGENTS.md b/AGENTS.md
new file mode 100644
index 0000000..f8c15b5
--- /dev/null
+++ b/AGENTS.md
@@ -0,0 +1,14 @@
+# 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.
+- 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.
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..1b10e31
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,31 @@
+# Changelog
+
+## 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.
diff --git a/INTERN_GUIDE.md b/INTERN_GUIDE.md
index a6b1abd..aff377b 100644
--- a/INTERN_GUIDE.md
+++ b/INTERN_GUIDE.md
@@ -1,3044 +1,74 @@
-# 인턴 가이드: 개념, 아키텍처, 코드로 이해하는 Project Auth GitOps
+# Intern Guide
-## 1. 이 문서의 목표
+## 먼저 이해할 것
-이 문서의 목적은 **개념**, **현재 프로젝트 아키텍처**, **실제 코드**를 하나의 흐름으로 이해하게 만드는 것입니다.
+이 저장소에는 서로 다른 세 개의 reconciliation 경계가 있습니다.
-즉, 이 문서는 단순한 코드 해설서가 아니라 아래 3층 구조를 목표로 합니다.
+1. Gitea가 승인된 desired-state revision을 저장합니다.
+2. Argo CD가 그 revision의 Kubernetes 리소스를 지속적으로 맞춥니다.
+3. Terraform이 승인된 실행 환경에서 Vault API 객체를 관리합니다.
-1. 개념 레이어
- 이 프로젝트가 왜 Kubernetes, GitOps, Vault, Terraform, Keycloak 구조를 쓰는지 이해합니다.
-2. 아키텍처 레이어
- 현재 dev 환경에서 각 구성요소가 어떤 책임을 가지며 어떻게 연결되는지 이해합니다.
-3. 코드 레이어
- 실제로 `apps/`, `infra/`, `scripts/`, `terraform/`, `runbooks/` 안의 파일을 읽고 수정할 수 있게 합니다.
+Argo CD가 Terraform을 실행하지 않으며 CI가 routine deployment를 위해
+`kubectl apply`를 호출하지 않습니다. secret 값도 Git이나 Terraform을
+통과하지 않습니다.
-이 문서를 다 읽고 나면 최소한 아래 질문에 스스로 답할 수 있어야 합니다.
+## 안전한 변경 흐름
-- 이 저장소는 왜 존재하는가?
-- `apps/`만 보면 왜 절반밖에 이해하지 못하는가?
-- `auth-server`는 왜 `Deployment`와 `Job`을 둘 다 가지는가?
-- 비밀값은 어디에서 생성되고, 어디를 지나서, 어떤 방식으로 Pod 안으로 들어가는가?
-- 왜 `vault-transit`과 `vault`를 둘 다 두었는가?
-- 어떤 값은 `ConfigMap`에 두고, 어떤 값은 Vault에 두는가?
-- 무엇을 `base`에 두고 무엇을 `overlay`에 두는가?
-- 어떤 변경이 다른 파일들까지 연쇄적으로 수정하게 만드는가?
+1. `refactor/...`, `feat/...`, `fix/...` 브랜치에서 변경합니다.
+2. `make validate`를 실행합니다.
+3. rendered manifest 또는 Terraform plan을 검토합니다.
+4. 내부 Gitea에 PR을 생성합니다.
+5. 승인 후 `main`에 merge합니다.
+6. Kubernetes 변경은 Argo CD가 자동 반영합니다.
+7. Terraform 변경은 별도 승인 후 실행합니다.
-이 문서는 **현재 dev 환경 기준**으로 설명합니다.
+금지 사항:
-## 2. 가장 먼저 알아야 하는 사실
+- `.terraform`, state, plan, tfvars, Vault init JSON, token commit
+- 동일 Vault path/resource를 두 state에서 관리
+- image promotion 자동화의 `main` 직접 push
+- routine CI의 직접 `kubectl apply`
+- production skeleton이나 이름뿐인 production Application 추가
+- hook을 사용하는 Application에 `ApplyOutOfSyncOnly=true` 적용
-이 저장소는 애플리케이션 소스 저장소가 아니라 **GitOps 저장소**입니다.
+## 자주 쓰는 명령
-- `Project-Auth-Server`, `Project-Api-Server` 같은 앱 저장소는 코드와 CI를 담당합니다.
-- 이 저장소는 Kubernetes에 반영할 선언과 CD를 담당합니다.
-- 그래서 `apps/` 안의 YAML만 읽으면 "앱이 어떻게 배포되는지"는 보이지만, "누가 이걸 적용하는지", "비밀값은 누가 준비하는지", "왜 이런 구조인지"는 보이지 않습니다.
-
-즉, 이 저장소를 제대로 이해하려면 아래를 **한 묶음**으로 봐야 합니다.
-
-- `apps/`: 앱 매니페스트
-- `infra/`: 공용 인프라 매니페스트
-- `argocd/`: Argo CD가 어떤 폴더를 감시할지 정의
-- `scripts/`: 실제 운영 절차를 자동화하는 Bash
-- `terraform/`: Vault 내부 상태를 선언적으로 맞추는 코드
-- `runbooks/`: 사람이 최초 bootstrap할 때 따르는 문서와 Vault 정책
-
-## 3. 파일 확장자부터 정확히 알고 가기
-
-| 확장자 | 뜻 | 이 저장소에서 하는 일 |
-| ------- | -------------------------------- | -------------------------------------------------------------------- |
-| `.yaml` | 선언형 데이터 포맷 | Kubernetes 리소스, Argo CD Application, GitHub Actions 워크플로 정의 |
-| `.sh` | Bash 스크립트 | 운영 절차를 순서대로 자동 실행 |
-| `.hcl` | HashiCorp Configuration Language | Vault 정책, Vault 서버 설정 |
-| `.tf` | Terraform 설정 파일 | Vault 내부 리소스를 선언적으로 생성/동기화 |
-
-중요한 점은 `*.tf`도 내부적으로 HCL 문법을 사용하지만, **의미가 다르다**는 것입니다.
-
-- `runbooks/vault/dev/policies/*.hcl`: "누가 어떤 경로를 읽거나 쓸 수 있는가"를 적는 **Vault 정책**
-- `infra/vault/base/files/vault/vault.hcl`: Vault 서버 자체가 어떻게 동작할지 적는 **Vault 서버 설정**
-- `terraform/**/*.tf`: Terraform이 어떤 Vault 리소스를 만들어야 하는지 적는 **IaC 코드**
-
-## 4. 용어 사전
-
-### 4-1. Kubernetes 용어
-
-| 용어 | 뜻 | 이 저장소에서 왜 중요한가 |
-| -------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
-| Manifest | Kubernetes에 적용할 선언 파일 | `apps/`, `infra/`, `argocd/` 대부분이 manifest다 |
-| Namespace | 리소스를 논리적으로 나누는 공간 | `auth-dev`, `api-dev`, `platform`, `vault`, `vault-transit`가 서로 다른 책임을 가진다 |
-| Pod | 실제 컨테이너가 뜨는 가장 작은 실행 단위 | Deployment나 Job이 결국 Pod를 만든다 |
-| Deployment | stateless 앱을 원하는 개수만큼 유지하는 리소스 | `auth-server`, `api-server`, `keycloak`, `vault`가 여기에 해당한다 |
-| StatefulSet | 이름, 저장소, 순서가 중요한 워크로드용 리소스 | `postgres`는 데이터가 있으므로 `Deployment`가 아니라 `StatefulSet`을 쓴다 |
-| Job | 한 번 실행하고 끝나는 작업 | DB migration, Keycloak client sync에 사용된다 |
-| Service | Pod 앞에 놓는 고정된 네트워크 진입점 | Pod IP가 바뀌어도 `auth-server`, `api-server`, `postgres`에 접속할 수 있다 |
-| Ingress | 클러스터 바깥 또는 north-south HTTP 진입 규칙 | Traefik을 통해 public host를 연결한다 |
-| NetworkPolicy | Pod 간 통신 허용/차단 규칙 | 기본 차단 후 필요한 통신만 허용하는 구조를 만든다 |
-| ConfigMap | 민감하지 않은 설정값 저장소 | 포트, 호스트, issuer URI 같은 값을 둔다 |
-| Secret | 민감한 값 저장소 | 이 저장소는 runtime secret을 가급적 Vault로 옮기고 image pull secret만 예외로 남긴다 |
-| SealedSecret | Git에 올려도 되는 암호화된 Secret 형태 | `ghcr-regcred`처럼 예외적으로 Git에 남겨야 하는 secret에 쓴다 |
-| ServiceAccount | Pod가 Kubernetes API 세계에서 갖는 신분 | Vault Kubernetes auth가 이 신분을 이용해 Pod를 검증한다 |
-| ExternalName Service | 다른 DNS 이름으로 트래픽을 넘기는 Service | `auth-public`, `api-public`처럼 Traefik 이름을 우회해 내부에서도 같은 public host를 쓰게 한다 |
-| Probe | 컨테이너 준비 상태/생존 상태 확인 | 준비 전 트래픽 차단, 비정상 재시작 판단에 쓰인다 |
-
-### 4-2. GitOps / Kustomize / Argo CD 용어
-
-| 용어 | 뜻 | 이 저장소에서 왜 중요한가 |
-| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
-| GitOps | Git에 있는 선언을 실제 클러스터 상태의 기준으로 삼는 방식 | 사람이 직접 `kubectl edit` 하지 않고 Git을 수정한다 |
-| Source of truth | 최종 기준이 되는 원본 | 앱 배포 선언의 원본은 이 저장소이고, 일부 seed secret의 원본은 provider Vault다 |
-| Kustomize | YAML을 base + overlay 구조로 합성하는 도구 | 공통 뼈대와 환경별 차이를 분리한다 |
-| Base | 환경과 무관한 공통 정의 | 공통 Deployment, Service, ServiceAccount 등이 들어간다 |
-| Overlay | 특정 환경에만 적용되는 차이 | dev용 namespace, host, image tag, Vault patch가 들어간다 |
-| Patch | 기존 리소스 일부만 덮어쓰는 변경 조각 | `deployment.vault-patch.yaml`이 대표적이다 |
-| Argo CD Application | "이 경로를 이 namespace로 동기화하라"는 선언 | `argocd/applications/dev/**`가 담당한다 |
-| Sync wave | Argo CD 적용 순서를 정하는 숫자 | Vault, platform, apps 순서를 안정적으로 맞춘다 |
-| Hook / PreSync | 일반 리소스 적용 전/후에 특별하게 실행되는 리소스 | DB migration Job은 앱 배포보다 먼저 돈다 |
-
-### 4-3. Vault / Terraform 용어
-
-| 용어 | 뜻 | 이 저장소에서 왜 중요한가 |
-| --------------- | --------------------------------------------------------- | --------------------------------------------------------------------------- |
-| Vault | 비밀값과 암호 기능을 제공하는 시스템 | 이 프로젝트의 runtime secret 관리 중심이다 |
-| Provider Vault | 다른 Vault를 돕는 상위 Vault | 여기서는 `vault-transit`이 workload Vault를 돕는다 |
-| Workload Vault | 앱이 직접 사용하는 Vault | `vault`가 여기에 해당한다 |
-| Transit engine | 데이터를 직접 저장하지 않고 암호 연산만 해주는 Vault 엔진 | JWT 서명, auto-unseal에 사용된다 |
-| Auto-unseal | Vault가 재기동 시 자동으로 봉인을 풀 수 있게 하는 방식 | workload Vault는 provider Vault의 transit key로 자동 unseal된다 |
-| Mount | Vault 안의 기능이 걸린 경로 | `kv`, `database`, `transit`, `auth/kubernetes`, `auth/approle`이 대표적이다 |
-| Policy | Vault 경로별 권한 규칙 | `auth-server-dev.hcl` 같은 파일이 여기에 해당한다 |
-| Kubernetes auth | Pod의 ServiceAccount JWT로 Vault에 로그인하는 방식 | 앱/플랫폼 Pod가 이 방식으로 secret을 받는다 |
-| AppRole | 기계 대 기계 로그인 방식 | CI와 bootstrap 자동화가 사용한다 |
-| TTL | 토큰이나 동적 계정의 수명 | migration용 DB 계정 같은 것을 오래 살지 않게 만든다 |
-| Terraform state | Terraform이 "내가 무엇을 만들었는지" 기억하는 파일 | bootstrap 루트와 reconcile 루트가 분리되어 있다 |
-| Idempotent | 여러 번 실행해도 결과가 같게 유지되는 성질 | reconcile 스크립트와 Terraform 설계의 핵심이다 |
-
-## 5. 이 저장소를 이해하기 위한 핵심 개념
-
-이 섹션은 "왜 이런 구조가 필요한가"를 설명합니다.
-이 섹션을 먼저 이해해야 뒤에서 나오는 YAML, Bash, HCL, TF가 단순 문법이 아니라 **설계의 결과물**로 보입니다.
-
-### 5-1. 인증, 인가, OAuth2, OIDC, JWT, Keycloak
-
-이 프로젝트를 이해하려면 먼저 아래 개념을 구분해야 합니다.
-
-| 용어 | 뜻 | 이 프로젝트에서의 의미 |
-| -------------------- | ---------------------------------------- | -------------------------------------------------------- |
-| 인증(Authentication) | "너 누구냐?"를 확인하는 것 | 로그인 성공 여부, 토큰 발급 대상 식별 |
-| 인가(Authorization) | "너 여기 접근해도 되냐?"를 확인하는 것 | API 접근 권한, 역할(Role) 확인 |
-| OAuth2 | 권한 위임 프레임워크 | 소셜 로그인, 외부 로그인 연동의 큰 틀 |
-| OIDC(OpenID Connect) | OAuth2 위에 사용자 신원 확인을 얹은 표준 | 로그인 결과를 표준화된 토큰 형태로 다룸 |
-| JWT | 서명된 토큰 포맷 | auth-server가 발급하고 api-server가 검증 |
-| Issuer | 토큰을 발급한 주체 | `auth-server`, `Keycloak`, `Vault transit` 설정과 연결됨 |
-| Client | 인증 서버를 이용하는 애플리케이션 | Keycloak 안의 `project-auth-server` client |
-| Redirect URI | 로그인 후 다시 돌아올 주소 | Keycloak client sync Job이 코드로 맞춘다 |
-
-이 프로젝트에서는 인증 관련 역할이 셋으로 나뉩니다.
-
-- `Keycloak`
- 외부 로그인 제공자와 연결되고, 로그인 브로커 역할을 한다
-- `auth-server`
- 우리 서비스 관점의 인증 서버 역할을 하며 JWT를 발급한다
-- `api-server`
- auth-server가 발급한 JWT를 검증하는 리소스 서버 역할을 한다
-
-이 셋이 실제로 어떻게 맞물려 동작하는지 하나의 흐름으로 보면 이렇습니다.
-
-```mermaid
-sequenceDiagram
- participant User as 사용자 브라우저
- participant KC as Keycloak
(로그인 브로커)
- participant Google as 구글/GitHub 등
- participant Auth as auth-server
(JWT 발급자)
- participant API as api-server
(리소스 서버)
-
- User->>Auth: "소셜 로그인할래요"
- Auth->>KC: Keycloak 로그인 페이지로 리다이렉트
- KC->>Google: 소셜 제공자와 OAuth2 통신
- Google-->>KC: 사용자 정보 반환
- KC-->>Auth: OIDC 표준 토큰으로 변환해서 콜백
- Auth->>Auth: 사용자 DB 조회/가입 처리 + 우리 JWT 발급
- Auth-->>User: 우리 서비스 JWT 토큰 반환
- User->>API: JWT를 헤더에 담아 API 호출
- API->>API: JWT 서명 검증 (Vault Transit 공개키로)
- API-->>User: API 응답
-```
-
-이 그림에서 핵심은 **각자가 맡은 범위가 다르다**는 것입니다. Keycloak은 외부 제공자와의 복잡한 통신만 처리하고, auth-server는 우리 서비스의 JWT만 발급하고, api-server는 그 JWT를 검증만 합니다. 하나의 서비스가 모든 것을 하지 않기 때문에, 각 부분을 독립적으로 교체하거나 수정할 수 있습니다.
-
-왜 이렇게 나누는가?
-
-- 소셜 로그인 제공자별 차이를 Keycloak이 흡수하게 하기 위해
-- 우리 서비스의 토큰 정책과 외부 로그인 흐름을 분리하기 위해
-- API 서버가 로그인 로직과 토큰 발급 책임까지 모두 떠안지 않게 하기 위해
-
-> 💡 이 흐름의 각 단계가 **구체적으로 어떤 메커니즘**으로 동작하는지는 24장에서 심층적으로 다룹니다.
-
-이 개념을 이해해야 아래 파일들이 왜 존재하는지 자연스럽게 연결됩니다.
-
-- `infra/platform/base/keycloak-deployment.yaml`
-- `infra/platform/base/keycloak-client-sync-job.yaml`
-- `apps/auth-server/overlays/dev/configmap.yaml`
-- `apps/api-server/overlays/dev/configmap.yaml`
-
-### 5-2. 리눅스, 컨테이너, 프로세스, 파일
-
-이 저장소의 YAML을 읽을 때 사실상 리눅스 프로세스 개념을 알아야 합니다.
-특히 Vault patch를 읽을 때 이 이해가 없으면 `command`, `args`, `. /vault/secrets/runtime-env`, `exec java -jar ...` 같은 부분이 전부 주문처럼 보입니다.
-
-꼭 이해해야 하는 개념은 아래와 같습니다.
-
-| 개념 | 뜻 | 이 프로젝트에서 왜 중요한가 |
-| ------------------- | --------------------------------------------- | ----------------------------------------------------------------------- |
-| Process | 실행 중인 프로그램 | 컨테이너 안에서 결국 Java, Postgres, Keycloak도 모두 프로세스다 |
-| PID 1 | 컨테이너 안의 첫 번째 프로세스 | 신호 처리와 종료 동작에 영향이 크다 |
-| `command` / `args` | 컨테이너가 실제로 어떤 명령으로 시작할지 정의 | Vault secret을 읽고 나서 원래 앱을 띄우기 위해 자주 재정의한다 |
-| `source` (`. file`) | 파일 안의 셸 명령을 현재 셸에 적용 | Vault Agent가 만든 `export ...` 파일을 환경변수로 불러온다 |
-| `exec` | 현재 셸 프로세스를 실제 앱 프로세스로 교체 | PID 1을 셸이 아니라 Java/Postgres/Keycloak로 만들기 위해 중요하다 |
-| Volume mount | 파일이나 디렉터리를 컨테이너에 붙이는 것 | Vault secret file, init script, config file이 모두 이 방식으로 들어온다 |
-
-예를 들어 `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`의 핵심은 이 순서입니다.
-
-1. Vault Agent가 `/vault/secrets/runtime-env` 파일 생성
-2. `/bin/sh -ec` 셸 시작
-3. `. /vault/secrets/runtime-env`로 환경변수 로드
-4. `exec java -jar /app/application.jar`로 실제 앱 프로세스 시작
-
-이것을 프로세스 관점에서 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart TD
- subgraph Container["auth-server 컨테이너 내부"]
- direction TB
- A["/bin/sh -ec 시작
PID 1 = 셸 프로세스"] --> B[". /vault/secrets/runtime-env
export 명령들이 현재 셸에 적용
→ 환경변수가 셸 메모리에 올라감"]
- B --> C["exec java -jar /app/application.jar
셸 프로세스가 Java 프로세스로 교체
→ PID 1 = Java (셸은 사라짐)"]
- end
-
- subgraph 만약_exec_없이["만약 exec를 안 쓰면?"]
- direction TB
- D["PID 1 = 셸 (계속 살아있음)"] --> E["PID 2 = Java (자식 프로세스)"]
- E --> F["K8s가 SIGTERM → 셸이 받음
셸은 자식에게 전달 안 할 수 있음
→ Java가 graceful shutdown 못 함"]
- end
-```
-
-`exec`가 왜 중요한지 이 그림이 보여줍니다. K8s가 Pod를 종료할 때 **PID 1에게** SIGTERM 신호를 보냅니다. `exec` 없이 셸이 PID 1이면, 셸은 이 신호를 Java에게 전달하지 않을 수 있습니다. 결과적으로 Java가 연결을 정리하지 못한 채 강제 종료(SIGKILL)됩니다. `exec`를 쓰면 Java가 PID 1이 되어 직접 SIGTERM을 받고, 연결을 정리한 뒤 깔끔하게 종료합니다.
-
-즉, 여기서 중요한 것은 "Vault가 비밀값을 준다"는 사실만이 아닙니다.
-**비밀값을 파일로 렌더링하고, 셸이 그 파일을 읽고, 마지막에 앱 프로세스로 넘어간다**는 실행 모델 전체를 이해해야 합니다.
-
-이 개념이 없으면 아래 같은 질문에 답하기 어렵습니다.
-
-- 왜 secret을 환경변수 자체로 바로 안 넣고 파일로 렌더링하나?
-- 왜 `exec`를 쓰나?
-- 왜 base Deployment에서는 `command`가 없는데 overlay patch에서는 생기나?
-
-> 💡 PID 1와 시그널 처리, 컨테이너 내부 프로세스 모델에 대한 더 깊은 이해는 23장에서 다룹니다.
-
-### 5-3. Kubernetes는 "컨테이너 실행기"가 아니라 "원하는 상태를 유지하는 시스템"이다
-
-초보자는 Kubernetes를 "도커를 원격으로 띄우는 도구"처럼 이해하기 쉽습니다. 하지만 더 정확히는 **원하는 상태(desired state)를 유지하는 시스템**입니다.
-
-이 프로젝트에서 꼭 알아야 하는 핵심 리소스는 아래입니다.
-
-| 리소스 | 무엇을 위한 것인가 | 이 프로젝트의 예시 |
-| -------------- | ------------------------------------------- | --------------------------------------------------- |
-| Deployment | 계속 살아 있어야 하는 stateless 앱 | `auth-server`, `api-server`, `keycloak`, `vault` |
-| StatefulSet | 저장소와 정체성이 중요한 워크로드 | `postgres` |
-| Job | 한 번 실행하고 끝나야 하는 작업 | `auth-db-migration`, `keycloak-client-sync` |
-| Service | Pod 앞의 고정 네트워크 이름 | `auth-server`, `api-server`, `postgres`, `keycloak` |
-| Ingress | HTTP 요청의 진입 규칙 | public host와 Traefik 연결 |
-| NetworkPolicy | Pod 간 허용할 통신만 남기는 네트워크 방화벽 | 각 namespace의 default deny 구조 |
-| ServiceAccount | Pod의 신분 | Vault Kubernetes auth에서 핵심 |
-| ConfigMap | 비민감 설정 | 포트, URL, issuer, host |
-| Secret / Vault | 민감 설정 | DB 비밀번호, client secret, token |
-
-이 리소스들을 왜 구분해서 써야 할까요?
-
-- 앱은 계속 살아야 하므로 `Deployment`
-- DB는 디스크와 이름이 안정적이어야 하므로 `StatefulSet`
-- migration은 한 번만 돌고 끝나야 하므로 `Job`
-
-이 판단을 잘못하면 부작용이 큽니다.
-
-- DB를 `Deployment`로 만들면 저장소와 이름 안정성이 약해진다
-- migration을 `Deployment`로 만들면 계속 재시작될 수 있다
-- 앱을 `Job`로 만들면 정상 서비스가 유지되지 않는다
-
-K8s가 이 리소스들의 상태를 어떻게 유지하는지 핵심 루프를 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart LR
- A["개발자가 선언
replicas: 3"] --> B["API Server에
Desired State 저장"]
- B --> C{"Controller Manager
현재 vs 원하는 상태 비교"}
- C -->|"Pod 2개 살아있음
1개 부족"| D["Pod 1개 추가 생성"]
- C -->|"Pod 4개 살아있음
1개 초과"| E["Pod 1개 삭제"]
- C -->|"Pod 3개 살아있음
일치 ✅"| F["아무것도 안 함"]
- D --> C
- E --> C
- F -->|"계속 감시
(Reconciliation Loop)"| C
-```
-
-이 루프가 **끊임없이** 도는 것이 K8s의 핵심입니다. 사용자가 "3개 돌려라"라고 선언하면, K8s는 현재 상태를 계속 확인하면서 차이를 조정합니다. Pod가 죽어도 자동으로 새로 만듭니다. 이것이 "컨테이너 실행기"가 아니라 "상태 유지 시스템"인 이유입니다.
-
-> 💡 이 Reconciliation Loop, Watch 메커니즘, Control Plane 각 컴포넌트의 역할은 25장에서 프로세스 수준으로 상세히 다룹니다.
-
-### 5-4. GitOps, Kustomize, Argo CD
-
-이 프로젝트는 "좋은 YAML을 써놨다"에서 끝나지 않습니다.
-이 YAML을 **누가**, **어떤 기준으로**, **반복적으로** 적용하느냐가 중요합니다.
-
-#### GitOps
-
-GitOps는 "실제 클러스터 상태의 기준을 Git에 둔다"는 운영 방식입니다.
-
-장점:
-
-- 누가 무엇을 바꿨는지 Git 기록으로 남는다
-- 수동 클릭보다 재현 가능하다
-- 문제가 생기면 선언 기준으로 되돌리기 쉽다
-
-주의할 점:
-
-- 클러스터에서 직접 수정하면 Git과 드리프트가 생긴다
-- Git에 민감값을 넣으면 GitOps의 편의가 보안 리스크로 바뀐다
-
-#### Kustomize
-
-Kustomize는 공통(base)과 환경별 차이(overlay)를 분리합니다.
-
-- `base`: 환경과 무관한 공통 뼈대
-- `overlay`: dev/prod별 차이
-
-이 프로젝트에서 이 구조가 중요한 이유:
-
-- `auth-server`의 기본 보안 설정, 포트, 프로브는 공통이지만
-- Vault 경로, ingress host, image tag, namespace는 환경별로 다를 수 있기 때문입니다
-
-#### Argo CD
-
-Argo CD는 Git에 있는 선언을 실제 클러스터와 맞추는 실행 주체입니다.
-
-이 프로젝트에서 Argo CD가 하는 일:
-
-- `argocd/applications/dev/**`에 정의된 경로를 감시
-- 해당 경로의 manifest를 dev 클러스터에 동기화
-- 드리프트가 생기면 다시 선언 상태로 되돌리려 함
-
-즉, `apps/auth-server/overlays/dev`를 수정한다는 것은 단순히 파일을 고치는 것이 아니라
-**Argo CD가 나중에 실제 클러스터 상태를 바꾸게 될 선언을 수정하는 것**입니다.
-
-이 세 가지(GitOps, Kustomize, Argo CD)가 맞물리는 전체 흐름을 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart LR
- subgraph Developer["개발자"]
- A["base/deployment.yaml 수정
또는 overlay/configmap.yaml 수정"]
- end
-
- subgraph Git["Git 저장소 (Source of Truth)"]
- B["base/ + overlay/
= 최종 선언"]
- end
-
- subgraph ArgoCD["Argo CD"]
- C["Git 감시
변경 감지"] --> D["Kustomize로
base + overlay 합성"]
- D --> E["합성 결과와
현재 클러스터 비교"]
- end
-
- subgraph Cluster["K8s 클러스터"]
- F["실제 리소스
Deployment, Service 등"]
- end
-
- A -->|"git push"| B
- B -->|"Watch"| C
- E -->|"차이 있으면
kubectl apply"| F
- F -->|"드리프트 발생 시
다시 선언으로 복원"| E
-```
-
-개발자는 Git만 수정합니다. 클러스터를 직접 건드리지 않습니다. Argo CD가 Git의 선언과 클러스터의 실제 상태를 계속 비교하고, 차이가 있으면 선언 쪽으로 맞춥니다. 만약 누군가 `kubectl edit`으로 클러스터를 직접 수정하면, Argo CD가 그것을 "드리프트"로 감지하고 Git 기준으로 되돌립니다.
-
-### 5-5. Secret 관리: Kubernetes Secret, SealedSecret, Vault
-
-이 프로젝트의 핵심 설계 중 하나는 "무엇을 어디에 저장할 것인가"입니다.
-
-#### Kubernetes Secret
-
-Kubernetes Secret은 Kubernetes 안에서 secret을 다루기 위한 기본 기능입니다.
-하지만 이 프로젝트에서는 runtime secret의 최종 해답으로 보지 않습니다.
-
-이유:
-
-- 클러스터 안에 secret 복사본이 많이 생기기 쉽다
-- GitOps 저장소에 그대로 두기 어렵다
-- 장기 자격증명을 쉽게 만들 수 있다
-
-#### SealedSecret
-
-SealedSecret은 "Git에 올릴 수 있게 암호화된 Secret"입니다.
-
-이 프로젝트에서 SealedSecret이 남아 있는 이유:
-
-- `ghcr-regcred` 같은 image pull secret은 **Pod가 뜨기 전**에 필요하다
-- Vault Agent는 Pod 생성 이후에 동작한다
-- 즉, 이미지 pull credential은 Vault injection만으로 해결할 수 없다
-
-그래서 이 프로젝트는 아래처럼 분리합니다.
-
-- image pull secret: SealedSecret
-- runtime secret: Vault
-
-#### Vault
-
-Vault는 "비밀값을 저장하는 곳"이면서 동시에 "권한을 기준으로 필요한 순간에만 비밀을 주는 곳"입니다.
-
-이 프로젝트에서 Vault를 쓰는 이유:
-
-- 앱마다 필요한 secret만 읽게 하기 위해
-- 장기 비밀번호를 Git에서 제거하기 위해
-- dynamic DB credential을 발급하기 위해
-- JWT 서명을 key file 없이 transit으로 처리하기 위해
-
-#### Secret Zero Problem
-
-"Vault에 로그인하려면 처음에 무엇으로 인증하나?"라는 질문이 바로 Secret Zero Problem입니다.
-
-이 프로젝트의 해법은 두 가지입니다.
-
-- 앱/플랫폼 Pod: Kubernetes auth 사용
-- CI / bootstrap automation: AppRole 사용
-
-즉,
-
-- Pod는 자기 ServiceAccount JWT로 신분을 증명하고
-- CI는 별도의 AppRole credential로 로그인합니다
-
-이 두 경로를 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart TB
- subgraph Pod_경로["경로 1: Pod가 Vault에 접근할 때"]
- direction LR
- P1["Pod 내부의
ServiceAccount JWT"] -->|"자동 마운트됨"| P2["Vault Agent가
JWT를 들고 인증"]
- P2 --> P3["Vault가 K8s API에
'이 JWT 진짜야?' 확인"]
- P3 --> P4["Secret 발급"]
- end
-
- subgraph CI_경로["경로 2: CI가 Vault에 접근할 때"]
- direction LR
- C1["GitHub Actions
Secrets에 저장된
Role ID + Secret ID"] --> C2["AppRole 로그인"]
- C2 --> C3["Vault 토큰 발급"]
- C3 --> C4["Terraform 실행"]
- end
-```
-
-Pod 경로에서는 **K8s가 이미 부여한 신분(ServiceAccount)**을 재활용합니다. 별도의 비밀번호가 필요 없습니다. CI 경로에서는 **GitHub Actions의 Secrets 기능**이 Secret Zero를 담당합니다. 완벽하지는 않지만, 비밀번호를 코드에 직접 쓰는 것보다 훨씬 안전합니다.
-
-> 💡 각 인증 방식의 핸드셰이크 상세는 바로 아래 5-6에서, 그리고 프로세스 수준의 동작은 26장에서 다룹니다.
-
-### 5-6. Kubernetes auth, AppRole, Transit, Dynamic Secret
-
-이 네 개념은 이 저장소를 이해할 때 반드시 구분해야 합니다.
-
-| 개념 | 무엇인가 | 누가 쓰는가 | 이 프로젝트의 예시 |
-| --------------- | ------------------------------------------- | ----------------------------- | -------------------------------------- |
-| Kubernetes auth | Pod의 ServiceAccount로 Vault 로그인 | 앱/플랫폼 Pod | `auth-server`, `postgres`, `keycloak` |
-| AppRole | 기계용 Vault 로그인 방식 | CI, bootstrap, 운영 자동화 | `vault-dev-reconcile` workflow |
-| Transit | 키를 직접 밖으로 꺼내지 않고 암호 연산 제공 | auth-server, auto-unseal 구조 | JWT signing, workload Vault unseal |
-| Dynamic secret | 일정 시간이 지나면 만료되는 자격증명 | migration Job, operator | `database/creds/auth-db-migration-dev` |
-
-이 네 가지를 섞어 생각하면 안 됩니다.
-
-- Kubernetes auth는 "Pod용 로그인 방식"
-- AppRole은 "비Pod 자동화용 로그인 방식"
-- Transit은 "암호 기능"
-- Dynamic secret은 "짧게 사는 계정"
-
-이 네 가지가 이 프로젝트의 어디에서 쓰이는지 한 눈에 보면 이렇습니다.
-
-```mermaid
-flowchart TB
- subgraph Vault["Workload Vault"]
- KV["KV Engine
(고정 secret 저장)"]
- DB["Database Engine
(동적 계정 발급)"]
- TR["Transit Engine
(암호 연산)"]
- KA["K8s Auth
(Pod 로그인)"]
- AR["AppRole Auth
(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으로
policy/role/secret 설정"| KV
-```
-
-auth-server는 K8s Auth로 로그인해서 KV(고정 비밀번호)와 Transit(JWT 서명)을 사용합니다. migration Job은 K8s Auth로 로그인해서 Database Engine(임시 계정)을 사용합니다. CI는 AppRole로 로그인해서 Terraform으로 설정을 관리합니다. **같은 Vault지만 로그인 방식과 사용하는 엔진이 다릅니다.**
-
-> 💡 각 엔진과 인증 방식의 내부 동작은 26장에서 상세히 다룹니다.
-
-### 5-7. Terraform, State, 멱등성, Bootstrap vs Reconcile
-
-Terraform은 단순히 "리소스를 만드는 도구"가 아닙니다.
-핵심은 **현재 상태와 원하는 상태의 차이를 계산한다**는 점입니다.
-
-#### State
-
-Terraform state는 Terraform이 "내가 무엇을 만들었는지" 기억하는 파일입니다.
-
-이 프로젝트에서 state가 중요한 이유:
-
-- Vault 안에는 정책, auth backend, AppRole, database role 등 많은 리소스가 있다
-- 이것을 사람이 매번 수동으로 비교하는 것은 어렵다
-- Terraform이 state를 바탕으로 diff를 계산해야 반복 적용이 안전해진다
-
-#### 멱등성(Idempotency)
-
-멱등성이란 "같은 작업을 여러 번 해도 결과가 같게 유지되는 성질"입니다.
-
-이 프로젝트에서 왜 중요한가?
-
-- GitHub Actions workflow는 반복 실행될 수 있다
-- Vault reconcile도 여러 번 돌아야 한다
-- 같은 apply가 다시 실행되더라도 리소스가 중복 생성되면 안 된다
-
-#### Bootstrap vs Reconcile
-
-이 프로젝트는 Terraform 루트를 일부러 나눴습니다.
-
-- bootstrap 루트
- 최초 1회, 강한 권한으로 구조를 세우는 용도
-- reconcile 루트
- 이미 세워진 구조를 반복적으로 안전하게 맞추는 용도
-
-왜 이렇게 나눌까?
-
-- CI에 root 수준 권한을 오래 주지 않기 위해
-- 최초 생성과 일상 동기화의 책임을 분리하기 위해
-- 사람이 승인해야 할 작업과 자동화가 해도 되는 작업을 구분하기 위해
-
-이 개념을 먼저 이해해야 뒤에서 나오는:
-
-- `terraform/vault/dev`
-- `terraform/vault/reconcile`
-- `terraform/vault-transit/dev`
-- `terraform/vault-transit/reconcile`
-
-이 네 디렉터리의 의미가 선명해집니다.
-
-이 분리를 권한 경계 관점에서 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart LR
- subgraph Bootstrap["Bootstrap (최초 1회, 사람이 실행)"]
- direction TB
- B1["vault/dev
mount 생성, auth backend 활성화
root 수준 권한 필요"]
- B2["vault-transit/dev
transit key 생성, AppRole 생성
root 수준 권한 필요"]
- end
-
- subgraph Reconcile["Reconcile (반복, CI가 실행)"]
- direction TB
- R1["vault/reconcile
policy 업데이트, role 업데이트
secret 복사, DB role 설정
제한된 권한으로 충분"]
- R2["vault-transit/reconcile
policy 업데이트, role 업데이트
제한된 권한으로 충분"]
- end
-
- Bootstrap -->|"구조가 세워진 뒤
이후는 reconcile만"| Reconcile
-```
-
-핵심은 **CI(GitHub Actions)가 root 토큰을 갖지 않는다**는 것입니다. Bootstrap은 운영자가 직접, 한 번만 실행합니다. 이후 CI는 reconcile 루트만 반복 실행하며, 최소한의 권한으로 기존 구조를 유지보수합니다.
-
-> 💡 Terraform의 State 관리, Plan/Apply 사이클, Provider 이중 설정 등 내부 메커니즘은 27장에서 상세히 다룹니다.
-
-## 6. 이 저장소의 큰 흐름
-
-```mermaid
-flowchart TD
- A[App repo CI] --> B[GitOps repo image tag update]
- C[main push or infra change] --> D[.github/workflows/vault-dev-reconcile.yaml]
- D --> E[scripts/ci/reconcile-vault-dev.sh]
- E --> F[terraform/vault-transit/reconcile]
- E --> G[terraform/vault/reconcile]
- E --> H[argocd/applications/dev]
- H --> I[Argo CD sync]
- I --> J[apps/auth-server]
- I --> K[apps/api-server]
- I --> L[infra/platform]
- I --> M[infra/vault]
- I --> N[infra/vault-transit]
- N --> M
- M --> J
- M --> L
- J --> K
-```
-
-이 그림을 문장으로 풀면 아래와 같습니다.
-
-1. 앱 저장소 CI가 새 이미지를 만들면 이 저장소의 overlay 이미지 태그를 바꿉니다.
-2. 이 저장소의 `main`에 변경이 들어오면 `vault-dev-reconcile` 워크플로가 실행됩니다.
-3. 워크플로는 Bash 스크립트를 통해 먼저 Vault 관련 상태를 맞춥니다.
-4. 그 다음 Argo CD Application 정의를 적용합니다.
-5. Argo CD가 실제 `apps/`와 `infra/` 폴더를 읽어 클러스터 상태를 맞춥니다.
-6. 앱 Pod는 최종적으로 workload Vault에서 secret을 받아 기동합니다.
-
-## 7. 폴더 지도
-
-| 경로 | 역할 | 여기서 반드시 이해해야 하는 것 |
-| ----------------------------------- | --------------------------------------- | --------------------------------------------------- |
-| `apps/auth-server` | 인증 서버 배포 정의 | DB migration, Vault injection, OAuth/Keycloak 연동 |
-| `apps/api-server` | API 서버 배포 정의 | auth-server가 발급한 JWT를 검증하는 구조 |
-| `infra/platform` | Postgres, Keycloak, Keycloak sync 정의 | 앱이 의존하는 플랫폼 계층 |
-| `infra/vault` | workload Vault 배포 정의 | 앱이 직접 접근하는 Vault |
-| `infra/vault-transit` | provider Vault 배포 정의 | workload Vault auto-unseal 지원 |
-| `argocd/applications/dev` | Argo CD가 어떤 경로를 적용할지 정의 | sync 순서와 대상 namespace |
-| `scripts/ci` | GitHub Actions가 호출하는 자동화 진입점 | reconcile 순서와 안전장치 |
-| `scripts/vault/dev` | workload Vault bootstrap/reconcile 보조 | provider Vault에서 bootstrap 정보를 읽는 방식 |
-| `scripts/vault-transit/dev` | provider Vault bootstrap 보조 | seed secret 입력, seal token 준비 |
-| `terraform/vault/dev` | workload Vault 최초 bootstrap용 루트 | mount, auth backend, policy, role까지 만든다 |
-| `terraform/vault/reconcile` | workload Vault routine reconcile용 루트 | CI가 반복 적용하는 루트 |
-| `terraform/vault-transit/dev` | provider Vault 최초 bootstrap용 루트 | transit key, AppRole, seal Secret 생성 |
-| `terraform/vault-transit/reconcile` | provider Vault routine reconcile용 루트 | CI가 반복 적용하는 provider 쪽 루트 |
-| `runbooks/vault/**` | 사람이 직접 bootstrap할 때 보는 문서 | 왜 bootstrap과 reconcile이 분리됐는지 이해해야 한다 |
-
-## 8. 파일을 읽을 때 항상 던져야 하는 5가지 질문
-
-이 저장소의 어떤 파일이든 아래 5가지 질문으로 읽으면 이해가 훨씬 빨라집니다.
-
-1. 이 파일은 **누가 적용하는가**?
-2. 이 파일은 **누가 소비하는가**?
-3. 이 파일의 값은 **민감한 값인가 아닌가**?
-4. 이 파일은 **최초 1회 bootstrap용인가**, 아니면 **반복 실행되는 reconcile용인가**?
-5. 이 파일을 잘못 바꾸면 **어디가 먼저 깨지는가**?
-
-예를 들어 `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`을 볼 때는 이렇게 읽어야 합니다.
-
-- 누가 적용하는가: Argo CD
-- 누가 소비하는가: Vault Agent Injector와 최종적으로 auth-server 컨테이너
-- 민감한 값인가: 파일 자체는 secret을 담지 않지만 secret 경로를 지정한다
-- bootstrap용인가 reconcile용인가: reconcile 이후 실제 앱 배포에 쓰이는 runtime 정의다
-- 잘못 바꾸면 어디가 깨지는가: Vault secret render 실패, Pod 기동 실패, 로그인 기능 장애
-
-## 9. 대표 시나리오 1: auth-server가 DB 비밀번호를 받는 과정
-
-이 시나리오를 이해하면 이 프로젝트의 핵심을 절반 이상 이해한 것입니다.
-
-### 9-1. 실제 흐름
-
-1. 운영자 또는 초기 bootstrap 절차가 `scripts/vault-transit/dev/populate-workload-seeds.sh`를 실행합니다.
-2. 이 스크립트는 provider Vault의 `kv/dev/workload/platform/postgres/auth-server` 경로에 값을 넣습니다.
-3. `terraform/vault/reconcile/main.tf`는 provider Vault에서 이 값을 읽습니다.
-4. 같은 Terraform이 workload Vault의 `kv/data/dev/platform/postgres/auth-server` 경로로 값을 복사합니다.
-5. `runbooks/vault/dev/policies/auth-server-dev.hcl`는 auth-server가 그 경로를 읽을 수 있게 허용합니다.
-6. `terraform/vault/reconcile/main.tf`는 `auth-dev` namespace의 `auth-server` ServiceAccount에 이 정책을 연결하는 Kubernetes auth role도 만듭니다.
-7. `apps/auth-server/overlays/dev/namespace.yaml`에는 `vault-injection: enabled` 라벨이 있어서 injector webhook 대상이 됩니다.
-8. `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`는 Vault에게 어떤 secret을 어디서 읽을지, 어떤 role로 로그인할지 알려줍니다.
-9. Pod가 뜰 때 Vault Agent가 `/vault/secrets/runtime-env` 파일을 만들어 줍니다.
-10. auth-server 컨테이너는 그 파일을 `source`한 뒤 `java -jar /app/application.jar`로 기동합니다.
-
-이 10단계를 하나의 그림으로 보면 이렇습니다.
-
-```mermaid
-sequenceDiagram
- participant OP as 운영자/Bootstrap
- participant PV as Provider Vault
(vault-transit)
- participant TF as Terraform
- participant WV as Workload Vault
- participant API as K8s API Server
- participant INJ as Vault Agent Injector
- participant POD as auth-server Pod
-
- Note over OP,PV: 1~2단계: Seed 입력
- OP->>PV: populate-workload-seeds.sh로
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 사이드카 추가
- INJ-->>API: 수정된 Pod 정의 반환
- API->>POD: Pod 생성 (원래 컨테이너 + Vault Agent)
- POD->>WV: K8s SA JWT로 인증 → secret 요청
- WV-->>POD: /vault/secrets/runtime-env 파일 생성
- POD->>POD: source runtime-env → exec java
-```
-
-이 그림에서 왼쪽(1~2단계)은 **사람이 최초 1회 하는 일**, 가운데(3~6단계)는 **CI가 반복하는 일**, 오른쪽(7~10단계)는 **매 배포마다 자동으로 일어나는 일**입니다.
-
-> 💡 각 단계의 내부 동작(Mutating Webhook, K8s Auth Handshake, Go 템플릿 렌더링 등)은 26장에서 상세히 다룹니다.
-
-### 9-2. 여기서 꼭 이해해야 하는 파일
-
-| 파일 | 역할 | 왜 중요한가 |
-| ----------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------- |
-| `scripts/vault-transit/dev/populate-workload-seeds.sh` | 최초 seed 입력 | secret의 진짜 출발점이다 |
-| `terraform/vault/reconcile/main.tf` | provider Vault -> workload Vault 동기화 | secret이 어디로 복사되는지 결정한다 |
-| `runbooks/vault/dev/policies/auth-server-dev.hcl` | 읽기 권한 제한 | auth-server가 무엇을 읽을 수 있는지 최소 권한으로 제한한다 |
-| `apps/auth-server/overlays/dev/deployment.vault-patch.yaml` | Pod 기동 시 secret 주입 설정 | secret이 파일로 렌더되고 실행 시점에 읽힌다 |
-
-### 9-3. 왜 이렇게 복잡하게 하나
-
-단순히 Kubernetes Secret에 DB 비밀번호를 넣어도 앱은 뜹니다. 그런데 이 프로젝트는 그렇게 하지 않습니다.
-
-- secret 원본을 Git에 두지 않기 위해
-- 사람이 직접 장기 토큰을 여기저기 복붙하지 않기 위해
-- Pod마다 필요한 secret만 읽게 하기 위해
-- 이후 동적 계정, transit 서명, 최소 권한 같은 보안 정책을 일관되게 적용하기 위해
-
-### 9-4. 잘못 건드렸을 때 생기는 부작용
-
-| 잘못된 변경 | 바로 생기는 문제 | 더 큰 문제 |
-| ------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------- |
-| policy 경로를 너무 좁게 적음 | auth-server가 secret을 못 읽고 기동 실패 | 배포는 성공한 것처럼 보여도 앱은 죽는다 |
-| policy 경로를 너무 넓게 적음 | 당장은 잘 동작 | auth-server 침해 시 읽을 수 있는 비밀 범위가 커진다 |
-| patch에서 `automountServiceAccountToken: true`를 빼먹음 | Vault Kubernetes auth 실패 | injector가 secret을 렌더링하지 못한다 |
-| secret을 `ConfigMap`으로 옮김 | 당장은 쉬워 보임 | 민감한 값이 GitOps 정의 안으로 들어온다 |
-
-## 10. 대표 시나리오 2: 왜 auth-db-migration은 Job인가
-
-`auth-server`는 앱이 하나만 있는 것이 아니라, **DB migration Job + 앱 Deployment** 두 가지로 구성됩니다.
-
-### 10-1. 관련 파일
-
-| 파일 | 역할 |
-| ----------------------------------------------------------------- | -------------------------------------------- |
-| `apps/auth-server/base/db-migration-job.yaml` | 실제 migration Job 정의 |
-| `apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml` | migration Job에 Vault 기반 동적 DB 계정 주입 |
-| `apps/auth-server/base/deployment.yaml` | 실제 API 서버 프로세스 |
-
-### 10-2. 왜 Job인가
-
-DB schema 변경은 "앱이 떠 있는 동안 계속 돌고 있어야 하는 프로세스"가 아닙니다.
-
-- 한 번 실행해서 끝나야 합니다.
-- 앱보다 먼저 끝나야 합니다.
-- 실패하면 앱이 뜨기 전에 문제를 알리는 것이 더 안전합니다.
-
-그래서 `db-migration-job.yaml`에는 아래 Argo CD 어노테이션이 들어 있습니다.
-
-- `argocd.argoproj.io/hook: PreSync`
-- `argocd.argoproj.io/sync-wave: "-1"`
-
-의미는 이렇습니다.
-
-- 일반 리소스보다 먼저 돈다
-- 성공하면 지워도 된다
-- 성공 전에는 뒤에 있는 앱 배포가 진행되지 않도록 앞단에서 멈춘다
-
-이 타이밍을 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart LR
- subgraph PreSync["① PreSync 단계"]
- M["db-migration Job
sync-wave: -1"] --> MC{"Migration
결과"}
- MC -->|"성공 ✅"| NEXT["다음 단계로"]
- MC -->|"실패 ❌"| STOP["전체 sync 중단
앱 배포 안 함"]
- end
-
- subgraph Sync["② Sync 단계"]
- D["auth-server Deployment
앱 배포"]
- end
-
- NEXT --> D
-```
-
-Migration Job이 성공해야만 auth-server Deployment가 배포됩니다. Migration이 실패하면 앱 배포가 아예 진행되지 않아서, schema가 안 맞는 상태로 앱이 뜨는 위험을 방지합니다.
-
-> 💡 Flyway의 lock 메커니즘과 Job 분리 전략의 상세는 24장에서 다룹니다.
-
-
-### 10-3. 왜 동적 계정을 쓰는가
-
-`apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml`를 보면 migration Job은 `database/creds/auth-db-migration-dev`를 읽습니다.
-
-이것은 KV에 저장된 고정 비밀번호가 아니라, **Vault database engine이 짧은 TTL을 가진 계정을 그때그때 발급**한다는 뜻입니다.
-
-이 구조를 택한 이유는 다음과 같습니다.
-
-- migration은 고권한 작업일 수 있으므로 장기 계정을 남기고 싶지 않다
-- Job은 짧게 실행되므로 동적 계정과 잘 맞는다
-- 누가 언제 어떤 계정을 발급받았는지 추적하기 쉽다
-
-### 10-4. 이 설계에서 고민해야 할 점
-
-| 고민 | 왜 해야 하는가 | 이 저장소의 답 | 잘못 선택했을 때 |
-| ------------------------------------------ | ---------------------------------------------------------- | ------------------ | ------------------------------- |
-| migration을 앱 시작 로직에 넣을까? | 서버 여러 대가 동시에 뜨면 schema 변경 경쟁이 생길 수 있다 | 별도 Job으로 분리 | 동시 실행, 락 충돌, 배포 불안정 |
-| migration 계정을 고정할까 동적으로 만들까? | 장기 계정이 유출되면 피해가 길어진다 | 동적 계정 사용 | 유출 시 회수와 추적이 어려움 |
-| 실패 시 앱도 뜨게 할까? | schema가 안 맞는데 앱이 뜨면 더 큰 장애를 만든다 | PreSync에서 막는다 | 런타임 예외, 데이터 손상 가능성 |
-
-## 11. 대표 시나리오 3: auth-server가 JWT를 서명하는 과정
-
-이 부분은 처음 보면 특히 헷갈립니다. auth-server는 단순히 DB secret만 읽는 것이 아닙니다. **Vault Transit을 이용해 JWT 서명도 수행**합니다.
-
-### 11-1. 관련 파일
-
-| 파일 | 역할 |
-| ----------------------------------------------------------- | -------------------------------------------------------------------------- |
-| `apps/auth-server/overlays/dev/configmap.yaml` | auth-server가 Vault transit으로 JWT를 다룬다는 설정 제공 |
-| `apps/auth-server/overlays/dev/deployment.vault-patch.yaml` | Vault token file을 컨테이너로 전달 |
-| `runbooks/vault/dev/policies/auth-server-dev.hcl` | `transit/keys/project-auth-jwt`, `transit/sign/project-auth-jwt` 접근 허용 |
-| `terraform/vault/dev/main.tf` | `project-auth-jwt` transit key 생성 |
-
-### 11-2. 핵심 이해 포인트
-
-- auth-server는 로컬에서 개인키를 직접 생성하지 않습니다.
-- Vault transit engine에 "이 key로 서명해 달라"고 요청합니다.
-- 그래서 애플리케이션 컨테이너 안에 장기 서명 키 파일이 존재하지 않습니다.
-
-이 과정을 그림으로 보면 이렇습니다.
-
-```mermaid
-sequenceDiagram
- participant User as 사용자
- participant Auth as auth-server
- participant Vault as Workload Vault
(Transit Engine)
- participant API as api-server
-
- User->>Auth: "로그인 해줘"
- Auth->>Auth: 사용자 확인, JWT 페이로드 준비
- Auth->>Vault: "transit/sign/project-auth-jwt로
이 페이로드에 서명해줘"
- Note over Vault: 내부의 RSA 개인키로 서명
개인키는 Vault 밖으로 절대 안 나감
- Vault-->>Auth: 서명된 JWT 반환
- Auth-->>User: JWT 토큰 전달
-
- User->>API: JWT를 담아 API 호출
- API->>Vault: "transit/keys/project-auth-jwt로
공개키 읽기"
- Vault-->>API: RSA 공개키 반환
- API->>API: 공개키로 JWT 서명 검증
- API-->>User: API 응답
-```
-
-핵심은 **auth-server가 개인키를 한 번도 본 적이 없다**는 것입니다. 서명이 필요할 때 Vault API를 호출하고, Vault가 내부에서 서명한 결과만 돌려줍니다. api-server는 공개키만 가져와서 검증합니다.
-
-이 방식은 운영 난이도는 조금 올라가지만, **키 보관을 중앙화**하고 **키 유출 위험을 줄이는 장점**이 있습니다.
-
-> 💡 Transit Engine의 키 Rotation, 이중 역할(JWT 서명 + Auto-Unseal), 성능 트레이드오프는 26장에서 상세히 다룹니다.
-
-## 12. auth-server 파일을 어떻게 읽어야 하는가
-
-### 12-1. `apps/auth-server/base/kustomization.yaml`
-
-이 파일은 auth-server를 구성하는 공통 리소스 목록입니다.
-
-- `serviceaccount.yaml`
-- `auth-db-migration-serviceaccount.yaml`
-- `service.yaml`
-- `db-migration-job.yaml`
-- `deployment.yaml`
-
-이 단계에서 꼭 봐야 하는 것은 "auth-server는 서비스 하나가 아니라 **서비스 + migration job**으로 구성된다"는 사실입니다.
-
-### 12-2. `apps/auth-server/base/deployment.yaml`
-
-이 파일은 secret 주입이 없는 기본 뼈대입니다. 여기서 눈여겨볼 지점은 다음과 같습니다.
-
-| 항목 | 의미 | 왜 이렇게 했는가 |
-| ------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------- |
-| `serviceAccountName: auth-server` | 이 Pod의 Kubernetes 신분 | 나중에 Vault Kubernetes auth와 연결된다 |
-| `automountServiceAccountToken: false` | 기본적으로 SA 토큰을 Pod에 넣지 않음 | 필요한 경우에만 토큰을 노출하려는 보안 기본값 |
-| `runAsNonRoot`, `seccompProfile` | 보안 기본 설정 | root 실행과 과도한 시스템 호출을 줄인다 |
-| `envFrom`의 `configMapRef`와 `secretRef` | 기본 설계상 설정/비밀을 받는 자리 | dev에서는 overlay patch로 secret 부분이 Vault 방식으로 대체된다 |
-| `readinessProbe`, `livenessProbe`, `startupProbe` | 준비/생존/초기 부팅 상태 확인 | 느린 기동과 장애를 구분하기 위함 |
-
-중요한 포인트는 **base의 값이 최종값이 아닐 수 있다**는 것입니다. overlay patch가 들어오면 일부 항목은 바뀝니다.
-
-### 12-3. `apps/auth-server/overlays/dev/kustomization.yaml`
-
-이 파일은 dev 환경에서 auth-server가 실제로 어떤 모습으로 배포되는지 정합니다.
-
-주요 포인트:
-
-- namespace는 `auth-dev`
-- dev 전용 `configmap`, `ingress`, `networkpolicy`, `sealedsecret`를 추가
-- `deployment.vault-patch.yaml`, `db-migration-job.vault-patch.yaml`로 base를 덮어씀
-- 이미지 태그는 여기서 관리됨
-
-즉, 실제 dev 배포를 이해하려면 **base만 보면 안 되고 overlay까지 합쳐서 봐야** 합니다.
-
-이 합성 과정을 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart TD
- subgraph Base["base/ (공통 뼈대)"]
- B1["deployment.yaml
automountServiceAccountToken: false
command/args: 없음"]
- B2["service.yaml"]
- B3["serviceaccount.yaml"]
- end
-
- subgraph Overlay["overlays/dev/ (환경별 차이)"]
- O1["deployment.vault-patch.yaml
automountServiceAccountToken: true
command/args: 추가"]
- O2["configmap.yaml (새로 추가)"]
- O3["networkpolicy.yaml (새로 추가)"]
- O4["namespace: auth-dev"]
- end
-
- subgraph Result["최종 배포 결과 (Kustomize 합성)"]
- R1["deployment.yaml
automountServiceAccountToken: true ← patch로 변경
command/args: Vault 시작 명령 추가"]
- end
-
- B1 --> R1
- O1 -->|"패치 적용
(strategic merge)"| R1
-```
-
-base에서 `automountServiceAccountToken: false`이지만, overlay patch가 이것을 `true`로 덮어씁니다. **즉, base만 보고 "토큰이 안 들어가네"라고 판단하면 틀립니다.** overlay까지 합쳐야 실제 동작을 알 수 있습니다.
-
-### 12-4. `apps/auth-server/overlays/dev/configmap.yaml`
-
-이 파일은 민감하지 않은 값만 둡니다.
-
-대표 항목:
-
-- `APP_DATASOURCE_URL`
-- `APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI`
-- `APP_SECURITY_JWT_ISSUER`
-- `APP_SECURITY_JWT_VAULT_ENABLED`
-- `APP_SECURITY_JWT_VAULT_ADDRESS`
-- `APP_SECURITY_JWT_VAULT_MOUNT_PATH`
-
-여기서 중요한 기준은 간단합니다.
-
-- 값이 공개되어도 치명적이지 않으면 `ConfigMap`
-- 비밀번호, client secret, token처럼 노출되면 안 되면 Vault
-
-### 12-5. `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`
-
-이 파일은 dev auth-server의 핵심입니다.
-
-반드시 이해해야 하는 항목:
-
-| 항목 | 의미 | 놓치면 안 되는 이유 |
-| ------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
-| `vault.hashicorp.com/agent-inject: "true"` | injector가 이 Pod를 가로채 secret 파일을 만든다 | 이 값이 없으면 Vault 주입이 시작되지 않는다 |
-| `agent-inject-secret-runtime-env` | 어떤 Vault 경로에서 값을 읽을지 | secret 출처를 정확히 가리킨다 |
-| `agent-inject-template-runtime-env` | secret을 쉘 `export` 형식으로 렌더링 | 앱이 `. /vault/secrets/runtime-env` 로 바로 읽을 수 있다 |
-| `vault.hashicorp.com/role: auth-server-dev` | Vault 로그인 시 사용할 역할 이름 | policy와 Kubernetes auth role 이름이 연결된다 |
-| `automountServiceAccountToken: true` | 이 Pod만 예외적으로 SA 토큰 허용 | Vault가 Pod 신분을 검증하려면 필요하다 |
-| `command`, `args` 재정의 | Java 실행 전에 secret 파일과 token file을 읽음 | Vault 주입 결과를 프로세스 환경에 연결한다 |
-
-이 파일을 읽을 때 꼭 이해해야 하는 역설이 하나 있습니다.
-
-- base에서는 `automountServiceAccountToken: false`
-- overlay patch에서는 `automountServiceAccountToken: true`
-
-왜 이런 모순처럼 보이는 구조를 쓰는가?
-
-- 기본값은 "토큰을 넣지 않는다"가 더 안전하기 때문
-- 하지만 Vault Kubernetes auth를 쓰는 특정 Pod는 로그인에 토큰이 필요하기 때문
-- 그래서 **필요한 워크로드에서만 예외를 허용**한다
-
-이게 바로 "보안 기본값은 닫고, 필요한 곳만 연다"는 설계입니다.
-
-### 12-6. `apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml`
-
-이 파일은 더 중요합니다. migration Job은 고정 secret이 아니라 동적 DB 계정을 받기 때문입니다.
-
-핵심 포인트:
-
-- `database/creds/auth-db-migration-dev` 사용
-- `agent-pre-populate-only: "true"` 사용
-- 컨테이너 시작 전에 secret 파일을 만들어두고, 사이드카를 계속 띄우지 않음
-
-왜 `pre-populate-only`가 어울리는가?
-
-- Job은 짧게 실행되고 끝난다
-- secret을 한 번 받아 실행하면 충분하다
-- 장시간 sidecar를 유지할 필요가 없다
-
-### 12-7. `apps/auth-server/overlays/dev/networkpolicy.yaml`
-
-이 파일은 "막아두고 필요한 것만 연다"는 철학을 가장 잘 보여줍니다.
-
-구조는 다음과 같습니다.
-
-- 기본적으로 ingress, egress 모두 차단
-- DNS만 허용
-- Postgres와 Vault로 가는 egress 허용
-- Traefik에서 들어오는 ingress 허용
-
-이 파일을 수정할 때는 항상 이 질문을 해야 합니다.
-
-- 새로 필요한 네트워크 경로가 정말 있는가?
-- 그 경로는 어느 namespace, 어느 label, 어느 port인가?
-- DNS는 이미 열려 있는가?
-
-가장 흔한 실수는 "앱이 안 뜬다"는 문제를 보고 Deployment만 수정하는 것입니다. 실제 원인은 NetworkPolicy일 수 있습니다.
-
-### 12-8. `namespace.yaml`, `public-access.yaml`, `ghcr-regcred.sealedsecret.yaml`
-
-이 세 파일은 초보자가 자주 지나치지만, 실제로는 구조 이해에 매우 중요합니다.
-
-`apps/auth-server/overlays/dev/namespace.yaml`
-
-- `auth-dev` namespace를 만든다
-- `vault-injection: enabled` 라벨을 준다
-- Pod Security 관련 라벨도 같이 준다
-
-여기서 중요한 것은 `vault-injection: enabled`입니다.
-`argocd/applications/dev/infra/vault-agent-injector.yaml`를 보면 injector webhook은 **이 라벨이 있는 namespace에만** 동작합니다.
-
-즉, auth-server가 Vault injection을 받는 이유는 단순히 Deployment patch 때문만이 아니라, **namespace도 injector 대상 조건을 만족**하기 때문입니다.
-
-반대로 `apps/api-server/overlays/dev/namespace.yaml`에는 이 라벨이 없습니다. api-server는 Vault injection을 쓰지 않기 때문입니다.
-
-`apps/auth-server/overlays/dev/public-access.yaml`
-
-- `auth-public`이라는 `ExternalName` Service를 만든다
-- 실제로는 `traefik.kube-system.svc.cluster.local`을 가리킨다
-
-이 구조를 두는 이유는 "클러스터 내부에서도 public host 기준으로 접근하게 만들기 위해서"입니다.
-
-예를 들어 issuer URI나 callback URL은 public host 기준으로 맞추는 편이 일관성이 좋습니다. 그때 내부 Pod가 그 host를 해석했을 때도 Traefik으로 가도록 `ExternalName`을 둡니다.
-
-`apps/auth-server/overlays/dev/ghcr-regcred.sealedsecret.yaml`
-
-- GHCR 이미지 pull secret을 암호화된 형태로 Git에 저장한다
-
-왜 runtime secret은 Vault로 옮기면서 이건 SealedSecret으로 남겼는가?
-
-- 이미지 pull secret은 **Pod가 뜨기 전에** 필요하다
-- Vault Agent는 Pod 생성 후에 동작한다
-- 즉, 이미지를 받기도 전에 필요한 자격증명은 Vault injection으로 해결할 수 없다
-
-이 차이를 이해해야 "왜 어떤 secret은 Vault고 어떤 secret은 SealedSecret이지?"라는 질문이 풀립니다.
-
-## 13. api-server는 auth-server와 무엇이 다른가
-
-`api-server`는 구조가 더 단순합니다. DB migration도 없고 Vault injection도 없습니다.
-
-### 13-1. 관련 파일
-
-| 파일 | 역할 |
-| ------------------------------------------------- | ----------------------------- |
-| `apps/api-server/base/deployment.yaml` | API 서버 기본 Deployment |
-| `apps/api-server/overlays/dev/configmap.yaml` | JWT issuer URI 등 비민감 설정 |
-| `apps/api-server/overlays/dev/networkpolicy.yaml` | Traefik 및 DNS 관련 통신 제어 |
-| `argocd/applications/dev/apps/api-server.yaml` | Argo CD 적용 대상 선언 |
-
-### 13-2. 꼭 봐야 할 포인트
-
-- `APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI`가 `auth-public.auth-dev.svc.cluster.local`을 가리킨다
-- 즉, api-server는 auth-server가 발급한 토큰을 **검증하는 소비자**다
-- auth-server가 죽거나 issuer 주소가 틀리면 api-server의 인증 기능도 깨질 수 있다
-
-이 저장소를 읽을 때 흔히 하는 실수는 `api-server`를 완전히 독립된 서비스로 보는 것입니다. 하지만 인증 관점에서는 **auth-server에 의존**합니다.
-
-이 의존 관계를 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart LR
- USER["브라우저"] --> API["api-server
JWT 검증만 수행"]
- USER --> AUTH["auth-server
JWT 발급"]
- AUTH -->|"OIDC"| KC["Keycloak"]
- AUTH -->|"JWT 서명"| VAULT["Vault Transit"]
- AUTH -->|"DB R/W"| PG["Postgres"]
- API -->|"공개키 읽기
(issuer URI 경유)"| AUTH
-
- style API fill:#e8f5e9
- style AUTH fill:#fff3e0
-```
-
-api-server는 auth-server의 issuer URI를 통해 공개키를 가져와 JWT를 검증합니다. auth-server가 죽거나 issuer 주소가 바뀌면, api-server도 인증을 못 합니다.
-
-## 14. platform 계층을 같이 봐야 하는 이유
-
-`auth-server`만 보면 "DB URL이 왜 저기지?", "Keycloak client secret은 어디서 쓰이지?"가 남습니다. 그래서 `infra/platform`도 같이 봐야 합니다.
-
-### 14-1. Postgres
-
-`infra/platform/base/postgres-statefulset.yaml`과 `infra/platform/overlays/dev/postgres.vault-patch.yaml`를 같이 봐야 합니다.
-
-이 조합에서 확인해야 할 것:
-
-- 왜 `StatefulSet`인가
-- `AUTH_DB_PASSWORD`, `KEYCLOAK_DB_PASSWORD`도 Vault에서 주입받는가
-- 초기 DB 생성 스크립트는 어떻게 들어가는가
-
-여기서 배울 점은 "앱만 Vault를 쓰는 게 아니라 **플랫폼 컴포넌트도 Vault를 쓴다**"는 것입니다.
-
-### 14-2. Keycloak
-
-`infra/platform/base/keycloak-deployment.yaml`과 `infra/platform/overlays/dev/keycloak.vault-patch.yaml`를 보면 Keycloak도 DB 비밀번호와 bootstrap admin 비밀번호를 Vault에서 받습니다.
-
-이 구조를 이해해야 `auth-server`의 OAuth 설정이 왜 Keycloak과 맞물리는지 보입니다.
-
-### 14-3. Keycloak client sync Job
-
-`infra/platform/base/keycloak-client-sync-job.yaml`과 `infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml`는 "Keycloak client 설정도 코드로 맞춘다"는 것을 보여줍니다.
-
-이 Job이 하는 일:
-
-- Keycloak admin 계정으로 로그인
-- `project-auth-server` client를 찾음
-- client secret, base URL, redirect URI, web origins를 업데이트
-
-즉, 사람 손으로 Keycloak 콘솔을 클릭하지 않고 **코드와 Job으로 클라이언트 설정을 맞추는 구조**입니다.
-
-이 platform 계층의 의존 관계를 그림으로 정리하면 이렇습니다.
-
-```mermaid
-flowchart TB
- VAULT["Workload Vault
(secret 제공)"] --> PG["Postgres
(StatefulSet)"]
- VAULT --> KC["Keycloak
(Deployment)"]
- PG --> KC_SYNC["Keycloak Client
Sync Job"]
- KC --> KC_SYNC
-
- PG --> AUTH["auth-server"]
- KC --> AUTH
- VAULT --> AUTH
- KC_SYNC -.->|"client secret 설정
redirect URI 설정"| AUTH
-
- AUTH -->|"JWT issuer"| API["api-server"]
-```
-
-Vault가 모든 컴포넌트에 secret을 제공하고, Postgres와 Keycloak이 auth-server의 기반이 되며, Keycloak client sync Job이 auth-server와 Keycloak 사이의 설정을 자동으로 맞춥니다. **앱(auth-server)만 보면 이 기반이 보이지 않습니다.**
-
-## 15. Argo CD는 정확히 무엇을 하는가
-
-앱과 인프라 YAML을 실제로 클러스터에 반영하는 주체는 Argo CD입니다.
-
-### 15-1. 꼭 읽어야 할 파일
-
-| 파일 | 의미 |
-| --------------------------------------------------------- | ----------------------------- |
-| `argocd/applications/dev/infra/vault-transit.yaml` | provider Vault 배포 선언 |
-| `argocd/applications/dev/infra/vault.yaml` | workload Vault 배포 선언 |
-| `argocd/applications/dev/infra/platform.yaml` | Postgres, Keycloak 배포 선언 |
-| `argocd/applications/dev/apps/auth-server.yaml` | auth-server dev 배포 선언 |
-| `argocd/applications/dev/apps/api-server.yaml` | api-server dev 배포 선언 |
-| `argocd/applications/dev/infra/vault-agent-injector.yaml` | injector Helm chart 배포 선언 |
-
-### 15-2. sync wave를 꼭 이해해야 하는 이유
-
-현재 dev 기준 순서는 대략 아래입니다.
-
-- Vault, Vault Transit, Vault Agent Injector: `10`
-- Platform: `20`
-- auth-server: `30`
-- api-server: `40`
-
-왜 이런 순서가 필요한가?
-
-- Vault와 injector가 먼저 있어야 secret 주입이 가능하다
-- platform이 먼저 있어야 auth-server가 붙을 Postgres와 Keycloak이 준비된다
-- auth-server가 먼저 있어야 api-server가 issuer를 안정적으로 참조할 수 있다
-
-순서를 잘못 잡으면 "코드는 맞는데 배포만 실패하는" 문제가 생깁니다.
-
-이 순서를 타임라인으로 보면 이렇습니다.
-
-```mermaid
-flowchart LR
- W10["wave 10
Vault Transit
Vault
Agent Injector"] --> W20["wave 20
Platform
(Postgres, Keycloak)"]
- W20 --> W30["wave 30
auth-server"]
- W30 --> W40["wave 40
api-server"]
-
- W10 -.->|"이것 없이 다음 단계로 가면
secret 주입 실패"| W20
- W20 -.->|"이것 없이 다음 단계로 가면
DB 연결 실패"| W30
- W30 -.->|"이것 없이 다음 단계로 가면
JWT 검증 실패"| W40
-```
-
-각 wave는 **이전 wave가 완료된 후에** 적용됩니다. wave 10이 완료되어야 Vault가 준비되고, wave 20이 완료되어야 Postgres가 준비되고, 그래야 auth-server가 DB와 Vault에 연결할 수 있습니다.
-
-## 16. GitHub Actions와 Bash 스크립트는 무엇을 하는가
-
-이 저장소에서 YAML만큼 중요한 것이 `scripts/`와 `.github/workflows/`입니다.
-
-### 16-1. `.github/workflows/vault-dev-reconcile.yaml`
-
-이 워크플로는 dev 환경의 routine reconcile 진입점입니다.
-
-주요 단계:
-
-1. 저장소 checkout
-2. 필수 도구 확인
-3. kubeconfig 설정
-4. Argo CD infra 정의 적용
-5. provider Vault reconcile
-6. workload Vault reconcile
-7. Argo CD app 정의 적용
-
-즉, 이 워크플로는 "앱 배포만" 하는 것이 아니라 **Vault 상태를 먼저 맞춘 뒤 앱을 반영**합니다.
-
-### 16-2. `scripts/ci/reconcile-vault-dev.sh`
-
-이 스크립트는 실전 운영 로직의 핵심입니다.
-
-반드시 읽어야 할 이유:
-
-- `require_cmd`, `require_env`로 선행조건을 강제한다
-- `start_port_forward`와 `trap`으로 백그라운드 프로세스를 정리한다
-- `ensure_transit_state_resource`, `ensure_workload_state_resource`로 state import를 자동 보조한다
-- `prepare-infra`, `reconcile-transit`, `reconcile-workload`, `apply-apps`를 분리해 순서를 명확히 한다
-
-이 파일을 이해하지 못하면 "왜 Terraform이 두 번 돌지?", "왜 Argo CD 적용이 나중이지?"가 계속 헷갈립니다.
-
-이 워크플로 전체 흐름을 그림으로 보면 이렇습니다.
-
-```mermaid
-sequenceDiagram
- participant GH as GitHub Actions
- participant SH as reconcile-vault-dev.sh
- participant TF1 as Terraform
(vault-transit/reconcile)
- participant TF2 as Terraform
(vault/reconcile)
- participant ARGO as Argo CD
- participant K8S as K8s 클러스터
-
- GH->>SH: 스크립트 실행
- SH->>SH: require_cmd/require_env
사전조건 검증
- SH->>K8S: Argo CD infra 정의 적용
(Vault, Platform 등)
- SH->>K8S: kubectl wait
Vault Pod 준비 대기
- SH->>TF1: Provider Vault reconcile
(policy, role 업데이트)
- SH->>TF2: Workload Vault reconcile
(secret 복사, auth role 설정)
- SH->>K8S: Argo CD app 정의 적용
(auth-server, api-server)
- ARGO->>K8S: 앱 매니페스트 sync
-```
-
-핵심은 **Vault 상태가 먼저 준비되고, 그 다음에 앱이 배포된다**는 것입니다. 순서가 바뀌면 앱이 secret을 못 받고 기동 실패합니다.
-
-> 💡 이 스크립트의 방어적 프로그래밍(set -euo pipefail, trap, require_cmd)은 27장에서 한 줄씩 해부합니다.
-
-## 17. Terraform은 왜 bootstrap 루트와 reconcile 루트가 분리되어 있는가
-
-이 질문은 꼭 깊게 고민해야 합니다.
-
-> 💡 이 장의 내용은 5-7절에서 개념적으로 다뤄으며, 27장에서 Terraform State, import, Provider 이중 설정 등 내부 메커니즘을 상세히 해부합니다.
-
-### 17-1. 관련 디렉터리
-
-| 경로 | 목적 |
-| ----------------------------------- | -------------------------------- |
-| `terraform/vault-transit/dev` | provider Vault 최초 bootstrap |
-| `terraform/vault-transit/reconcile` | provider Vault routine reconcile |
-| `terraform/vault/dev` | workload Vault 최초 bootstrap |
-| `terraform/vault/reconcile` | workload Vault routine reconcile |
-
-### 17-2. 왜 분리하는가
-
-bootstrap과 reconcile은 필요한 권한이 다릅니다.
-
-- bootstrap은 auth backend 생성, mount 생성, 초기 root 수준 작업이 들어간다
-- reconcile은 이미 만들어진 구조를 반복적으로 맞추는 데 집중한다
-
-이 둘을 섞어버리면 어떤 문제가 생길까?
-
-- CI가 너무 강한 권한을 가져야 한다
-- 실수로 초기화 수준 작업을 routine workflow가 건드릴 수 있다
-- 운영자가 의도한 수동 승인 절차가 사라진다
-
-즉, 이 분리는 단순한 취향이 아니라 **권한 경계와 사고 범위를 줄이기 위한 설계**입니다.
-
-### 17-3. `terraform/vault/reconcile/main.tf`에서 꼭 봐야 하는 것
-
-이 파일은 크게 다섯 가지를 합니다.
-
-1. Vault policy 생성
-2. Kubernetes auth role 생성
-3. AppRole 생성
-4. provider Vault seed 값을 workload Vault KV로 복사
-5. database engine connection과 dynamic role 생성
-
-이 파일을 읽을 때는 리소스를 한 줄씩 보는 것보다, 아래 묶음으로 보는 것이 좋습니다.
-
-- policy 묶음
-- Kubernetes auth role 묶음
-- provider seed -> workload KV 복사 묶음
-- database backend 묶음
-
-### 17-4. `runbooks/vault/dev/policies/*.hcl`는 왜 따로 뒀는가
-
-정책을 Terraform 파일 안에 큰 문자열로 넣을 수도 있습니다. 그런데 이 저장소는 정책을 별도 `*.hcl` 파일로 분리했습니다.
-
-이렇게 한 이유:
-
-- 역할별 권한을 눈으로 검토하기 쉽다
-- PR 리뷰에서 "이 서비스가 읽는 경로가 넓어졌는가"를 바로 볼 수 있다
-- 정책만 따로 읽어도 서비스 권한 모델을 이해할 수 있다
-
-## 18. 왜 `vault-transit`과 `vault`를 둘 다 두는가
-
-이것도 반드시 고민해야 하는 포인트입니다.
-
-### 18-1. 현재 구조
-
-- `vault-transit`: provider Vault
-- `vault`: workload Vault
-
-provider Vault는 주로 아래를 담당합니다.
-
-- workload Vault auto-unseal 지원
-- workload용 seed secret의 source of truth 제공
-- workflow AppRole 정보 제공
-
-workload Vault는 주로 아래를 담당합니다.
-
-- 앱/플랫폼 Pod가 실제로 읽는 runtime secret 제공
-- Kubernetes auth 제공
-- JWT signing transit 제공
-- dynamic DB credential 발급
-
-### 18-2. 왜 하나로 합치지 않았는가
-
-하나로 합치면 구조는 단순해집니다. 하지만 이 저장소는 분리를 선택했습니다.
-
-이유:
-
-- auto-unseal trust boundary를 분리할 수 있다
-- runtime secret 저장소와 unseal provider 역할을 분리할 수 있다
-- CI가 직접 workload Vault root 수준 정보를 오래 들고 있지 않게 만들 수 있다
-
-대신 비용도 있습니다.
-
-- Vault가 2개라 운영 난이도가 올라간다
-- bootstrap 절차가 길어진다
-- 학습 난이도도 높아진다
-
-즉, 이 구조는 "무조건 더 좋다"가 아니라 **보안 경계를 얻는 대신 운영 복잡도를 감수한 선택**입니다.
-
-이 2-Vault 구조의 신뢰 관계를 그림으로 보면 이렇습니다.
-
-```mermaid
-flowchart TB
- subgraph Provider["Provider Vault (vault-transit)"]
- T_UNSEAL["Transit Key
workload-vault-dev-unseal
(Unseal용)"]
- T_JWT["Transit Key
project-auth-jwt
(JWT 서명용)"]
- SEED["Seed Secrets
(원본 비밀번호)"]
- end
-
- subgraph Workload["Workload Vault"]
- KV["KV Engine
(복사된 runtime secret)"]
- DB_ENG["Database Engine
(동적 계정 발급)"]
- K8S_AUTH["K8s Auth
(Pod 인증)"]
- end
-
- T_UNSEAL -->|"Auto-Unseal
마스터키 복호화"| Workload
- SEED -->|"Terraform이
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
-```
-
-Provider Vault는 **Workload Vault의 생명(unseal)**과 **원본 secret(seed)**를 관리합니다. Workload Vault는 **앱이 실제로 사용하는 인터페이스**를 제공합니다. auth-server의 JWT 서명은 Provider Vault의 Transit을 직접 사용하지만, DB 비밀번호 등 runtime secret은 Workload Vault의 KV를 사용합니다.
-
-> 💡 이 구조의 Seal/Unseal 메커니즘, Transit 이중 역할, Agent Injector 동작은 26장에서 상세히 다룹니다.
-
-
-## 19. 이 프로젝트에서 꼭 고민해야 하는 설계 포인트
-
-아래 표는 인턴이 실제로 생각해야 하는 지점을 일부러 늘려 정리한 것입니다.
-
-| 상황 | 왜 고민해야 하는가 | 이 저장소의 현재 선택 | 그렇게 해야 하는 이유 | 잘못 선택했을 때 부작용 |
-| --------------------------------------- | ------------------------------------------ | ---------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------- |
-| 새 설정값 추가 | 설정마다 민감도가 다르다 | 비민감 값은 `ConfigMap`, 민감 값은 Vault | Git과 manifest에 secret을 남기지 않기 위해 | secret 유출 또는 값 추적 어려움 |
-| 새 secret 추가 | source of truth가 어디인지 정해야 한다 | provider Vault seed -> workload Vault 복사 | bootstrap과 runtime을 분리하기 위해 | 경로 불일치, CI와 runtime 동기화 실패 |
-| 앱이 DB schema를 건드림 | 앱과 schema 변경 타이밍이 충돌할 수 있다 | 별도 PreSync Job 사용 | 앱보다 먼저, 한 번만 실행되게 하려는 목적 | 락 경쟁, 앱 부팅 실패, 반쪽 배포 |
-| 새 워크로드가 Vault를 써야 함 | 기본 보안값과 예외를 정해야 한다 | base는 SA token off, 필요한 overlay만 on | 불필요한 토큰 노출을 막기 위해 | Vault 로그인 실패 또는 토큰 과노출 |
-| 새 권한 추가 | 권한 범위를 얼마나 넓힐지 결정해야 한다 | policy 경로를 최소화 | 침해 시 피해 범위를 줄이기 위해 | 과권한 부여 |
-| 새 네트워크 경로 추가 | 통신이 되게 하면서도 너무 열지 말아야 한다 | default deny 후 allowlist | 우연한 통신 의존성을 막기 위해 | 앱 기동 실패 또는 불필요한 개방 |
-| base와 overlay 어디에 둘지 | 환경 독립성과 환경 특수성을 구분해야 한다 | 공통 구조는 base, dev host/path/tag는 overlay | prod/dev 드리프트를 줄이기 위해 | 다른 환경까지 의도치 않게 바뀜 |
-| bootstrap과 reconcile 중 어디를 바꿀지 | 권한 수준과 실행 주체가 다르다 | 최초 생성은 bootstrap, 반복 동기화는 reconcile | CI 권한 최소화와 절차 분리를 위해 | routine workflow가 과도한 권한 요구 |
-| Job와 Deployment 중 무엇을 쓸지 | 실행 수명과 재시작 특성이 다르다 | 일회성은 Job, 지속 서비스는 Deployment | 워크로드의 본질에 맞추기 위해 | 완료돼야 할 작업이 계속 재시작되거나, 계속 살아야 할 앱이 종료됨 |
-| Deployment와 StatefulSet 중 무엇을 쓸지 | 저장소와 정체성 보장이 필요한지 다르다 | Postgres만 StatefulSet | 데이터 볼륨과 안정적 네트워크 식별자가 필요해서 | 데이터 유실, 스토리지 재연결 문제 |
-
-## 20. 실전에서 가장 많이 하는 변경과 수정 위치
-
-### 20-1. auth-server에 비민감 설정값 1개 추가
-
-수정 순서:
-
-1. `apps/auth-server/overlays/dev/configmap.yaml`에 값 추가
-2. 앱이 그 값을 실제로 읽는지 확인
-3. 필요하면 prod overlay에도 대응
-
-이 경우 Vault, Terraform, policy 수정은 보통 필요 없습니다.
-
-### 20-2. auth-server에 민감한 값 1개 추가
-
-수정 순서:
-
-1. `scripts/vault-transit/dev/populate-workload-seeds.sh`에 seed 입력 항목 추가
-2. `terraform/vault/dev/main.tf` 또는 `terraform/vault/reconcile/main.tf`에서 provider -> workload 복사 로직 추가
-3. `runbooks/vault/dev/policies/auth-server-dev.hcl`에 읽기 권한 추가
-4. `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`의 template에 `export` 추가
-5. 앱 코드가 그 환경변수를 읽는지 확인
-
-이 과정 중 하나라도 빠지면 Pod는 떠도 값이 비어 있거나, Vault 권한 오류가 납니다.
-
-### 20-3. auth-server가 새 DB 권한을 요구함
-
-확인 순서:
-
-1. 단순 런타임 읽기인지
-2. migration처럼 고권한 일회성 작업인지
-3. 사람 운영용 접근인지
-
-현재 저장소의 기준:
-
-- 런타임 앱: KV 기반 고정 secret
-- migration / operator: database engine 기반 동적 계정
-- 사람 운영: 짧은 TTL 운영자 토큰
-
-### 20-4. 외부 접근 host를 바꾸고 싶음
-
-수정 후보:
-
-- `apps/auth-server/overlays/dev/ingress.yaml`
-- `apps/auth-server/overlays/dev/public-access.yaml`
-- `apps/api-server/overlays/dev/ingress.yaml`
-- `infra/platform/overlays/dev/keycloak-ingress.yaml`
-- 관련 `ConfigMap`의 issuer/base URL
-- 필요 시 Keycloak client sync 관련 값
-
-host 변경은 단순 Ingress 한 파일 수정으로 끝나지 않는 경우가 많습니다.
-
-## 21. 이 문서를 읽은 뒤 실제로 해봐야 할 연습 문제
-
-아래 연습은 "이해했다"고 착각하지 않게 해줍니다.
-
-1. `APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET`가 어디서 시작해서 어느 파일을 거쳐 auth-server Pod까지 들어오는지 경로를 종이에 적어보세요.
-2. `auth-db-migration`이 왜 `Deployment`가 아니라 `Job`인지, 그리고 왜 `PreSync`인지 설명해보세요.
-3. workload Vault가 재시작되었을 때 누가 어떻게 unseal을 돕는지 설명해보세요.
-4. `api-server`가 왜 DB secret이 필요 없는지, 대신 어떤 서비스에 의존하는지 설명해보세요.
-5. auth-server에 새 secret을 추가해야 한다고 가정하고, 수정해야 할 파일을 빠짐없이 나열해보세요.
-
-이 다섯 개를 막힘 없이 말할 수 있으면, 이 저장소를 "눈으로 본 수준"이 아니라 "실제로 수정 가능한 수준"으로 이해한 것입니다.
-
-## 22. 마지막으로: 이 저장소를 배울 때 절대 잊지 말아야 할 관점
-
-이 저장소는 단순히 YAML 모음이 아닙니다. 각 파일은 아래 네 가지 중 하나의 책임을 갖습니다.
-
-- 클러스터에 무엇을 띄울지 정하는 파일
-- Vault 안에 어떤 권한과 비밀 경로를 만들지 정하는 파일
-- 그 선언을 어떤 순서로 적용할지 정하는 파일
-- 사람이 최초 1회 어떤 절차를 밟아야 하는지 정리한 파일
-
-초보자가 가장 많이 하는 실수는 **한 파일만 보고 이해하려는 것**입니다.
-
-이 저장소는 반드시 연결해서 봐야 합니다.
-
-- 앱 manifest는 Argo CD와 연결해서
-- Vault patch는 policy와 Terraform과 연결해서
-- ConfigMap은 실제 의존 서비스와 연결해서
-- workflow는 bootstrap/runbook과 연결해서
-
-이 관점으로 보면, `apps/`의 YAML, `scripts/`의 Bash, `runbooks/`의 HCL, `terraform/`의 TF가 서로 따로 있는 것이 아니라 **한 배포 시스템의 서로 다른 층**이라는 것이 보이기 시작합니다.
-
----
-
-# Part II: 심층 해부 — 왜 이 기술을 쓰고, 내부에서 무슨 일이 벌어지는가
-
-Part I(1~22장)은 "이 저장소의 파일을 어떻게 읽고 수정하는가"에 집중했습니다.
-
-Part II는 한 걸음 더 들어가서 **"왜 이런 기술을 선택했고, 그 기술이 내부에서 어떤 원리로 돌아가는가"**를 설명합니다.
-
-이 파트를 읽고 나면 Part I에서 표면적으로만 이해했던 개념들이 입체적으로 연결되기 시작합니다.
-
-## 23. 운영체제와 서버 인프라의 근간
-
-### 23-1. Linux vs Windows: 왜 서버는 리눅스인가
-
-#### 서버에 윈도우를 안 쓰는 진짜 이유
-
-우리가 일상에서 쓰는 윈도우 PC를 떠올려 보세요. 부팅하면 바탕화면이 뜨고, 마우스 커서가 나타나고, 시작 메뉴가 보입니다. 이 모든 것이 실행 중인 **프로세스**입니다.
-
-바탕화면을 렌더링하는 프로세스, 마우스 커서를 따라 그리는 프로세스, 알림 영역을 관리하는 프로세스 등이 부팅 직후 수십 개가 올라옵니다. 이것들이 소비하는 메모리만 해도 최소 2~4GB에 달합니다.
-
-서버는 다릅니다. **모니터에 무언가를 보여줄 일이 없습니다.** 서버의 유일한 목적은 "네트워크로 들어온 요청을 처리하고 결과를 돌려주는 것"입니다. 그런데 이걸 하겠다고 바탕화면 렌더링에 몇 GB를 쓰는 것은 자원 낭비입니다.
-
-리눅스 서버는 **텍스트 터미널만** 있습니다. GUI가 없으니 그래픽 프로세스가 전혀 올라오지 않습니다. 같은 하드웨어에서 운영체제가 차지하는 메모리가 수백 MB 수준이라, 나머지 리소스를 전부 실제 서비스(Java, Postgres, Vault 등)에 쓸 수 있습니다.
-
-하지만 이것만이 리눅스를 서버로 쓰는 이유는 아닙니다. 더 근본적인 이유가 있습니다.
-
-#### "모든 것이 파일이다" — 리눅스의 설계 철학
-
-리눅스에는 **"Everything is a file"**이라는 설계 원칙이 있습니다.
-
-이게 무슨 뜻인지 구체적으로 보겠습니다.
-
-| 대상 | 윈도우에서는 | 리눅스에서는 |
-|---|---|---|
-| 하드디스크 | "디스크 관리" 프로그램으로 관리 | `/dev/sda` 라는 파일로 접근 |
-| USB 장치 | 장치 관리자에서 확인 | `/dev/usb/...` 파일로 접근 |
-| 네트워크 설정 | 제어판 → 네트워크 설정 GUI | `/proc/net/...` 파일을 읽으면 됨 |
-| 실행 중인 프로세스 정보 | 작업 관리자 GUI | `/proc/[PID]/...` 파일을 읽으면 됨 |
-| 시스템 로그 | 이벤트 뷰어 GUI | `/var/log/...` 텍스트 파일을 읽으면 됨 |
-
-하드디스크든, 네트워크 포트든, 실행 중인 프로세스의 정보든, 리눅스에서는 **전부 파일처럼 읽고 쓸 수 있습니다.** 이 통일성이 왜 중요할까요?
-
-Kubernetes는 컨테이너 안의 리소스 사용량을 모니터링하고, 네트워크를 설정하고, 볼륨을 마운트합니다. 이 모든 작업을 리눅스에서는 "파일을 읽고 쓰는 것"으로 통일해서 처리할 수 있습니다. 만약 하드디스크 접근은 A 방식, 네트워크 접근은 B 방식, 프로세스 접근은 C 방식이라면 Kubernetes 같은 오케스트레이터를 만드는 것 자체가 훨씬 어려워집니다.
-
-#### 그렇다면 우리 윈도우 개발 PC에서 Docker는 어떻게 돌아가는가?
-
-여기서 자연스러운 의문이 생깁니다. "서버는 리눅스인데, 내 개발 PC는 윈도우잖아? 그런데 Docker로 리눅스 컨테이너를 돌리고 있잖아? 이게 어떻게 가능하지?"
-
-답은 **WSL2(Windows Subsystem for Linux 2)**입니다. WSL2는 단순한 에뮬레이터가 아닙니다. 마이크로소프트가 **진짜 리눅스 커널**을 윈도우 안에서 돌리는 구조를 만든 것입니다.
-
-```mermaid
-flowchart TB
- subgraph 우리_PC["우리 개발 PC (Windows)"]
- direction TB
- WIN[Windows NT 커널
바탕화면, 마우스, VS Code 등 실행]
-
- subgraph HYPERV["Hyper-V 가상화 층"]
- direction TB
- LINUX[진짜 Linux 커널
WSL2가 제공하는 경량 가상머신]
- end
-
- subgraph DOCKER["Docker Desktop"]
- direction TB
- ENGINE[Docker Engine 데몬
Linux 커널 위에서 실행됨]
- ENGINE --> C1[auth-server 컨테이너]
- ENGINE --> C2[postgres 컨테이너]
- ENGINE --> C3[vault 컨테이너]
- end
- end
-
- WIN -->|"Hyper-V를 통해
리눅스 커널 호스팅"| LINUX
- LINUX -->|"커널 기능 제공
(cgroups, namespaces)"| ENGINE
-```
-
-이 그림을 단계별로 설명하면 이렇습니다.
-
-1. 우리 윈도우 PC의 `Windows NT 커널`이 먼저 부팅됩니다. 바탕화면, VS Code 등이 여기서 돕니다.
-2. Windows에는 `Hyper-V`라는 가상화 기능이 내장되어 있습니다. 이것이 아주 가벼운 가상머신을 하나 만듭니다.
-3. 그 가상머신 안에 **진짜 리눅스 커널**이 올라갑니다. 에뮬레이션이 아니라 실제 리눅스 커널 바이너리입니다.
-4. Docker Desktop은 이 리눅스 커널 위에서 Docker Engine을 실행합니다.
-5. 우리가 `docker run`으로 띄우는 Postgres, Vault, auth-server 컨테이너들은 모두 이 리눅스 커널의 기능(cgroups, namespaces)을 사용합니다.
-
-그래서 우리 윈도우 PC에서 리눅스 컨테이너가 아무 문제 없이 돌아가는 것입니다. 컨테이너 입장에서는 자기가 리눅스 위에서 돌고 있다고 "느끼는" 것이고, 실제로도 그렇습니다.
-
-#### 리눅스 계열의 차이: Ubuntu, CentOS, Arch는 뭐가 다른가
-
-"리눅스"라고 하면 하나의 운영체제처럼 들리지만, 실제로는 **수십 가지 배포판(Distribution)**이 있습니다. 커널(심장)은 같지만, 그 위에 올라가는 패키지 관리 방식, 기본 설치 범위, 업데이트 정책이 다릅니다.
-
-비유하자면, 같은 엔진을 쓰는 자동차라도 세단, SUV, 트럭이 다르듯이, 같은 리눅스 커널 위에 어떤 옷을 입혔느냐가 배포판의 차이입니다.
-
-| 계열 | 대표 배포판 | 패키지 관리 도구 | 특징 | 서버 적합도 |
-|---|---|---|---|---|
-| Debian 계열 | **Ubuntu**, Debian | `apt` (deb 패키지) | 초보자 친화적, 커뮤니티 크고 자료 많음, 2년마다 LTS(장기 지원) 버전 출시 | ⭐⭐⭐⭐⭐ 가장 많이 사용 |
-| RHEL 계열 | CentOS, Rocky Linux, AlmaLinux | `yum` / `dnf` (rpm 패키지) | 기업 환경에서 검증된 안정성, Red Hat의 상업 지원 가능, 보안 인증(FIPS 등) | ⭐⭐⭐⭐⭐ 엔터프라이즈 표준 |
-| Arch 계열 | **Arch Linux**, Manjaro | `pacman` | 아무것도 기본 설치하지 않음, 사용자가 직접 모든 것을 구성, 롤링 릴리스(항상 최신) | ⭐⭐ 학습용에 가까움 |
-| Alpine | Alpine Linux | `apk` | 극도로 가벼움(5MB급 베이스 이미지), 보안 중심 설계 | ⭐⭐⭐⭐ Docker 이미지 베이스로 인기 |
-
-이 프로젝트의 K3s 노드는 **Ubuntu** 위에서 돌고 있습니다. Ubuntu를 선택한 이유는 K3s 공식 문서의 1순위 지원 대상이고, 자료가 가장 많고, `apt`로 패키지 설치가 간편하기 때문입니다.
-
-PPT에서 언급한 **Arch Linux**는 "아무것도 안 해주니까 직접 다 하면서 원리를 배우기 좋다"는 학습 목적으로 시도한 것입니다. 실제 서버 운영에는 Ubuntu나 RHEL 계열을 쓰는 것이 일반적입니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- 리눅스의 "모든 것이 파일" 원칙이 왜 컨테이너 기술(Docker, K8s)과 궁합이 좋은지, `/proc`와 `/sys` 파일시스템의 역할을 조사해보세요.
-- Alpine Linux가 Docker 이미지 베이스로 인기 있는 이유는 무엇일까요? 반대로, Alpine을 쓰면 생기는 단점(glibc vs musl 차이)은 무엇일까요?
-- 우리 프로젝트의 `hashicorp/vault:1.18` 이미지는 어떤 리눅스 배포판을 베이스로 사용하고 있을까요? `docker inspect`로 확인해보세요.
-- WSL2 없이 윈도우에서 리눅스 컨테이너를 돌릴 수 있는 다른 방법이 있을까요? (힌트: VirtualBox, VMware, Multipass)
-
-### 23-2. 부팅과 커널의 비밀: Dual Boot와 GRUB 부트로더
-
-#### 컴퓨터 전원을 누르면 실제로 무슨 일이 일어나는가
-
-우리는 매일 전원 버튼을 누르지만, 전원 버튼을 누른 순간부터 로그인 화면이 뜰 때까지 컴퓨터 내부에서는 **5단계의 정밀한 체인**이 순서대로 실행됩니다.
-
-이 체인 중 하나라도 실패하면 화면에 아무것도 안 뜹니다. 또는 에러 메시지만 나오고 멈춥니다.
-
-```mermaid
-flowchart TD
- A["1단계: 전원 ON
전기가 메인보드에 공급됨"] --> B["2단계: BIOS / UEFI 실행
메인보드 칩에 박혀있는 초소형 프로그램
하드웨어 점검(POST) 수행"]
- B --> C["3단계: 부트로더 실행
BIOS가 하드디스크의 약속된 위치에서
부트로더 프로그램을 찾아 실행"]
- C --> D{"4단계: OS 선택
부트로더가 설치된 OS 목록을 보여줌
(Dual Boot인 경우)"}
- D -->|"Ubuntu 선택"| E["5a단계: Linux 커널 적재
vmlinuz(커널 바이너리)를
RAM에 통째로 올림"]
- D -->|"Windows 선택"| F["5b단계: Windows Boot Manager
Windows 커널을 RAM에 올림"]
- E --> G["6단계: init / systemd 실행
커널이 PID 1번 프로세스를 생성
이것이 모든 프로세스의 조상"]
- G --> H["7단계: 로그인 화면
또는 텍스트 프롬프트"]
-```
-
-각 단계를 하나씩 풀어보겠습니다.
-
-**1~2단계: BIOS / UEFI**
-
-전원을 누르면 가장 먼저 실행되는 것은 윈도우도 리눅스도 아닙니다. **메인보드 칩에 박혀 있는 아주 작은 프로그램**이 먼저 뜹니다. 이것이 BIOS(오래된 방식) 또는 UEFI(최신 방식)입니다.
-
-이 프로그램이 하는 일은 "하드웨어가 정상인지 확인"하는 것입니다. RAM이 있는지, 하드디스크가 연결되어 있는지, 그래픽카드가 있는지 등을 빠르게 체크합니다(이것을 POST, Power-On Self-Test라고 부릅니다).
-
-**3단계: 부트로더**
-
-하드웨어 점검이 끝나면 BIOS/UEFI는 "이제 운영체제를 찾아야 한다"고 판단합니다. 하지만 BIOS 자체는 운영체제를 이해하지 못합니다. 그래서 하드디스크의 **약속된 위치(EFI System Partition)**에서 **부트로더**라는 중간 프로그램을 찾아서 실행합니다.
-
-부트로더는 쉽게 말하면 **"어떤 운영체제를 켤지 고르는 메뉴판 프로그램"**입니다.
-
-**4단계: OS 선택 (Dual Boot)**
-
-만약 하드디스크에 Ubuntu와 Windows가 둘 다 설치되어 있다면, 부트로더가 두 선택지를 보여줍니다. 사용자가 Ubuntu를 선택하면 리눅스 커널을 RAM에 올리고, Windows를 선택하면 Windows Boot Manager에게 제어를 넘깁니다.
-
-**이것이 듀얼 부팅이 가능한 원리입니다.** 하드디스크에 두 OS가 나란히 깔려 있고, 부트로더가 "어느 쪽 커널을 메모리에 올릴지" 선택하는 것입니다.
-
-**5~6단계: 커널 → PID 1**
-
-리눅스 커널(`vmlinuz`)이 RAM에 올라오면, 커널은 시스템의 모든 하드웨어를 초기화하고, 그 다음 **딱 하나의 프로세스**를 생성합니다. 이것이 `systemd` (또는 구형 시스템에서는 `init`)이고, **PID 번호 1번**을 받습니다.
-
-PID 1번은 **모든 프로세스의 조상**입니다. 이후에 뜨는 SSH 서버, 네트워크 데몬, Docker Engine, K3s kubelet 등은 전부 이 PID 1번의 자손입니다.
-
-Part I의 5-2에서 "왜 `exec`를 써서 PID 1을 Java 프로세스로 교체하는가"를 설명했습니다. 이제 부팅 과정을 알았으니 그 의미가 더 선명해집니다. 컨테이너 안에서도 PID 1은 특별하고, 운영체제에서의 PID 1이 곧 컨테이너의 PID 1과 같은 개념입니다.
-
-#### GRUB 부트로더와 다른 부트로더의 차이
-
-현재 이 프로젝트의 개발 환경은 **GRUB(GRand Unified Bootloader)**을 사용하고 있습니다. GRUB은 리눅스에서 가장 널리 쓰이는 부트로더입니다. 하지만 세상에는 다른 부트로더도 있습니다.
-
-| 부트로더 | 특징 | 커스터마이징 | Dual Boot 지원 | 적합한 상황 |
-|---|---|---|---|---|
-| **GRUB** | 가장 유명, 거의 모든 리눅스 배포판의 기본 부트로더 | 매우 유연(테마, 스크립트, 커널 파라미터 조정 가능) | ⭐⭐⭐⭐⭐ `os-prober`로 다른 OS 감지 | 일반 서버/데스크탑 대부분 |
-| **systemd-boot** | systemd 프로젝트의 일부, 설정이 단순 | 설정 파일이 간결하지만 유연성은 낮음 | ⭐⭐⭐ 수동 설정 필요 | UEFI 전용, 단일 OS 서버 |
-| **rEFInd** | GUI가 예쁘고, EFI 엔트리를 직접 스캔 | 아이콘/테마 커스터마이징 편리 | ⭐⭐⭐⭐⭐ EFI 파티션 직접 탐색 | 맥 + 리눅스 듀얼부팅 |
-| **Syslinux/ISOLINUX** | 극도로 가벼움, 설치 미디어(USB/CD)용 | 제한적 | ⭐⭐ 단일 OS 전용 | 부팅 USB, 임베디드 |
-
-#### GRUB은 어떻게 Windows를 "자동으로" 감지하는가
-
-위 표에서 GRUB이 "다른 OS를 감지한다"고 적었는데, **이것은 마법이 아니라 구체적인 메커니즘**이 있습니다. 이것을 이해해야 "왜 가끔 GRUB이 감지를 못 하는 상황이 생기는지"까지 알 수 있습니다.
-
-Ubuntu에서 `sudo update-grub` 명령을 실행하면 내부적으로 이런 일이 벌어집니다.
-
-```mermaid
-flowchart TD
- A["사용자가 sudo update-grub 실행"] --> B["grub-mkconfig 프로그램 시작
GRUB 메뉴 설정 파일을 자동 생성하는 도구"]
- B --> C["1단계: /boot 디렉터리 스캔
리눅스 커널 파일(vmlinuz)을 찾음"]
- B --> D["2단계: os-prober 실행
다른 OS가 있는지 디스크를 뒤짐"]
- D --> E["os-prober가 하는 일:
① 모든 디스크 파티션을 하나씩 마운트
② 각 파티션에서 OS 흔적을 찾음"]
- E --> F{"Windows 흔적 발견?
예: /EFI/Microsoft/Boot/bootmgfw.efi
또는 NTFS 파티션의 bootmgr"}
- F -->|"흔적 발견"| G["GRUB 메뉴에 'Windows Boot Manager' 항목 추가"]
- F -->|"흔적 없음"| H["GRUB 메뉴에 리눅스만 표시"]
- G --> I["결과: /boot/grub/grub.cfg 파일 생성
이 파일이 부팅 시 메뉴로 표시됨"]
- H --> I
-```
-
-핵심은 **`os-prober`**라는 프로그램입니다. 이 프로그램이 하는 일을 쉽게 설명하면 이렇습니다.
-
-1. 컴퓨터에 연결된 **모든 디스크의 모든 파티션**을 하나씩 열어봅니다.
-2. 각 파티션을 임시로 마운트해놓고, 그 안에 **운영체제의 흔적**이 있는지 찾습니다.
- - Windows의 흔적: EFI 파티션 안의 `/EFI/Microsoft/Boot/bootmgfw.efi` 파일, 또는 NTFS 파티션의 `bootmgr` 파일
- - 다른 리눅스의 흔적: `/boot/vmlinuz` 커널 파일
- - macOS의 흔적: HFS+ 파티션의 특정 구조
-3. 흔적을 찾으면 "이 파티션에 이런 OS가 있다"고 보고합니다.
-4. `grub-mkconfig`가 이 보고를 받아서 GRUB 메뉴 설정 파일(`/boot/grub/grub.cfg`)에 해당 OS 항목을 추가합니다.
-
-그래서 GRUB의 "자동 감지"란 사실 **디스크의 모든 파티션을 뒤져서 알려진 OS의 파일 패턴을 찾는 것**입니다. 이것을 알면 아래 상황이 왜 생기는지도 이해됩니다.
-
-- **Windows를 나중에 깔면 GRUB이 사라지는 이유**: Windows 설치 프로그램이 EFI System Partition의 기본 부트 엔트리를 자기 것(Windows Boot Manager)으로 **덮어씁니다**. GRUB 파일이 지워지는 것은 아니지만, UEFI가 "기본으로 실행할 부트로더"를 Windows 것으로 바꿔버리니까 GRUB 메뉴가 안 뜨는 것입니다. 이 경우 Ubuntu 설치 USB로 부팅한 뒤 `sudo grub-install`로 GRUB을 기본 부트 엔트리로 다시 등록하면 복구됩니다.
-- **`os-prober`가 비활성화된 최신 Ubuntu에서 Dual Boot 메뉴가 안 뜨는 이유**: Ubuntu 21.10부터 보안상의 이유로 `os-prober`가 기본 비활성화되었습니다. `/etc/default/grub`에 `GRUB_DISABLE_OS_PROBER=false`를 추가하고 `sudo update-grub`을 다시 실행해야 Windows가 메뉴에 나타납니다.
-
-반면 **rEFInd**는 `os-prober` 같은 별도 프로그램 없이, **EFI System Partition 안의 `.efi` 파일들을 직접 스캔**해서 부팅 가능한 OS 목록을 만듭니다. EFI 표준 자체가 "이 디렉터리 구조에 부팅 파일을 놓아라"는 규칙을 정해놨기 때문에, 그 규칙대로 있는 파일들만 찾으면 되는 것입니다.
-
-**systemd-boot**는 이런 자동 탐색을 아예 안 합니다. 관리자가 설정 파일에 "Windows는 이 파티션에 있고, 이 loader를 써라"고 직접 적어줘야 합니다. 그래서 Dual Boot 지원이 약한 것입니다.
-
-GRUB이 압도적으로 많이 쓰이는 이유는 이런 **범용성** 때문입니다. 디스크를 스캔해서 OS를 알아서 찾아주고, 커널 부팅 파라미터를 세밀하게 조정할 수 있고, 복구 모드 진입도 GRUB 메뉴에서 직접 가능합니다.
-
-서버 운영에서 커널 파라미터 튜닝은 특히 중요합니다. 예를 들어 K8s 노드에서는 `net.bridge.bridge-nf-call-iptables=1` 같은 커널 파라미터를 켜야 CNI 네트워크가 정상 동작합니다. GRUB은 부팅 시 이 파라미터를 커널에 전달하는 역할도 합니다(`/etc/default/grub`의 `GRUB_CMDLINE_LINUX` 항목).
-
-#### 이 부팅 지식이 K8s 운영에서 왜 중요한가
-
-"부팅 과정까지 알아야 하나?" 싶을 수 있습니다. 하지만 서버를 운영하다 보면 이런 상황이 생깁니다.
-
-- K8s 워커 노드가 갑자기 응답을 멈춤 → **커널 패닉**인지, **systemd 데몬 장애**인지, **kubelet 프로세스 죽음**인지 구분해야 합니다.
-- CNI 플러그인이 동작하지 않음 → 원인이 **커널 파라미터**(net.bridge 설정)인지, CNI 바이너리(Flannel/Calico)인지 진단해야 합니다.
-- 보안 업데이트 후 재부팅했는데 안 켜짐 → **GRUB 설정이 깨졌는지**, 새 커널에 문제가 있는지 확인해야 합니다.
-
-이 모든 상황에서 "전원 → BIOS → GRUB → 커널 → systemd → kubelet → containerd"라는 체인을 머릿속에 그릴 수 있어야 어느 층에서 문제가 생겼는지 빠르게 좁혀갈 수 있습니다.
-
-```mermaid
-flowchart LR
- A[전원] --> B[BIOS/UEFI] --> C[GRUB] --> D[Linux 커널] --> E[systemd
PID 1] --> F[kubelet
K8s 워커 에이전트] --> G[containerd
컨테이너 런타임] --> H[Pod 안의
Java/Vault/Postgres]
-```
-
-위 체인에서 왼쪽으로 갈수록 "사무실 건물의 전기"에 가깝고, 오른쪽으로 갈수록 "사무실 안의 사원"에 가깝습니다. 장애 진단은 항상 "어느 층에서 끊겼는가"를 찾는 것입니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- BIOS와 UEFI의 차이는 무엇인가요? 왜 최신 컴퓨터는 UEFI를 쓰는 걸까요? (힌트: 2TB 이상 디스크 지원, 보안 부팅)
-- Dual Boot 환경에서 Ubuntu를 먼저 설치하고 Windows를 나중에 깔면 GRUB이 깨졌다는 말을 자주 듣습니다. 왜 그런 걸까요? (힌트: Windows가 EFI Partition의 부트로더를 자기 것으로 덮어쓰기)
-- `systemd`가 PID 1인 이유와, 컨테이너 안에서 PID 1이 `sh`(셸)이면 왜 시그널 처리에 문제가 생기는지 조사해보세요. (Part I 5-2의 `exec` 사용 이유와 연결됩니다)
-- 커널 파라미터 `net.bridge.bridge-nf-call-iptables=1`이 왜 K8s CNI에 필수인지 공식 문서에서 찾아보세요.
-- K8s 노드가 재부팅될 때, 그 노드 위에 있던 Pod들은 어떻게 되나요? 다른 노드로 옮겨지나요, 아니면 그냥 사라지나요?
-
-## 24. Spring Security, OAuth2, Keycloak 심층 해부
-
-### 24-1. Spring Security 무용론? — 클린 아키텍처와의 조화
-
-#### 일반적인 Spring Security 로그인은 어떻게 동작하는가
-
-Spring Security를 처음 배우면 이런 흐름을 따릅니다.
-
-```mermaid
-sequenceDiagram
- participant User as 사용자 브라우저
- participant Filter as Spring Security Filter Chain
(웹 앞단, 인프라 계층)
- participant DAO as DaoAuthenticationProvider
- participant UDS as UserDetailsService
- participant DB as 데이터베이스
-
- User->>Filter: 로그인 요청 (이메일 + 비밀번호)
- Filter->>DAO: 인증 시도
- DAO->>UDS: "이 이메일의 사용자 정보 가져와"
- UDS->>DB: SELECT * FROM users WHERE email = ?
- DB-->>UDS: 사용자 정보 (해시된 비밀번호 포함)
- UDS-->>DAO: UserDetails 객체 반환
- DAO->>DAO: 비밀번호 비교 (BCrypt)
- DAO-->>Filter: 인증 성공 → Authentication 객체 생성
- Filter-->>User: 로그인 성공, 세션 또는 토큰 발급
-```
-
-이 흐름에서 중요한 것은 **Security Filter 안에서 DB 조회가 일어난다는 점**입니다. `UserDetailsService`가 DB에서 사용자를 찾고, `DaoAuthenticationProvider`가 비밀번호를 검증합니다. 이 모든 것이 웹 필터 체인, 즉 **인프라 계층** 안에서 발생합니다.
-
-작은 프로젝트에서는 이게 간편합니다. Spring이 다 해주니까요. 하지만 이 구조에는 **아키텍처적 문제**가 있습니다.
-
-#### 왜 우리 프로젝트는 이 방식을 안 쓰는가 — 클린 아키텍처 위반
-
-클린 아키텍처(Clean Architecture)의 핵심 규칙은 하나입니다.
-
-> **의존성은 무조건 바깥에서 안쪽으로만 향해야 한다.**
-
-이것을 원으로 그리면 이렇습니다.
-
-```mermaid
-flowchart TB
- subgraph 가장_바깥["가장 바깥: 인프라/프레젠테이션"]
- direction TB
- A["Spring Security Filter
Spring MVC Controller
JPA Repository
DB 드라이버"]
- end
-
- subgraph 중간["중간: Application / UseCase"]
- direction TB
- B["로그인 UseCase
회원가입 UseCase
토큰 발급 UseCase"]
- end
-
- subgraph 가장_안쪽["가장 안쪽: Domain"]
- direction TB
- C["User 엔티티
비즈니스 규칙
순수 Java 코드"]
- end
-
- A -->|"의존 가능 ✅"| B
- B -->|"의존 가능 ✅"| C
- C -.-x|"의존 불가 ❌
Domain은 Security를
몰라야 함"| A
-```
-
-**바깥 원(Security Filter, Controller, DB)**은 안쪽 원(UseCase, Domain)을 호출할 수 있습니다. 하지만 안쪽 원은 바깥 원의 존재를 **전혀 몰라야** 합니다.
-
-그런데 기본 Spring Security 흐름에서는 **Filter(바깥 원) 안에서 DB 조회(비즈니스 로직)가 직접 실행**됩니다. 이것은 바깥 원이 안쪽 원을 관통해서 직접 DB를 건드리는 것이라, 클린 아키텍처 원칙에 **위배**됩니다.
-
-Spring 공식 문서도 이 점을 인정합니다. 공식 Web Integration 문서에서는 "웹 계층은 여러 계층 중 하나일 뿐이며, 서비스 계층에 정의된 서비스 객체에 비즈니스 관련 사용 사례를 처리하도록 위임하라"고 권고합니다.
-
-#### 우리 프로젝트는 어떻게 했는가
-
-이 프로젝트의 `auth-server`에서는 Security의 역할을 **문지기로 축소**했습니다.
-
-```mermaid
-flowchart LR
- subgraph Security_역할["Spring Security가 하는 일 (문지기)"]
- direction TB
- S1["CORS 설정
다른 출처의 요청 차단/허용"]
- S2["JWT 서명 검증
토큰이 위조되었는지 확인"]
- S3["RBAC 권한 체크
일반 유저인지, 관리자인지
API 접근 허용/차단"]
- end
-
- subgraph Service_역할["비즈니스 로직이 하는 일 (UseCase)"]
- direction TB
- U1["로그인 검증
DB에서 사용자 조회"]
- U2["회원 가입 처리
DB에 사용자 저장"]
- U3["토큰 발급
Vault Transit으로 서명"]
- end
-
- Security_역할 -->|"Argument Resolver로
토큰 정보만 넘겨줌"| Service_역할
-```
-
-구체적으로:
-
-| 역할 | 일반적인 Spring Security | 이 프로젝트 |
-|---|---|---|
-| 비밀번호 검증 | Filter 안의 `DaoAuthenticationProvider`가 직접 수행 | UseCase(Service 계층)에서 수행 |
-| DB 조회 | `UserDetailsService`가 Filter 안에서 DB 접근 | Controller가 Argument Resolver로 토큰 정보만 파싱한 뒤, **Service 계층에서** DB 접근 |
-| 비밀번호 암호화 | Security의 `BCryptPasswordEncoder` 직접 사용 | 도메인이 "비밀번호 검증해줘"라고 **포트(인터페이스)**를 통해 요청 → 바깥의 Security가 BCrypt로 처리 |
-| OAuth2 로그인 | Security의 OAuth2LoginFilter가 session에 유저 정보를 밀어넣음 | 토큰 교환까지만 Security가 처리 → 이후 Argument Resolver → Service 계층에서 DB 조회 및 가입 처리 |
-
-이렇게 하면 **도메인 코드는 Spring Security의 존재를 전혀 모릅니다.** 내일 갑자기 Security를 걷어내고 다른 보안 프레임워크로 바꿔도, 도메인과 UseCase 코드는 수정할 필요가 없습니다.
-
-#### ArchUnit: 이 규칙을 어떻게 강제하는가
-
-사람이 코드를 짜다 보면 실수로 규칙을 어길 수 있습니다. "급하니까 Domain에서 직접 Security 클래스를 import하자"라고 하는 순간 아키텍처가 무너집니다.
-
-이 프로젝트에서는 **ArchUnit**이라는 라이브러리로 이 규칙을 **테스트 코드로 강제**합니다.
-
-ArchUnit은 JUnit 테스트처럼 동작합니다. 개발자가 코드를 커밋하고 CI가 돌 때, ArchUnit 테스트가 함께 실행됩니다. 만약 `domain` 패키지의 클래스가 `infrastructure` 패키지의 클래스를 import하고 있으면, **테스트가 실패**합니다.
-
-즉, ArchUnit은 런타임에 동작하는 것이 아니라 **테스트 시점(CI 빌드 시점)**에 동작합니다. 실제 서비스가 돌고 있을 때 성능에 영향을 주는 것이 아니라, 코드를 올릴 때 "이 코드는 아키텍처 규칙을 어겼으므로 빌드 실패"라고 알려주는 **정적 분석 파수꾼**입니다.
-
-이 프로젝트의 ConfigMap인 `apps/auth-server/overlays/dev/configmap.yaml`을 보면 Security가 남긴 흔적을 확인할 수 있습니다.
-
-```yaml
-# Vault Transit으로 JWT를 다룬다는 설정 — Security는 이 주소를 알고 서명을 요청할 뿐
-APP_SECURITY_JWT_VAULT_ENABLED: "true"
-APP_SECURITY_JWT_VAULT_ADDRESS: http://vault.vault.svc.cluster.local:8200
-APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME: project-auth-jwt
-
-# Keycloak과의 OIDC 연동 — Security는 토큰 교환까지만 담당
-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_JWT_VAULT_ENABLED: "true"`는 "JWT 서명을 로컬에서 하지 않고 Vault Transit에 위임하겠다"는 뜻입니다. Security가 서명의 **실행자**가 아니라 서명을 **중계하는 문지기** 역할만 한다는 것을 설정값으로도 확인할 수 있습니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- ArchUnit이 검사하는 시점은 언제인가? 런타임인가, CI 빌드 시점인가? (답: 테스트 시점, 즉 JUnit이 돌 때)
-- Keycloak이 완전히 다운되면 기존에 발급된 JWT 토큰은 여전히 유효한가? (힌트: JWT는 자기 완결적 — 서명 검증에 외부 호출이 필요 없다면 유효하다)
-- `BcryptPasswordEncoder`를 도메인 코드에서 직접 쓰지 않고 포트(인터페이스)로 뺀 이유는 무엇인가? 포트를 안 쓰고 직접 BCrypt를 호출하면 어떤 문제가 생기는가?
-- 이 프로젝트에서 `APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP: "false"`로 설정한 이유는 무엇인가? (힌트: 키 생성도 Vault Transit에 맡겼기 때문)
-- MVC 패턴과 클린 아키텍처의 가장 큰 차이는 무엇인가? MVC에서 Controller가 직접 DB를 호출하는 것이 왜 큰 프로젝트에서는 문제가 되는가?
-
-### 24-2. 현업의 OAuth2: Keycloak과 OIDC 내부 동작
-
-#### OAuth2의 핵심 아이디어: "비밀번호를 우리가 받지 않는다"
-
-사용자가 구글 로그인을 누르는 상황을 생각해보세요. 우리 서비스가 사용자의 구글 비밀번호를 직접 받아서 구글에 대신 로그인해주는 것은 **매우 위험**합니다. 사용자 입장에서도 "내 구글 비밀번호를 니네 서비스에 왜 줘야 하지?" 싶습니다.
-
-OAuth2는 이 문제를 해결합니다. **비밀번호를 우리한테 주는 게 아니라, 구글 로그인 페이지에서 직접 로그인하고, 구글이 "이 사람은 진짜야"라는 증거(토큰)만 우리한테 건네주는 구조**입니다.
-
-이 흐름을 **인가 코드 부여 방식(Authorization Code Grant)**이라고 부릅니다.
-
-```mermaid
-sequenceDiagram
- participant User as 사용자 브라우저
- participant Spring as auth-server
(Spring Security)
- participant Google as 구글 로그인 서버
-
- User->>Spring: "구글로 로그인할래요"
- Spring->>User: 구글 로그인 페이지로 리다이렉트
(우리 서버가 비밀번호를 받지 않음!)
- User->>Google: 구글 로그인 페이지에서 직접 로그인
- Google->>User: 로그인 성공! 인가 코드를 들고
우리 서버의 콜백 URL로 돌아가거라
- User->>Spring: 인가 코드(임시 교환권)를 들고 콜백 URL로 돌아옴
-
- Note over Spring,Google: 여기서부터는 서버 대 서버 통신 (사용자 브라우저를 거치지 않음)
- Spring->>Google: "이 인가 코드, 진짜 너네가 준 거 맞지?
Access Token으로 바꿔줘"
- Google-->>Spring: Access Token + ID Token 반환
- Spring->>Spring: ID Token에서 사용자 이메일, 이름 등 추출
- Spring-->>User: 우리 서비스의 JWT 토큰 발급
-```
-
-핵심 포인트는 **인가 코드(Authorization Code)**입니다. 이것은 "일회용 교환권"같은 것입니다.
-
-1. 구글이 사용자에게 인가 코드를 줍니다.
-2. 사용자가 그 코드를 우리 서버에 전달합니다.
-3. 우리 서버가 구글에 "이 코드 진짜야? 토큰으로 바꿔줘"라고 서버 대 서버 통신을 합니다.
-
-왜 처음부터 토큰을 안 주고 코드를 먼저 줄까요? 코드는 **사용자의 브라우저를 경유**합니다. 브라우저는 해킹당할 수 있습니다. 그래서 코드는 일회용이고 짧은 시간만 유효합니다. 실제 토큰 교환은 **서버 대 서버 통신**으로 이루어지니 브라우저가 탈취당해도 토큰은 안전합니다.
-
-#### Keycloak이 없으면 어떤 끔찍한 일이 생기는가
-
-위 흐름을 구글, 카카오, 네이버, GitHub 각각에 대해 구현한다고 생각해보세요.
-
-```mermaid
-flowchart TB
- subgraph 문제["Keycloak 없이 직접 구현한 경우"]
- direction TB
- APP[auth-server]
- APP --> G[구글 API
응답 형식: A]
- APP --> K[카카오 API
응답 형식: B]
- APP --> N[네이버 API
응답 형식: C]
- APP --> GH[GitHub API
응답 형식: D]
- end
-```
-
-각 소셜 로그인 제공자마다:
-- 콜백 URL 형식이 다릅니다.
-- 사용자 정보 응답 형식(JSON 구조)이 다릅니다. 구글은 `email` 필드, 카카오는 `kakao_account.email` 필드입니다.
-- 토큰 갱신 방식이 다릅니다.
-
-제공자가 4개면 4가지 파싱 코드를 짜야 합니다. 10개가 되면 10가지입니다. 코드가 폭발합니다.
-
-#### Keycloak은 이 문제를 어떻게 해결하는가
-
-**Keycloak**은 **사설 통합 인증 센터(Identity Provider Broker)**입니다. 쉽게 말하면 "외부 로그인 제공자와의 복잡한 대화를 대신 해주고, 우리한테는 항상 같은 형식으로 결과를 알려주는 중간 통역사"입니다.
-
-```mermaid
-flowchart TB
- subgraph 해결["Keycloak을 둔 경우"]
- direction TB
- APP2[auth-server
Keycloak 하나만 상대하면 됨]
- KC[Keycloak
통합 인증 센터]
- APP2 -->|"항상 OIDC 표준 형식
하나의 콜백, 하나의 응답 규격"| KC
- KC --> G2[구글]
- KC --> K2[카카오]
- KC --> N2[네이버]
- KC --> GH2[GitHub]
- end
-```
-
-이 구조에서 `auth-server`의 Spring Security가 하는 일은 극적으로 줄어듭니다.
-
-1. 사용자가 "구글로 로그인" 클릭 → Security가 사용자를 **Keycloak**으로 리다이렉트 (구글로 직접이 아님!)
-2. Keycloak이 구글과 알아서 통신 → 구글의 응답을 **OIDC 표준 형식(JSON)**으로 변환
-3. Keycloak이 auth-server에 콜백 → Security가 OIDC 표준 토큰을 수신
-4. 여기까지가 Security의 역할 끝 → 이후 Argument Resolver가 토큰에서 사용자 정보 추출 → Service 계층에서 DB 조회
-
-**OIDC(OpenID Connect)**란 OAuth2 위에 "사용자 신원 확인" 규격을 얹은 표준입니다. OAuth2만으로는 "이 사람이 어떤 리소스에 접근 가능하다"만 알 수 있지만, OIDC를 쓰면 "이 사람의 이메일은 뭐고 이름은 뭐다"까지 표준화된 형식(ID Token)으로 받을 수 있습니다.
-
-#### 이 구조가 실제 코드에 어떻게 반영되어 있는가
-
-이 저장소의 실제 파일들을 보면 위 아키텍처가 선언으로 드러납니다.
-
-**1. Keycloak이 auth-server의 인증 상대방이라는 것을 알려주는 설정**
-
-`apps/auth-server/overlays/dev/configmap.yaml`:
-```yaml
-# auth-server가 Keycloak을 통해 OIDC 인증을 받겠다는 선언
-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
-
-# 구글/GitHub 로그인도 Keycloak을 경유한다는 뜻
-# registration ID가 "keycloak-google", "keycloak-github"다
-APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google
-APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID: keycloak-github
-```
-
-`APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google`이 중요합니다. 이름이 **"keycloak-google"**인 것은 **구글 로그인이지만 Keycloak을 거쳐서 간다**는 뜻입니다. auth-server는 구글 API를 직접 호출하지 않고, Keycloak에게 "구글 쪽으로 브로커링 해줘"라고 힌트(`idp_hint: google`)만 보냅니다.
-
-**2. Keycloak의 client 설정을 코드로 맞추는 Job**
-
-`infra/platform/base/keycloak-client-sync-job.yaml`을 보면, 이 Job이 Keycloak Admin API를 호출해서:
-- `project-auth-server` 클라이언트의 `secret`을 업데이트하고
-- `baseUrl`을 맞추고
-- `redirectUris`를 코드 기반으로 등록합니다
-
-```yaml
-# keycloak-client-sync-job.yaml 중 핵심 부분
-/opt/keycloak/bin/kcadm.sh update "clients/${CLIENT_UUID}" \
- -r project-auth \
- -s "secret=$KEYCLOAK_CLIENT_SECRET" \
- -s "baseUrl=$AUTH_SERVER_BASE_URL" \
- -s 'redirectUris=[".../login/oauth2/code/keycloak-google",".../login/oauth2/code/keycloak-github"]'
-```
-
-사람이 Keycloak 관리 콘솔에서 마우스로 클릭하는 것이 아니라, **K8s Job이 코드로 자동 설정**합니다. 이것이 GitOps 관점에서 중요합니다. 설정이 코드에 있으니 변경 추적이 되고, 재현 가능합니다.
-
-**3. OAuth2 client secret은 Vault에서 온다**
-
-Keycloak과 auth-server가 통신하려면 **client secret**(일종의 비밀 비밀번호)이 필요합니다. 이 값은 `deployment.vault-patch.yaml`에서 Vault를 통해 주입됩니다.
-
-```yaml
-# deployment.vault-patch.yaml의 일부
-{{ with secret "kv/data/dev/platform/keycloak/client-auth-server" }}
-export APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET }}
-{{ end }}
-```
-
-즉, client secret은 Git에 없고, Vault에만 있고, Pod가 뜰 때 파일로 주입됩니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- OAuth2의 "인가 코드"를 사용자 브라우저가 중간에 가로챌 수 있는가? 가로채도 왜 안전한가? (힌트: 코드를 토큰으로 교환할 때 client secret이 필요하고, 이것은 서버만 알고 있다)
-- OIDC의 ID Token과 OAuth2의 Access Token은 어떤 차이가 있는가? 둘 다 JWT인가?
-- Keycloak이 완전히 다운되면 어떤 일이 생기는가? 이미 로그인한 사용자는 괜찮은가? 새로 로그인하려는 사용자는?
-- `redirectUris`가 잘못 설정되면 어떤 보안 문제가 생기는가? (힌트: Open Redirect 공격)
-- `keycloak-client-sync-job.yaml`에서 `until ... do sleep 5; done` 루프가 있는 이유는 무엇인가? (힌트: Keycloak이 아직 기동 중일 수 있다)
-
-### 24-3. DB 버전 관리(Flyway) 분리 배포 작전
-
-#### 왜 DB 스키마 변경은 특별하게 다뤄야 하는가
-
-일반적인 코드 변경(버그 수정, 기능 추가)은 서버를 새로 배포하면 반영됩니다. 이전 코드가 새 코드로 바뀌는 것뿐이니, 잘못되면 이전 버전으로 롤백하면 됩니다.
-
-하지만 **DB 스키마 변경**은 다릅니다. 새 컬럼을 추가하거나, 테이블 이름을 바꾸거나, 인덱스를 거는 것은 **데이터 구조 자체를 바꾸는 것**입니다. 한번 바꾸면 되돌리기가 훨씬 어렵습니다. 새 컬럼에 데이터가 이미 쌓였다면 단순 롤백으로는 원상복구가 안 됩니다.
-
-**Flyway**는 DB 스키마 변경을 **버전 관리**하는 도구입니다. 코드 변경을 Git으로 관리하듯이, DB 변경을 `V1__create_user.sql`, `V2__add_email_column.sql` 같은 번호 붙은 마이그레이션 파일로 관리합니다.
-
-#### 기존 방식의 문제: Spring Boot 기동 시 Flyway 자동 실행
-
-처음에는 Flyway를 Spring Boot 앱 안에 넣었습니다. 서버가 기동될 때 Flyway가 자동으로 실행되어 DB 스키마를 최신으로 맞추는 방식입니다.
-
-작은 서비스에서는 이것도 괜찮습니다. 하지만 **트래픽이 몰려서 서버를 10대로 늘려야 하는 상황**을 생각해보세요.
-
-```mermaid
-flowchart TB
- subgraph 문제_상황["서버 10대가 동시에 기동되는 상황"]
- direction TB
- S1[auth-server Pod 1] -->|"Flyway: ALTER TABLE users..."| DB[(PostgreSQL)]
- S2[auth-server Pod 2] -->|"Flyway: ALTER TABLE users..."| DB
- S3[auth-server Pod 3] -->|"Flyway: ALTER TABLE users..."| DB
- S4["... Pod 4~10도 동시에"] -->|"Flyway: ALTER TABLE users..."| DB
- end
-
- DB -->|"💥 Lock 경합!
누가 먼저야?
DDL Lock 충돌!"| DEAD[배포 데드락
일부 Pod는 마이그레이션 성공
일부 Pod는 Lock 대기 중 타임아웃]
-```
-
-Flyway는 내부적으로 DB Lock을 사용해서 중복 실행을 방지하려고 합니다. 하지만 10대의 서버가 **동시에** 일어나면서 모두 "나 먼저 마이그레이션 할게!"라고 달려들면, Lock 경합이 발생합니다. 일부는 성공하고 일부는 타임아웃으로 실패합니다. 실패한 Pod는 기동에 실패합니다.
-
-#### 이 프로젝트의 해결책: 완전 분리된 K8s Job
-
-이 프로젝트에서는 Flyway를 **앱 서버에서 완전히 떼어내서 별도의 K8s Job으로 분리**했습니다.
-
-```mermaid
-flowchart TD
- subgraph 해결_구조["이 프로젝트의 배포 흐름"]
- direction TB
-
- subgraph PreSync["1단계: PreSync (앱 배포 전)"]
- JOB[auth-db-migration Job
Flyway 실행
딱 1개만 실행됨] -->|"스키마 변경 완료"| DB2[(PostgreSQL)]
- end
-
- subgraph MainSync["2단계: Main Sync (스키마 준비 완료 후)"]
- S1b[auth-server Pod 1
Flyway 안 함] --> DB2
- S2b[auth-server Pod 2
Flyway 안 함] --> DB2
- S3b[auth-server Pod 3
Flyway 안 함] --> DB2
- end
-
- PreSync -->|"Job 성공해야
다음 단계 진행"| MainSync
- end
-```
-
-이 구조의 핵심은 Argo CD의 **PreSync Hook**입니다.
-
-`apps/auth-server/base/db-migration-job.yaml`을 보면:
-
-```yaml
-annotations:
- argocd.argoproj.io/hook: PreSync # 일반 리소스보다 먼저 실행
- argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded # 성공하면 정리
- argocd.argoproj.io/sync-wave: "-1" # 가장 먼저
-```
-
-그리고:
-
-```yaml
-spec:
- backoffLimit: 1 # 실패하면 1번만 재시도
- template:
- spec:
- restartPolicy: Never # 완료 후 재시작하지 않음
-```
-
-이 어노테이션과 설정을 합치면 이런 동작이 됩니다.
-
-1. Argo CD가 sync를 시작하면 **PreSync 리소스를 먼저** 실행합니다.
-2. migration Job이 **딱 1개** 뜹니다. (Deployment가 아니라 Job이니까 복제본 없음)
-3. Flyway가 DB 스키마를 변경합니다.
-4. Job이 성공하면(exit code 0) Argo CD가 다음 단계(Deployment 등)를 진행합니다.
-5. Job이 실패하면 **배포 전체가 멈춥니다.** 스키마가 반쪽만 적용된 상태로 앱이 뜨는 것을 방지합니다.
-
-auth-server의 ConfigMap에서 `APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP: "false"`인 이유도 이것 때문입니다. 앱 서버 자체는 Flyway를 실행하지 않습니다. 마이그레이션은 오직 Job만 합니다.
-
-#### 분리에 따른 의존성 문제와 해결
-
-PPT에서 언급했듯이, 처음에 Flyway를 분리하려 했을 때 문제가 있었습니다. Flyway가 Spring Boot 앱 안에 있다 보니, 관련 설정 Bean들이 앱 기동에 필요했습니다. 단순히 Flyway 실행만 떼어내는 것으로는 의존성이 끊기지 않았습니다.
-
-최종 해결책은 **Flyway를 아예 별도의 라이브러리 모듈로 분리**하고, Docker 이미지 빌드 시 `migration.jar`라는 독립적인 JAR 파일을 만든 것입니다.
-
-`db-migration-job.yaml`에서 이것이 드러납니다:
-
-```yaml
-containers:
- - name: auth-db-migration
- image: ghcr.io/donghyeonka/project-auth-server # 같은 이미지지만
- command:
- - java
- - -jar
- - /app/migration.jar # migration.jar를 따로 실행
- env:
- - name: SPRING_MAIN_WEB_APPLICATION_TYPE
- value: none # 웹 서버를 띄우지 않음
- - name: APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP
- value: "true" # 이 Job에서만 마이그레이션 실행
-```
-
-`SPRING_MAIN_WEB_APPLICATION_TYPE: none`은 "Spring의 웹 서버(Tomcat 등)를 켜지 마라"는 뜻입니다. migration에는 HTTP 서버가 필요 없으니까요. 이렇게 하면 순수하게 Flyway만 돌리고 종료됩니다.
-
-#### 동적 DB 계정: migration Job은 왜 고정 비밀번호를 안 쓰는가
-
-dev overlay의 `db-migration-job.vault-patch.yaml`을 보면, migration Job은 고정 비밀번호가 아니라 **Vault가 그 순간에 만들어주는 임시 DB 계정**을 사용합니다.
-
-```yaml
-vault.hashicorp.com/agent-inject-secret-migration-env: database/creds/auth-db-migration-dev
-```
-
-경로가 `kv/...`(고정 값 저장소)가 아니라 `database/creds/...`(동적 발급 엔진)인 것이 핵심입니다.
-
-이 경로로 요청하면 Vault가 **그 순간에** PostgreSQL에 접속해서 임시 사용자를 만들고, 짧은 TTL(수명)이 지나면 자동으로 삭제합니다.
-
-왜 이렇게 할까요?
-
-- migration은 **고권한 작업**입니다. 테이블 생성, 컬럼 변경 같은 DDL을 수행합니다.
-- 이런 강력한 권한을 가진 계정이 **영구적으로 존재**하면, 유출 시 피해가 큽니다.
-- 동적 계정은 Job 실행 후 자동 만료되니, 유출되어봐야 이미 삭제된 계정입니다.
-
-그리고 `agent-pre-populate-only: "true"` 어노테이션의 의미도 여기서 명확해집니다. Deployment에 붙는 Vault Agent는 sidecar로 계속 살아있으면서 secret을 갱신할 수 있지만, Job은 **짧게 실행되고 끝나는 일회성 작업**이니 사이드카가 계속 떠 있을 필요가 없습니다. 한 번 secret 파일을 만들어두고 바로 사라지는 것입니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- Flyway 마이그레이션 Job이 중간에 실패해서 테이블은 만들었는데 인덱스는 못 만든 상태라면, 다음 Job 실행 시 어떻게 되는가? Flyway는 이것을 어떻게 감지하는가? (힌트: `flyway_schema_history` 테이블)
-- DDL(테이블 구조 변경)과 DML(데이터 변경) 마이그레이션의 위험도 차이는 무엇인가? 왜 DDL 마이그레이션이 더 위험한가?
-- `backoffLimit: 1`의 의미는 무엇인가? 왜 migration Job은 횟수를 제한하는가?
-- 만약 Flyway가 성공했는데 앱 배포가 실패해서 롤백한다면, DB 스키마는 이전으로 돌아가는가? (답: 아니다. DB 스키마 롤백은 별도 마이그레이션이 필요하다)
-- `database/creds/auth-db-migration-dev`와 `kv/data/dev/platform/postgres/auth-server`의 차이는 무엇인가? 왜 runtime 앱은 KV를 쓰고 migration Job은 database engine을 쓰는가?
-
-## 25. Kubernetes(K3s) 심층 해부와 네트워크 구조
-
-### 25-1. K8s vs K3s: 왜 가볍고 왜 선택했는가
-
-#### Kubernetes의 기본 구조: 두 종류의 노드
-
-Kubernetes를 쉽게 비유하면, **회사의 본사(Control Plane)**와 **공장(Worker Node)**의 관계입니다.
-
-본사는 "무엇을 어디에 얼마나 만들지" 결정하고, 공장은 실제로 물건(컨테이너)을 생산합니다. 본사가 망하면 새 지시가 안 내려가지만, 이미 돌고 있는 공장은 당장은 계속 돌아갑니다.
-
-```mermaid
-flowchart TB
- subgraph CP["Control Plane (본사)"]
- direction TB
- API["API Server
모든 요청의 창구
kubectl 명령이 여기로 들어옴"]
- ETCD["etcd / SQLite
클러스터의 모든 상태를
저장하는 데이터베이스"]
- SCHED["Scheduler
새 Pod를 어느 노드에
배치할지 결정"]
- CM["Controller Manager
선언된 상태와 현재 상태의
차이를 감지하고 조정"]
-
- API <--> ETCD
- SCHED --> API
- CM --> API
- end
-
- subgraph W1["Worker Node 1 (공장)"]
- direction TB
- KL1["kubelet
API Server의 지시를 받아
컨테이너를 생성/삭제"]
- KP1["kube-proxy
네트워크 규칙을 관리
(Service → Pod 라우팅)"]
- CR1["containerd
실제 컨테이너를 실행하는
런타임 엔진"]
-
- KL1 --> CR1
- end
-
- subgraph W2["Worker Node 2 (공장)"]
- direction TB
- KL2["kubelet"]
- KP2["kube-proxy"]
- CR2["containerd"]
- KL2 --> CR2
- end
-
- API -->|"Watch 스트림으로
이벤트 전달"| KL1
- API -->|"Watch 스트림으로
이벤트 전달"| KL2
-```
-
-각 컴포넌트가 하는 일을 구체적으로 설명합니다.
-
-| 컴포넌트 | 어디에 있나 | 무슨 일을 하나 | 없으면 어떻게 되나 |
-|---|---|---|---|
-| **API Server** | Control Plane | 모든 통신의 중심. `kubectl` 명령, kubelet 보고, Scheduler 요청이 전부 여기를 거침 | 클러스터 전체가 통신 불가. 신규 배포, 스케일링, 조회 모두 불가능 |
-| **etcd** (K8s) / **SQLite** (K3s) | Control Plane | "이 클러스터에 무엇이 있고, 어떤 상태인지"를 영구 저장. Deployment 몇 개, Pod 몇 개, Service 설정 등 | 상태 정보가 날아감. 클러스터를 처음부터 다시 만들어야 함 |
-| **Scheduler** | Control Plane | 새 Pod가 생겼을 때 "어느 Worker에 배치할지" 결정 (CPU, 메모리, affinity 조건 등 고려) | Pod가 Pending 상태에서 영원히 멈춤 |
-| **Controller Manager** | Control Plane | "선언된 상태(Deployment에 replica: 3)"와 "현재 상태(Pod 2개 살아있음)"의 차이를 발견하고 Pod를 1개 더 만듦 | Pod가 죽어도 자동 복구 안 됨, 스케일링 안 됨 |
-| **kubelet** | 각 Worker | API Server에게 "제 노드에서 이 Pod를 실행하겠습니다"라는 지시를 받고, containerd에게 실제 실행을 시킴 | 해당 노드에서 컨테이너 생성, 삭제, 모니터링 불가 |
-| **kube-proxy** | 각 Worker | Service의 IP로 들어온 트래픽을 실제 Pod의 IP로 전달하는 네트워크 규칙을 관리 | Service를 통한 통신 불가 (Pod IP 직접 지정하면 가능) |
-| **containerd** | 각 Worker | 실제 컨테이너 이미지를 다운로드하고, 프로세스를 격리해서 실행 | 컨테이너 실행 자체가 불가 |
-
-#### kubelet은 API Server에게 어떻게 지시를 받는가 — Watch 메커니즘
-
-여기서 중요한 의문이 생깁니다. kubelet은 API Server에게 명령을 어떻게 받을까요?
-
-두 가지 방식이 가능합니다.
-
-1. **Polling(폴링)**: kubelet이 1초마다 API Server에 "나한테 새 일 있어?" 하고 물어보는 방식
-2. **Watch(감시)**: kubelet이 API Server에 "내 노드에 변경 생기면 바로 알려줘"라고 한 번 등록해놓고, 변경이 있을 때만 알림을 받는 방식
-
-Kubernetes는 **Watch 방식**을 씁니다. 왜냐하면 Worker Node가 100대, 1000대가 되면 모두가 1초마다 물어보는 건 API Server에게 엄청난 부하가 됩니다. Watch는 **변경이 있을 때만** 이벤트를 푸시하니까 훨씬 효율적입니다.
-
-```mermaid
-sequenceDiagram
- participant KL as kubelet (Worker Node)
- participant API as API Server (Control Plane)
-
- KL->>API: "내 노드에 관련된 변경사항을
Watch 스트림으로 구독합니다"
- Note over KL,API: HTTP Long-Poll 연결이 유지됨
- API-->>KL: (아무 일 없으면 조용)
-
- Note over API: 사용자가 kubectl apply로
새 Deployment 생성
- API->>API: Scheduler가 "Worker Node 1에 배치" 결정
- API-->>KL: "새 Pod를 실행하세요" 이벤트 푸시
- KL->>KL: containerd에게 컨테이너 생성 요청
- KL-->>API: "Pod 실행 중(Running)" 상태 보고
-```
-
-이 구조를 **선언적 상태 관리(Declarative State Management)**라고 합니다.
-
-사용자는 "auth-server를 3개 돌려라"라고 **원하는 상태를 선언**합니다. Controller Manager가 현재 상태와 원하는 상태를 지속적으로 비교합니다. Pod가 1개 죽으면 "2개인데 3개여야 하니까 1개 더 만들어야지"라고 판단하고, 이것을 **Reconciliation Loop(조정 루프)**라고 부릅니다. 이 루프는 **끊임없이** 돕니다.
-
-#### K3s는 이 구조를 어떻게 경량화했는가
-
-K3s는 위의 Kubernetes와 동일한 개념이지만, 일부를 **다이어트**했습니다.
-
-```mermaid
-flowchart LR
- subgraph K8S["K8s (풀 사이즈)"]
- direction TB
- E1["etcd
분산 합의 알고리즘 (Raft)
별도 클러스터 3~5대 필요
수백 MB 메모리"]
- C1["Cloud Controller Manager
AWS, GCP 연동 코드"]
- S1["Storage Driver
다양한 CSI 드라이버 포함"]
- end
-
- subgraph K3S["K3s (경량)"]
- direction TB
- E2["SQLite
단일 파일 DB
단일 바이너리 내장
수 MB 메모리"]
- C2["없음
클라우드 연동 코드 제거"]
- S2["Local Path Provisioner
기본 탑재, 간단한 로컬 볼륨"]
- end
-
- K8S -->|"이것이 100MB 바이너리 하나로
압축된 것이 K3s"| K3S
-```
-
-| 차이점 | K8s | K3s |
-|---|---|---|
-| 상태 저장소 | **etcd** — 분산 합의 알고리즘(Raft)을 자체 구현한 별도 프로세스. HA를 위해 최소 3대 클러스터 필요 | **SQLite** — 파일 하나(`/var/lib/rancher/k3s/server/db/state.db`)로 상태를 저장. 프로세스 추가 없음 |
-| 바이너리 크기 | 여러 바이너리 합계 수백 MB | **단일 바이너리 약 100MB** (API Server, Scheduler, Controller Manager, kubelet, kube-proxy 전부 포함) |
-| 네트워크 플러그인(CNI) | 별도 설치 필요 | **Flannel 기본 탑재** (설치 없이 바로 Pod 간 통신 가능) |
-| 인그레스 컨트롤러 | 별도 설치 (NGINX, Traefik 등) | **Traefik 기본 탑재** |
-| 자원 요구량 | Control Plane 최소 2GB RAM | **최소 512MB RAM** |
-
-PPT에서 "K3s가 etcd 대신 SQLite를 쓰기 때문에 가볍다"고 설명했습니다. 좀 더 깊이 들어가면, etcd는 **여러 노드 간의 데이터 일관성을 보장**하기 위해 Raft 합의 알고리즘을 돌립니다. 3대의 etcd 노드가 "이 데이터를 저장할게"라고 합의하는 과정 자체가 CPU와 메모리를 많이 씁니다.
-
-K3s의 SQLite는 이런 분산 합의가 **없습니다**. 파일 하나에 기록할 뿐입니다. 대신 트레이드오프가 있습니다. **하나의 Control Plane이 죽으면 상태 저장소가 함께 날아갑니다.** 이것이 K3s가 "실험/개발 환경, IoT 엣지 디바이스"에 적합하고, "대규모 운영 인프라"에는 풀 K8s를 쓰는 이유입니다.
-
-(다만 K3s도 HA를 원하면 SQLite 대신 외부 DB(MySQL, PostgreSQL 등)를 상태 저장소로 쓸 수 있습니다.)
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- etcd의 Raft 합의 알고리즘은 왜 노드가 최소 3대 필요한가? 2대면 안 되는 이유는? (힌트: 과반수 투표)
-- K3s에서 SQLite를 쓰면 HA 구성이 불가능한가? 가능하다면 어떤 방식인가?
-- Reconciliation Loop는 얼마나 자주 도는가? 1초마다? 이벤트가 있을 때만?
-- 우리 프로젝트의 `vault-deployment.yaml`에 `strategy: Recreate`로 설정한 이유는 무엇인가? RollingUpdate와 무엇이 다른가? (힌트: 볼륨 동시 마운트 문제)
-- API Server가 다운되면 이미 돌고 있는 Pod들은 어떻게 되는가? 바로 죽는가?
-
-### 25-2. 복잡한 K8s 네트워크의 마법 (CNI와 DNS)
-
-#### "Service"란 정확히 무엇인가 — 추상적 설명을 넘어서
-
-Part I에서 Service를 "Pod 앞에 놓는 고정된 네트워크 진입점"이라고 설명했습니다. 하지만 이것만으로는 **Service가 별도의 프로세스인지, 가상의 설정인지, 물리적으로 어디에 존재하는지** 감이 안 옵니다.
-
-답부터 말하면: **Service는 독립적인 프로세스나 컨테이너가 아닙니다.** Service는 **kube-proxy가 각 노드의 iptables(또는 IPVS)에 기록해놓은 네트워크 규칙(라우팅 테이블)**입니다.
-
-이것을 실제 동작으로 풀어보겠습니다.
-
-```mermaid
-sequenceDiagram
- participant APP as auth-server Pod
(다른 Pod에서 vault를 호출)
- participant IPTABLES as iptables 규칙
(kube-proxy가 관리)
- participant POD as vault Pod
(실제 컨테이너)
-
- Note over APP: configmap에 적힌 주소:
vault.vault.svc.cluster.local:8200
- APP->>APP: DNS 조회: vault.vault.svc.cluster.local
→ CoreDNS가 10.43.x.x (Service의 ClusterIP) 반환
- APP->>IPTABLES: 10.43.x.x:8200으로 패킷 전송
- Note over IPTABLES: kube-proxy가 미리 심어놓은 규칙:
"10.43.x.x:8200 → 실제 Pod IP 10.42.y.y:8200"
- IPTABLES->>POD: 실제 Pod의 IP(10.42.y.y:8200)로 전달
- POD-->>APP: 응답 반환
-```
-
-이 그림을 단계별로 설명하면 이렇습니다.
-
-**1단계: Service 생성 시** — `vault-service.yaml`을 `kubectl apply`하면 API Server가 이것을 etcd(K3s는 SQLite)에 기록합니다. 이 순간 Kubernetes가 Service에 **ClusterIP**(예: 10.43.x.x)라는 가상 IP를 할당합니다. 이 IP는 **어떤 노드에도 실제로 바인딩되어 있지 않은 가상 주소**입니다.
-
-**2단계: kube-proxy가 규칙 설정** — 각 노드의 kube-proxy가 Watch 스트림으로 "새 Service가 생겼다"는 이벤트를 받습니다. kube-proxy는 자기 노드의 **iptables에 규칙을 추가**합니다. "10.43.x.x:8200으로 가는 패킷은 → 실제 Pod IP 10.42.y.y:8200으로 보내라."
-
-**3단계: Pod가 Service를 호출할 때** — auth-server Pod가 `vault.vault.svc.cluster.local:8200`으로 요청을 보내면, DNS가 이것을 ClusterIP(10.43.x.x)로 해석합니다. 패킷이 노드의 네트워크 스택을 통과할 때, **iptables 규칙에 의해** 실제 Pod IP로 변환됩니다.
-
-그래서 "Service가 Pod 앞에 놓여있다"는 비유적 표현이고, 실제로는 **각 노드의 iptables 규칙이 트래픽을 중계하는 것**입니다. Service라는 별도 프로세스가 떠서 트래픽을 통과시키는 것이 아닙니다.
-
-이 프로젝트의 `infra/vault/base/vault-service.yaml`을 보면:
-
-```yaml
-apiVersion: v1
-kind: Service
-metadata:
- name: vault # 이 이름이 DNS 이름의 일부가 됨
-spec:
- selector:
- app: vault # label이 "app: vault"인 Pod로 트래픽을 연결
- ports:
- - name: http
- port: 8200 # Service가 받는 포트
- targetPort: 8200 # Pod가 실제로 리스닝하는 포트
- type: ClusterIP # 클러스터 내부에서만 접근 가능
-```
-
-`selector: app: vault`가 핵심입니다. kube-proxy는 `app: vault` 라벨이 달린 **모든 Pod의 IP**를 수집해서 iptables 규칙에 등록합니다. Pod가 죽으면 규칙에서 제거하고, 새 Pod가 뜨면 규칙에 추가합니다. 이것이 "고정된 진입점"이라고 불리는 이유입니다. **Pod는 죽고 태어나면 IP가 바뀌지만, Service의 ClusterIP와 DNS 이름은 바뀌지 않습니다.**
-
-#### CNI: Pod에 IP를 어떻게 부여하는가
-
-각 Pod에는 고유한 IP 주소가 있습니다. 하지만 Pod는 가상의 컨테이너입니다. 물리 서버처럼 NIC(네트워크 카드)가 있는 것이 아닙니다. 그러면 IP는 어떻게 생기는 걸까요?
-
-이것을 담당하는 것이 **CNI(Container Network Interface)** 플러그인입니다.
-
-K3s는 기본으로 **Flannel**이라는 CNI를 탑재하고 있습니다. Flannel은 **VXLAN 오버레이 네트워크**를 만듭니다.
-
-쉽게 비유하면, 물리적으로 다른 건물(노드)에 있는 사무실(Pod)들을 마치 같은 내부 네트워크에 있는 것처럼 **가상 터널**로 연결하는 것입니다.
-
-```mermaid
-flowchart TB
- subgraph Node1["Worker Node 1 (물리 IP: 192.168.1.10)"]
- direction TB
- P1["auth-server Pod
Pod IP: 10.42.0.5"]
- P2["migration Job Pod
Pod IP: 10.42.0.6"]
- F1["Flannel
VXLAN 터널 엔드포인트"]
- end
-
- subgraph Node2["Worker Node 2 (물리 IP: 192.168.1.11)"]
- direction TB
- P3["vault Pod
Pod IP: 10.42.1.3"]
- P4["postgres Pod
Pod IP: 10.42.1.4"]
- F2["Flannel
VXLAN 터널 엔드포인트"]
- end
-
- F1 <-->|"VXLAN 터널
Pod 패킷을 캡슐화해서
물리 네트워크 위로 전달"| F2
- P1 -.-|"10.42.0.5 → 10.42.1.3
다른 노드지만
직접 통신 가능"| P3
-```
-
-auth-server Pod(10.42.0.5)가 vault Pod(10.42.1.3)로 패킷을 보내면:
-1. 패킷이 Flannel의 VXLAN 인터페이스에 도착합니다.
-2. Flannel이 이 패킷을 **UDP로 캡슐화**(원래 패킷을 외부 패킷 안에 넣음)합니다.
-3. 물리 네트워크(192.168.1.10 → 192.168.1.11)를 통해 상대 노드로 전달합니다.
-4. 상대 노드의 Flannel이 캡슐을 벗기고 vault Pod에게 전달합니다.
-
-이것이 **다른 물리 서버에 있는 Pod들이 마치 같은 네트워크에 있는 것처럼** 통신할 수 있는 원리입니다.
-
-#### CoreDNS: vault.vault.svc.cluster.local은 누가 해석하는가
-
-이 프로젝트에서는 `vault.vault.svc.cluster.local`이라는 DNS 이름이 여기저기 등장합니다.
-
-이 이름의 구조를 분해하면:
-
-| 부분 | 의미 |
-|---|---|
-| `vault` | Service의 이름 (`metadata.name: vault`) |
-| `vault` | Service가 속한 Namespace |
-| `svc` | "이것은 Service의 DNS다"라는 고정 접미사 |
-| `cluster.local` | 클러스터의 기본 도메인 |
-
-이 이름을 IP 주소로 변환하는 것은 **CoreDNS**라는 Pod입니다. CoreDNS는 K3s(그리고 K8s)가 기본으로 띄우는 **클러스터 내부 DNS 서버**입니다.
-
-```mermaid
-flowchart LR
- A["auth-server Pod가
vault.vault.svc.cluster.local
을 호출"] --> B["Pod의 /etc/resolv.conf에
CoreDNS IP가 적혀있음"]
- B --> C["CoreDNS Pod가
K8s API로부터
Service 목록을 Watch"]
- C --> D["vault Service의
ClusterIP: 10.43.x.x
를 응답"]
- D --> E["iptables가
10.43.x.x를
실제 Pod IP로 변환"]
-```
-
-모든 Pod 안에는 `/etc/resolv.conf` 파일이 자동으로 생성되고, 여기에 CoreDNS의 IP가 적혀 있습니다. 그래서 Pod 안에서 `vault.vault.svc.cluster.local`을 호출하면 자동으로 CoreDNS에게 물어보게 됩니다.
-
-`vault.hcl` 설정 파일에서 이 DNS 의존성이 직접 드러납니다:
-
-```hcl
-api_addr = "http://vault.vault.svc.cluster.local:8200"
-cluster_addr = "http://vault.vault.svc.cluster.local:8201"
-```
-
-만약 CoreDNS가 죽으면? DNS 이름 해석이 안 되니, 이 주소로의 통신이 전부 실패합니다. 이미 해석된 IP가 캐시에 있으면 잠시 동안은 되겠지만, 캐시 TTL이 만료되면 끝입니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- `type: ClusterIP`와 `type: NodePort`, `type: LoadBalancer`의 차이는 무엇인가? 이 프로젝트에서 왜 `ClusterIP`만 쓰는가?
-- Flannel(VXLAN)과 Calico(BGP/eBPF)의 핵심 차이는 무엇인가? 왜 K3s는 Flannel을 기본으로 택했는가?
-- CoreDNS의 캐시 TTL이 30초인데, Pod가 20초 만에 죽고 새 Pod가 새 IP로 뜨면 DNS 캐시 때문에 오래된 IP로 가지 않는가?
-- `vault-service.yaml`에서 `port: 8200`과 `targetPort: 8200`이 같은데, 이 둘을 다르게 설정하면 어떤 일이 생기는가? 언제 다르게 쓰는가?
-- NetworkPolicy를 설정하면 iptables 규칙이 바뀌는가, 아니면 CNI 수준에서 별도 방화벽이 생기는가? (힌트: CNI마다 다르다)
-
-### 25-3. 깡통 서버(Stateless)와 볼륨 클레임(PVC)
-
-#### Stateless: 컨테이너가 죽으면 내부 데이터는 전부 사라진다
-
-K8s에서 컨테이너는 **깡통**입니다. 안에 뭘 저장하든, 컨테이너가 삭제되면 전부 사라집니다.
-
-이것이 왜 **장점**인지 직관적이지 않을 수 있습니다. "데이터가 사라지면 안 되지 않나?"
-
-장점은 **교체가 자유롭다**는 것입니다. auth-server Pod가 죽으면 K8s가 새 Pod를 만들어서 교체합니다. 이전 Pod의 "더러운" 상태(메모리 누수, 잘못된 임시 파일 등)가 깨끗이 청소되고, 새 Pod는 완전히 깨끗한 상태에서 시작합니다. 서버를 "수리"하는 것이 아니라 "교체"하는 것입니다. 이것을 **Cattle, not Pets(가축이지 애완동물이 아니다)** 패턴이라고 부릅니다.
-
-하지만 **반드시 데이터를 유지해야 하는 워크로드**가 있습니다:
-- **PostgreSQL**: DB 데이터가 사라지면 끝장입니다.
-- **Vault**: Raft 저장소에 seal/unseal 상태와 secret이 저장되어 있습니다.
-
-이런 워크로드를 위해 **PVC(PersistentVolumeClaim)**가 존재합니다.
-
-#### PVC → PV → StorageClass: 볼륨이 생기는 메커니즘
-
-PVC를 이해하려면 세 가지 개념 사이의 관계를 알아야 합니다.
-
-```mermaid
-flowchart LR
- subgraph 사용자_요청["개발자가 선언하는 것"]
- PVC["PVC
(PersistentVolumeClaim)
'5GB 볼륨 하나 주세요'"]
- end
-
- subgraph 중간_매개["K8s가 처리하는 것"]
- SC["StorageClass
'볼륨을 어떤 방식으로
만들지 정의한 템플릿'"]
- end
-
- subgraph 실제_저장소["실제로 생성되는 것"]
- PV["PV
(PersistentVolume)
'실제 5GB 디스크 공간'"]
- DISK["노드의 로컬 디스크
또는 NFS/클라우드 EBS"]
- end
-
- PVC -->|"① '5GB 주세요'
StorageClass 참조"| SC
- SC -->|"② Provisioner가
실제 볼륨 생성"| PV
- PV -->|"③ 바인딩 완료"| PVC
- PV -->|"실제 데이터 저장"| DISK
-```
-
-비유하자면:
-- **PVC**는 "5GB짜리 USB 하나 주세요"라는 **요청서**
-- **StorageClass**는 "USB는 삼성 제품으로, SSD로 만들겠다"는 **제조 사양서**
-- **PV**는 실제로 만들어진 **USB 그 자체**
-
-이 프로젝트의 `infra/vault/base/vault-pvc.yaml`을 보면:
-
-```yaml
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
- name: vault-data # 이 이름으로 Deployment에서 참조
-spec:
- accessModes:
- - ReadWriteOnce # 한 번에 하나의 노드만 읽기/쓰기 가능
- resources:
- requests:
- storage: 5Gi # 5GB 요청
-```
-
-`ReadWriteOnce`는 **이 볼륨을 동시에 두 노드에서 마운트할 수 없다**는 뜻입니다. 하나의 노드에서만 읽고 쓸 수 있습니다.
-
-이것이 `vault-deployment.yaml`에서 `strategy: Recreate`을 쓰는 이유와 직결됩니다.
-
-```yaml
-# vault-deployment.yaml
-spec:
- strategy:
- type: Recreate # 기존 Pod를 먼저 죽이고 새 Pod를 띄움
-```
-
-만약 `RollingUpdate`(기본값)를 쓰면, 새 Pod가 먼저 뜨고 나서 기존 Pod를 죽입니다. 그런데 `ReadWriteOnce` 볼륨은 동시에 두 Pod가 마운트할 수 없으니, 새 Pod는 볼륨을 마운트하지 못하고 **Pending 상태에서 영원히 멈춥니다.** 기존 Pod는 "새 Pod가 Ready가 될 때까지 죽지 마라"고 기다리니, 양쪽 다 데드락에 빠집니다.
-
-`Recreate`은 이 문제를 막습니다. 기존 Pod를 **먼저 완전히 종료**시키고, 볼륨 마운트가 해제된 다음에야 새 Pod를 띄웁니다. 대신 업데이트 동안 **잠깐 서비스가 중단**됩니다. Vault는 이 짧은 다운타임을 감수할 수 있으니 이 전략을 택한 것입니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- `ReadWriteOnce`와 `ReadWriteMany`, `ReadOnlyMany`의 차이는 무엇인가? PostgreSQL은 왜 `ReadWriteOnce`를 쓰는가?
-- PVC를 실수로 `kubectl delete pvc`로 삭제하면 데이터가 바로 날아가는가? (힌트: `reclaimPolicy`에 따라 다르다 — `Retain` vs `Delete`)
-- K3s의 기본 StorageClass인 `local-path-provisioner`는 어떻게 동작하는가? 노드의 어느 디렉터리에 데이터가 저장되는가?
-- StatefulSet과 Deployment의 차이는 무엇인가? PostgreSQL은 왜 StatefulSet을 쓰는가? (힌트: Pod 이름 안정성, 볼륨 1:1 대응)
-- `strategy: Recreate` 동안 Vault가 잠깐 죽는데, 이 시간 동안 auth-server의 JWT 서명은 어떻게 되는가?
-
-## 26. HashiCorp Vault Secret 주입 딥다이브
-
-### 26-1. Vault 아키텍처: Seal/Unseal과 Storage Backend
-
-#### Vault는 왜 "봉인(Sealed)" 상태로 시작하는가
-
-Vault를 처음 접하면 혼란스러운 개념이 **Sealed/Unsealed** 상태입니다.
-
-보통의 서비스는 프로세스가 뜨면 바로 사용 가능합니다. 하지만 Vault는 **프로세스가 떠도 "봉인" 상태**이면 아무 요청도 처리하지 못합니다. 모든 읽기/쓰기가 거부됩니다.
-
-왜 이렇게 설계했을까요?
-
-Vault가 저장하는 것은 DB 비밀번호, API 키, 암호화 키 등 **최고 기밀 정보**입니다. 이 정보들은 디스크에 **암호화되어** 저장됩니다. Vault가 기동될 때, 이 암호화된 데이터를 복호화하려면 **마스터 키**가 필요합니다.
-
-Sealed 상태란 **이 마스터 키가 메모리에 없는 상태**입니다. 디스크에 암호화된 데이터가 있지만, 열쇠가 없어서 읽을 수 없습니다.
-
-```mermaid
-flowchart LR
- subgraph SEALED["Sealed 상태 (기동 직후)"]
- direction TB
- S1["디스크: 암호화된 secret들
🔒 잠겨있음"]
- S2["메모리: 마스터 키 없음
❌ 복호화 불가"]
- S3["모든 API 요청 → 503 거부"]
- end
-
- subgraph UNSEAL["Unseal 과정"]
- direction TB
- U1["마스터 키를
메모리에 올림"]
- end
-
- subgraph UNSEALED["Unsealed 상태"]
- direction TB
- US1["디스크: 여전히 암호화 상태"]
- US2["메모리: 마스터 키 보유
✅ 요청 시 복호화 가능"]
- US3["모든 API 요청 → 정상 처리"]
- end
-
- SEALED -->|"Unseal 과정
(키 제공)"| UNSEAL
- UNSEAL --> UNSEALED
-```
-
-중요한 것은 **디스크의 데이터는 Unsealed 후에도 암호화 상태**라는 점입니다. 마스터 키는 오직 메모리에만 존재합니다. Vault 프로세스가 재시작되면 메모리가 초기화되니, 다시 Sealed 상태로 돌아갑니다.
-
-#### 이 프로젝트의 Auto-Unseal: Transit 방식
-
-사람이 매번 수동으로 unseal 하는 것은 실무에서 불가능합니다. Vault가 Pod 재시작될 때마다 24시간 대기하고 있을 수 없으니까요.
-
-이 프로젝트에서는 **Transit Auto-Unseal**을 사용합니다. 또 다른 Vault(Provider Vault, 이 프로젝트에서는 vault-transit)가 마스터 키의 암호화/복호화를 대행하는 패턴입니다.
-
-```mermaid
-sequenceDiagram
- participant WV as Workload Vault
(우리가 쓰는 Vault)
- participant PV as Provider Vault
(vault-transit)
-
- Note over WV: Pod 기동됨 → Sealed 상태
- WV->>WV: 디스크에서 암호화된
마스터 키를 읽음
- WV->>PV: "이 암호화된 마스터 키를
Transit 엔진으로 복호화해줘"
- Note over PV: Transit 키 "workload-vault-dev-unseal"로
복호화 수행
- PV-->>WV: 복호화된 마스터 키 반환
- WV->>WV: 마스터 키를 메모리에 올림
→ Unsealed 상태 전환!
- Note over WV: 이제 모든 secret 읽기/쓰기 가능
-```
-
-이 구조가 `vault.hcl`에 선언되어 있습니다:
-
-```hcl
-seal "transit" {
- address = "http://vault-transit.vault-transit.svc.cluster.local:8200"
- disable_renewal = "false"
- key_name = "workload-vault-dev-unseal"
- mount_path = "transit/"
- tls_skip_verify = "true"
-}
-```
-
-- `address`: Provider Vault의 주소입니다. K8s DNS 이름으로 접근합니다.
-- `key_name: "workload-vault-dev-unseal"`: Provider Vault의 Transit 엔진에 있는 암호화 키 이름입니다. 이 키가 Workload Vault의 마스터 키를 암호화/복호화합니다.
-
-여기서 의문이 생깁니다. Workload Vault가 Provider Vault에게 요청을 보내려면 **인증 토큰**이 필요합니다. 이 토큰은 어디서 올까요?
-
-`vault-deployment.yaml`에서 확인할 수 있습니다:
-
-```yaml
-env:
- - name: VAULT_TOKEN
- valueFrom:
- secretKeyRef:
- name: vault-transit-seal # K8s Secret에 저장된
- key: VAULT_TRANSIT_SEAL_TOKEN # Provider Vault의 제한된 토큰
-```
-
-이 토큰은 **Provider Vault의 Transit 엔진 사용 권한만** 가진 제한적 토큰입니다. 탈취되어도 할 수 있는 건 "unseal 키 복호화"뿐이며, Vault 안의 secret을 직접 읽는 것은 불가능합니다. 이것이 PPT에서 "토큰의 권한을 최소한으로"라고 언급한 부분입니다.
-
-#### Raft Storage: Vault의 내부 저장소
-
-`vault.hcl`의 다른 섹션을 보면:
-
-```hcl
-storage "raft" {
- path = "/vault/data"
- node_id = "vault-dev-0"
-}
-```
-
-**Raft**는 분산 합의 알고리즘입니다 (25장의 etcd에서 나온 그것과 같은 알고리즘). Vault는 별도의 외부 DB를 쓰지 않고, **자체적으로 Raft를 돌려서 데이터를 저장**합니다.
-
-현재 단일 노드(`node_id: vault-dev-0`)로 운영하고 있으니 합의 과정은 사실상 "혼자 결정"이지만, 노드를 추가하면 HA 구성이 가능합니다.
-
-`/vault/data` 경로는 PVC(`vault-data`)에 마운트되어 있어서, Pod가 재시작되어도 데이터가 유지됩니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- Provider Vault(vault-transit)도 재시작되면 누가 unseal하는가? 무한 재귀 아닌가? (힌트: Provider Vault는 수동 unseal 또는 별도의 auto-unseal 메커니즘 사용)
-- Raft 단일 노드에서 장애가 나면 복구 방법은? (힌트: PVC에 저장된 Raft 스냅샷)
-- `vault-transit-seal` K8s Secret이 바로 K8s Secret에 저장된다는 게 딜레마라고 PPT에서 언급했는데, 현업에서는 이걸 어떻게 해결하는가? (힌트: 클라우드의 KMS나 HSM)
-- `tls_skip_verify: "true"`는 왜 설정했는가? 운영 환경에서도 이렇게 하면 안 되는 이유는?
-- Vault의 Sealed 상태에서 readinessProbe가 fail하면 K8s는 어떻게 반응하는가?
-
-### 26-2. Vault Agent Injector의 Secret 주입 전체 과정
-
-#### Pod에 Secret이 들어가는 메커니즘: Mutating Webhook
-
-`deployment.vault-patch.yaml`에 `vault.hashicorp.com/agent-inject: "true"` 어노테이션을 달면 마법처럼 secret이 Pod에 들어갑니다. 하지만 이것은 마법이 아닙니다. **K8s의 Mutating Admission Webhook** 메커니즘이 동작하는 것입니다.
-
-이것이 무엇인지 단계별로 풀어보겠습니다.
-
-```mermaid
-sequenceDiagram
- participant USER as kubectl apply
(또는 Argo CD)
- participant API as K8s API Server
- participant INJECTOR as Vault Agent Injector
(Webhook 서버)
- participant KUBELET as kubelet
- participant AGENT as Vault Agent
(사이드카 컨테이너)
- participant VAULT as Vault Server
-
- USER->>API: "auth-server Pod를 만들어줘"
- API->>API: 어노테이션 확인:
vault.hashicorp.com/agent-inject: "true"
- API->>INJECTOR: "이 Pod 정의를 보내는데,
수정할 게 있으면 수정해줘"
-
- Note over INJECTOR: Pod 정의를 분석
vault 관련 어노테이션 발견
- INJECTOR->>INJECTOR: Pod 정의에 사이드카 컨테이너
(Vault Agent) 추가
- INJECTOR->>INJECTOR: 공유 볼륨
(/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 요청
"kv/data/dev/platform/postgres/auth-server"
- VAULT-->>AGENT: secret 데이터 반환
- AGENT->>AGENT: Go 템플릿으로 렌더링
→ /vault/secrets/runtime-env 파일 생성
-
- Note over KUBELET: auth-server 컨테이너가
/vault/secrets/runtime-env 파일을
읽어서 환경변수로 로드
-```
-
-핵심 메커니즘:
-
-**1. Mutating Webhook** — K8s API Server는 Pod가 생성되기 전에 등록된 Webhook 서버에게 "이 Pod 정의를 수정할 기회를 줄게"라고 보냅니다. Vault Agent Injector가 바로 이 Webhook 서버입니다. Injector는 vault 어노테이션이 있는 Pod 정의에 **사이드카 컨테이너(Vault Agent)**와 **공유 볼륨(/vault/secrets)**을 자동으로 추가합니다.
-
-**2. K8s Auth Handshake** — Vault Agent는 Pod 안에 자동으로 마운트된 **K8s ServiceAccount의 JWT 토큰**을 들고 Vault에 인증합니다. Vault는 이 JWT가 진짜인지 **K8s API Server에 직접 물어서** 확인합니다. 진짜라면 해당 ServiceAccount에 매핑된 policy에 따라 Vault 토큰을 발급합니다.
-
-`terraform/vault/reconcile/main.tf`에서 이 매핑이 선언되어 있습니다:
-
-```hcl
-resource "vault_kubernetes_auth_backend_role" "auth_server" {
- bound_service_account_names = ["auth-server"] # 이 SA만
- bound_service_account_namespaces = ["auth-dev"] # 이 네임스페이스에서만
- role_name = "auth-server-dev"
- token_policies = ["auth-server-dev"] # 이 policy의 권한만 부여
-}
-```
-
-이것은 "auth-dev 네임스페이스의 auth-server ServiceAccount를 가진 Pod만 `auth-server-dev` 정책으로 Vault를 사용할 수 있다"는 뜻입니다. 다른 네임스페이스의 Pod가 같은 이름의 ServiceAccount를 가져도 **접근 불가**입니다.
-
-**3. Template Rendering** — `deployment.vault-patch.yaml`의 이 부분:
-
-```yaml
-vault.hashicorp.com/agent-inject-template-runtime-env: |
- {{ with secret "kv/data/dev/platform/postgres/auth-server" }}
- export APP_DATASOURCE_USERNAME={{ printf "%q" .Data.data.APP_DATASOURCE_USERNAME }}
- export APP_DATASOURCE_PASSWORD={{ printf "%q" .Data.data.APP_DATASOURCE_PASSWORD }}
- {{ end }}
-```
-
-이것은 **Go 템플릿** 문법입니다. Vault Agent가 secret 데이터를 받아서, 이 템플릿에 맞춰 `/vault/secrets/runtime-env` 파일을 생성합니다. `printf "%q"`는 값을 따옴표로 감싸서 셸에서 안전하게 쓸 수 있도록 이스케이프하는 함수입니다.
-
-결과 파일은 이런 형태가 됩니다:
-```bash
-export APP_DATASOURCE_USERNAME="auth_user"
-export APP_DATASOURCE_PASSWORD="s3cret!p@ss"
-```
-
-auth-server 컨테이너의 시작 명령이 `. /vault/secrets/runtime-env`로 이 파일을 source하면, 환경변수로 로드됩니다.
-
-#### Sidecar vs Pre-Populate-Only: Deployment용과 Job용의 차이
-
-| | Deployment (auth-server) | Job (db-migration) |
-|---|---|---|
-| 어노테이션 | `agent-inject: "true"` (기본값) | `agent-pre-populate-only: "true"` |
-| Vault Agent 수명 | **sidecar로 계속 살아있음** | **init container로 한 번 실행 후 종료** |
-| Secret 갱신 | 주기적으로 Vault에 재요청하여 파일 갱신 가능 | 불가 (한 번 생성하고 끝) |
-| 적합한 워크로드 | 오래 실행되는 서비스. Secret rotation 시 파일이 자동 갱신됨 | 짧게 실행되고 끝나는 작업. sidecar가 계속 떠 있으면 Job이 "완료"로 전환 안 됨 |
-
-Job에서 `agent-pre-populate-only: "true"`를 안 쓰면 어떻게 될까요? Vault Agent sidecar가 계속 살아있으니까, 메인 컨테이너(migration)가 끝나도 **Pod가 Completed 상태로 전환되지 않습니다.** Job은 Pod가 완료되어야 성공으로 간주하는데, sidecar가 죽지 않으니 Job이 영원히 Running 상태에 머물러서 배포가 멈춥니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- `vault.hashicorp.com/agent-inject: "true"` 어노테이션이 필요한 것 외에, 네임스페이스에 어떤 라벨이 있어야 Injector가 동작하는가? (힌트: `vault-injection: enabled`)
-- Vault Agent가 secret rotation을 감지하면 파일은 갱신되지만, 앱 프로세스는 파일을 다시 읽지 않으면 반영이 안 된다. 이것을 어떻게 해결하는가?
-- `printf "%q"`를 안 쓰고 그냥 `.Data.data.PASSWORD`를 출력하면 어떤 문제가 생기는가? (힌트: 비밀번호에 특수문자가 있으면 셸 해석 문제)
-- `automountServiceAccountToken: true`가 왜 Vault Agent가 있는 Pod에서 필수인가?
-- 이 프로젝트에서 `vault.hashicorp.com/agent-inject-token: "true"` 어노테이션은 왜 auth-server에만 있고 migration Job에는 없는가?
-
-### 26-3. Transit Engine: JWT 서명과 Auto-Unseal의 이중 역할
-
-#### Transit Engine이란: "키를 꺼내주지 않고 대신 연산해주는 서비스"
-
-일반적인 암호화에서는 키를 가져와서 내 코드에서 직접 암호화/복호화를 합니다. 하지만 이 방식은 키가 애플리케이션 메모리에 올라오니, 메모리 덤프 등으로 키가 유출될 위험이 있습니다.
-
-Vault의 **Transit Engine**은 다릅니다. **키가 Vault 밖으로 절대 나가지 않습니다.** 대신 "이 데이터를 암호화해줘", "이 데이터에 서명해줘"라고 **API로 요청**하면, Vault가 내부에서 키를 써서 결과만 돌려줍니다.
-
-```mermaid
-flowchart LR
- subgraph 위험한_방식["일반적인 방식 (키 유출 위험)"]
- A1["앱이 키를 다운로드"] --> A2["앱 메모리에 키 올림"] --> A3["앱이 직접 서명"]
- end
-
- subgraph 안전한_방식["Transit Engine 방식"]
- B1["앱이 Vault API 호출
'이 데이터에 서명해줘'"] --> B2["Vault가 내부에서
키로 서명 수행"] --> B3["서명 결과만 반환
키는 Vault 밖으로 안 나감"]
- end
-```
-
-#### 이 프로젝트에서 Transit의 두 가지 역할
-
-Transit Engine은 이 프로젝트에서 **두 가지 완전히 다른 목적**으로 사용됩니다.
-
-**역할 1: auth-server의 JWT 서명**
-
-auth-server가 사용자에게 JWT 토큰을 발급할 때, RSA 개인키로 서명해야 합니다. 이 키를 auth-server의 메모리에 올리는 대신, **Vault Transit에게 서명을 위임**합니다.
-
-`runbooks/vault/dev/policies/auth-server-dev.hcl`:
-```hcl
-path "transit/keys/project-auth-jwt" {
- capabilities = ["read"] # 공개키 읽기 (JWT 검증용)
-}
-
-path "transit/sign/project-auth-jwt" {
- capabilities = ["update"] # 서명 요청 (JWT 발급용)
-}
-```
-
-auth-server는 `transit/sign/project-auth-jwt`로 "이 JWT 페이로드에 서명해줘"라고 요청합니다. Vault가 `project-auth-jwt`라는 RSA 키로 서명한 결과를 돌려줍니다. auth-server는 개인키를 **한 번도 본 적이 없습니다.**
-
-**역할 2: Workload Vault의 Auto-Unseal**
-
-26-1에서 설명한 것처럼, Provider Vault(vault-transit)의 Transit Engine이 Workload Vault의 마스터 키를 암호화/복호화합니다. Transit 키 이름은 `workload-vault-dev-unseal`입니다.
-
-두 역할을 그림으로 보면:
-
-```mermaid
-flowchart TB
- subgraph TRANSIT["Provider Vault의 Transit Engine"]
- direction TB
- K1["키: project-auth-jwt
(RSA 키, JWT 서명용)"]
- K2["키: workload-vault-dev-unseal
(AES 키, Unseal용)"]
- end
-
- AUTH["auth-server Pod"] -->|"'이 JWT에 서명해줘'
transit/sign/project-auth-jwt"| K1
- VAULT["Workload Vault"] -->|"'이 마스터키 복호화해줘'
transit/decrypt/workload-vault-dev-unseal"| K2
-```
-
-같은 Transit Engine이지만, **다른 키를 사용해서 완전히 다른 목적**으로 쓰이고 있습니다. `terraform/vault-transit/reconcile/main.tf`(또는 `dev/main.tf`)에서 이 키들이 Terraform으로 선언되어 있습니다.
-
-#### Transit 키 Rotation: 기존 JWT는 검증 실패하는가?
-
-Transit 키를 rotation(교체)하면 새 버전의 키가 생깁니다. Vault Transit은 **키 버전 관리**를 합니다.
-
-- **서명**: 항상 **최신 버전**의 키로 서명합니다.
-- **검증**: 서명에 포함된 키 버전 정보를 보고, **해당 버전**의 키로 검증합니다.
-
-즉, 키를 rotation해도 **이전 버전의 키가 삭제되지 않으면** 기존 JWT 검증은 계속 성공합니다. 이전 버전을 명시적으로 "min_decryption_version"으로 제한하기 전까지는 안전합니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- Transit Engine으로 JWT 서명을 하면 auth-server가 Vault에 의존하게 됩니다. Vault가 다운되면 JWT 발급이 불가한데, 이 단일 장애점(SPOF)을 어떻게 완화할 수 있는가?
-- `transit/keys/project-auth-jwt`에 `read` 권한을 준 이유는? read로 무엇을 가져오는가? (힌트: 공개키를 가져와서 JWT 검증에 사용)
-- Transit 키의 `min_encryption_version`과 `min_decryption_version`을 올리면 되돌릴 수 없다. 왜 위험한가?
-- auth-server가 Vault API를 호출하는 것과 직접 메모리에서 키로 서명하는 것의 **성능 차이**는 어느 정도인가? 이 트레이드오프는 어떤 상황에서 정당화되는가?
-- Provider Vault와 Workload Vault를 왜 분리했는가? 하나의 Vault에서 모든 것을 하면 안 되는 이유는?
-
-## 27. Terraform IaC와 Bash 자동화 심층 해부
-
-### 27-1. Terraform의 State 관리와 Plan/Apply 사이클
-
-#### Terraform은 대체 무엇을 하는 도구인가
-
-Kubernetes에서 `kubectl apply -f deployment.yaml`을 하면, K8s API Server가 "이 Deployment를 만들어라"라는 지시를 받고 처리합니다. Terraform도 **비슷한 역할**을 하지만, 대상이 K8s가 아닙니다.
-
-Terraform은 **인프라를 코드로 선언하고, 그 코드의 상태를 추적하면서, 변경이 필요한 부분만 자동으로 적용**하는 도구입니다.
-
-이 프로젝트에서 Terraform이 관리하는 것들은:
-- Vault의 **policy** (누가 어떤 secret을 읽을 수 있는지)
-- Vault의 **K8s auth backend role** (어떤 ServiceAccount가 어떤 policy를 받는지)
-- Vault의 **KV secret** (Provider Vault에서 Workload Vault로 secret 복사)
-- Vault의 **database secret engine** (동적 DB 자격 증명 설정)
-
-이것들을 사람이 하나씩 `vault` CLI로 수동 설정할 수도 있습니다. 하지만 그러면 **"지금 어디까지 설정했지?"를 기억하는 것이 불가능**합니다. 10개의 policy, 5개의 role, 8개의 secret을 수동으로 관리하면 누락이 생기고, 재현이 안 됩니다.
-
-#### State 파일: "내가 무엇을 만들었는지" 기억하는 메모장
-
-Terraform의 핵심은 **State 파일**입니다. 이것은 "Terraform이 지금까지 무엇을 만들었는지"를 기록한 JSON 파일입니다.
-
-```mermaid
-flowchart TD
- subgraph 선언["개발자가 작성한 것 (main.tf)"]
- TF["'auth-server-dev policy를 만들어라'
'auth-server K8s auth role을 만들어라'
'postgres secret을 복사해라'"]
- end
-
- subgraph state["State 파일 (.tfstate)"]
- ST["'auth-server-dev policy: 만들었음 ✅'
'auth-server K8s auth role: 만들었음 ✅'
'postgres secret: 만들었음 ✅'"]
- end
-
- subgraph 실제["실제 인프라 (Vault)"]
- REAL["auth-server-dev policy 존재
auth-server K8s auth role 존재
postgres secret 존재"]
- end
-
- TF -->|"terraform plan
선언 vs State 비교"| ST
- ST -->|"terraform apply
차이만 실제에 적용"| REAL
- REAL -->|"적용 결과를
State에 기록"| ST
-```
-
-이 흐름을 구체적으로 설명하면:
-
-**1. `terraform plan`** — "내가 원하는 상태(main.tf)"와 "지금까지 만든 것(State 파일)"을 비교합니다. 차이가 있으면 "이것을 추가하겠다", "이것을 수정하겠다", "이것을 삭제하겠다"는 계획을 보여줍니다. **아직 아무것도 실행하지 않습니다.** 계획만 보여주는 단계입니다.
-
-**2. `terraform apply`** — plan에서 나온 차이를 **실제로 적용**합니다. Vault API를 호출해서 policy를 만들고, role을 설정하고, secret을 복사합니다.
-
-**3. State 업데이트** — apply가 끝나면 "이것을 만들었다"는 기록을 State 파일에 저장합니다.
-
-다음에 같은 코드로 `terraform apply`를 다시 실행하면, State 파일을 보고 "이미 다 만들어져 있네, 할 일 없음"이라고 판단합니다. **멱등성(idempotency)** — 같은 코드를 여러 번 실행해도 결과가 동일합니다.
-
-#### State가 손상되거나 사라지면?
-
-State 파일이 손상되면 Terraform은 **자기가 무엇을 만들었는지 모르는 상태**가 됩니다. 이 상태에서 `terraform apply`를 하면, 이미 존재하는 리소스를 또 만들려고 시도해서 에러가 발생합니다.
-
-이런 상황을 복구하는 것이 `terraform import`입니다. "이 리소스는 이미 실제로 존재하는데, State에 기록이 없으니 기록해줘"라는 명령입니다.
-
-이 프로젝트의 `scripts/ci/reconcile-vault-dev.sh`에 이 패턴이 함수로 정의되어 있습니다:
+최종 dev manifest 렌더링:
```bash
-ensure_transit_state_resource() {
- local address="$1"
- local import_id="$2"
-
- # State에 이 리소스가 있는지 확인
- if ! terraform ... state show "$address" >/dev/null 2>&1; then
- # 없으면 import로 기존 리소스를 State에 등록
- log "Importing missing vault-transit state for ${address}"
- terraform ... import "$address" "$import_id"
- fi
-}
+kubectl kustomize clusters/dev-k3s/manifests/auth-server
+kubectl kustomize clusters/dev-k3s/manifests/auth-system
```
-이 함수가 하는 일을 풀어보면:
-1. `terraform state show "$address"` — State 파일에서 이 리소스가 기록되어 있는지 확인합니다.
-2. 기록이 없으면(`if !`) — `terraform import`로 실제 Vault에 존재하는 리소스를 State에 등록합니다.
-3. 기록이 있으면 — 아무것도 안 합니다.
-
-이것은 **CI 파이프라인이 State 손실에 강해지도록** 만드는 방어적 패턴입니다. CI 환경은 매번 깨끗한 러너에서 실행될 수 있으니, State가 없을 수도 있습니다. 그래도 에러 없이 이어서 할 수 있도록 보장합니다.
-
-#### Provider 이중 설정: 왜 Vault Provider가 두 개인가
-
-`terraform/vault/reconcile/main.tf`의 상단을 보면:
-
-```hcl
-provider "vault" {
- address = var.workload_vault_addr # Workload Vault (우리가 쓰는 것)
- token = var.workload_vault_token
-}
-
-provider "vault" {
- alias = "transit"
- address = var.transit_vault_addr # Provider Vault (Transit용)
- token = var.transit_vault_token
-}
-```
-
-Terraform의 **하나의 provider 블록은 하나의 서버**에 연결됩니다. 이 프로젝트에는 Vault가 **두 대**(Workload + Transit)이니, provider도 두 개가 필요합니다.
-
-`alias = "transit"`이 붙은 provider는 Transit Vault에 연결됩니다. 코드에서 `provider = vault.transit`를 지정하면 Transit Vault에 요청을 보내고, 지정하지 않으면 기본 provider(Workload Vault)에 요청을 보냅니다.
-
-```hcl
-# Provider Vault에서 secret 읽기 (transit alias 사용)
-data "vault_kv_secret_v2" "provider_postgres_superuser" {
- provider = vault.transit # ← Transit Vault에서 읽겠다
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/superuser"
-}
-
-# Workload Vault에 secret 쓰기 (기본 provider)
-resource "vault_kv_secret_v2" "platform_postgres_superuser" {
- # provider 지정 없음 → Workload Vault에 쓴다
- mount = var.kv_mount_path
- name = "dev/platform/postgres/superuser"
- data_json = jsonencode({
- POSTGRES_SUPERUSER_PASSWORD = data.vault_kv_secret_v2.provider_postgres_superuser.data["POSTGRES_SUPERUSER_PASSWORD"]
- })
-}
-```
-
-이 두 블록을 합치면: **Transit Vault에서 secret을 읽어서 → Workload Vault에 복사**하는 것입니다. 이것이 Terraform이 하는 "Secret 브릿징" 역할입니다.
-
-#### Data Source vs Resource: 읽기 전용과 쓰기의 차이
-
-| 키워드 | 의미 | 이 프로젝트 예시 |
-|---|---|---|
-| `data` | **읽기 전용**. 이미 존재하는 것을 참조만 함. Terraform이 관리하지 않음 | `data "vault_kv_secret_v2"` — Transit Vault의 secret을 **읽기만** |
-| `resource` | **생성/수정/삭제**. Terraform이 생명주기를 관리함. State에 기록됨 | `resource "vault_kv_secret_v2"` — Workload Vault에 secret을 **생성** |
-| `resource` | | `resource "vault_policy"` — Vault policy를 **생성/업데이트** |
-| `resource` | | `resource "vault_kubernetes_auth_backend_role"` — K8s Auth role **생성** |
-
-`data`로 읽은 값을 `resource`에서 사용하는 것이 **참조 패턴**입니다. Transit Vault에서 `data`로 비밀번호를 읽고, Workload Vault에 `resource`로 복사하는 것이 이 패턴의 전형입니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- `terraform destroy`를 CI에서 실수로 실행하면 어떻게 되는가? 모든 policy, role, secret이 삭제되면 서비스에 어떤 영향이 있는가?
-- State 파일(`.tfstate`)에는 민감한 정보(secret 값 등)가 포함되는가? 그렇다면 이 파일을 어떻게 보호해야 하는가?
-- `terraform plan`에서 "변경 없음"이 나왔는데 `terraform apply`를 하면 실제로 변경되는 경우가 있는가? (힌트: provider의 API가 plan 시점과 apply 시점 사이에 바뀌면)
-- 이 프로젝트에서 `backend "local"`을 쓰고 있는데, 팀으로 협업할 때는 왜 remote backend(S3, GCS 등)를 써야 하는가?
-- `skip_child_token = true`가 provider 설정에 있는 이유는 무엇인가? 이것을 안 쓰면 어떤 문제가 생기는가?
-
-### 27-2. Bash 방어적 프로그래밍: trap, set, wait
-
-#### `set -euo pipefail` — 셸 스크립트의 안전벨트 3종 세트
-
-이 프로젝트의 모든 Bash 스크립트는 첫 줄 근처에 이것이 있습니다:
+전체 검증:
```bash
-set -euo pipefail
+make validate
```
-이것은 **세 가지 안전장치**를 동시에 거는 것입니다. 각각이 무엇인지, **없으면 어떤 끔찍한 일이 생기는지** 구체적으로 보겠습니다.
-
-**`-e` (errexit): 에러 발생 시 즉시 종료**
+backend 없이 Terraform configuration 검증:
```bash
-# -e 없이 실행하면:
-kubectl apply -f wrong-file.yaml # ← 에러 발생! 파일이 없음
-echo "배포 성공!" # ← 이 줄이 실행됨!!! 에러가 무시됨
-vault write secret/data ... # ← 잘못된 상태에서 계속 진행
-
-# -e 있으면:
-kubectl apply -f wrong-file.yaml # ← 에러 발생!
-# 스크립트 즉시 종료. 아래 줄은 실행 안 됨
+terraform -chdir=iac/terraform/live/dev-k3s/vault-core init -backend=false
+terraform -chdir=iac/terraform/live/dev-k3s/vault-core validate
```
-`-e` 없이 스크립트를 짜면, 중간에 에러가 나도 멈추지 않고 **다음 줄로 넘어갑니다.** CI에서 이것은 치명적입니다. 앞 단계가 실패했는데 뒷 단계가 계속 실행되면, 반쪽짜리 인프라가 구성됩니다.
-
-**`-u` (nounset): 정의되지 않은 변수 사용 시 에러**
+실제 plan:
```bash
-# -u 없이:
-echo "Vault 주소: ${VAULT_ADR}" # ← 오타! VAULT_ADDR가 맞는데
-# → "Vault 주소: " 빈 문자열 출력, 에러 없이 넘어감
-vault login -address="" # ← 빈 주소로 로그인 시도...
-
-# -u 있으면:
-echo "Vault 주소: ${VAULT_ADR}" # ← 에러! "VAULT_ADR: unbound variable"
-# 스크립트 즉시 종료. 변수 오타를 바로 잡을 수 있음
+make terraform-plan \
+ TF_ROOT=vault-core \
+ BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
```
-변수 이름 오타는 누구나 합니다. `-u`가 없으면 오타된 변수가 빈 문자열로 조용히 치환되어, 디버깅하기 극도로 어려운 버그가 됩니다.
-
-**`-o pipefail`: 파이프라인에서 중간 명령 에러 전파**
-
-```bash
-# pipefail 없이:
-vault read secret/data | jq '.data'
-# 만약 vault read가 실패해도, jq가 성공(빈 입력에 에러 없이 종료)하면
-# 전체 파이프라인은 "성공"으로 간주됨!
-
-# pipefail 있으면:
-vault read secret/data | jq '.data'
-# vault read가 실패하면 → 파이프라인 전체가 실패로 간주
-```
-
-`A | B`에서 `-o pipefail` 없이는 **B의 종료 코드만** 확인합니다. A가 실패해도 B가 성공이면 전체가 성공입니다. `pipefail`을 켜면 A, B 중 **하나라도** 실패하면 전체가 실패합니다.
-
-#### `trap ... EXIT` — 뒷정리 보장 메커니즘
-
-`scripts/ci/reconcile-vault-dev.sh`의 `start_port_forward` 함수를 보면:
-
-```bash
-start_port_forward() {
- local namespace="$1"
- local service="$2"
- local local_port="$3"
- local remote_port="$4"
- local log_file="$5"
-
- # port-forward를 백그라운드에서 실행
- kubectl -n "$namespace" port-forward "svc/${service}" \
- "${local_port}:${remote_port}" >"$log_file" 2>&1 &
-
- local pf_pid=$! # 방금 백그라운드로 보낸 프로세스의 PID를 저장
-
- # EXIT 트랩: 스크립트가 어떤 이유로든 종료될 때 이 프로세스를 죽임
- trap 'kill "$pf_pid" >/dev/null 2>&1 || true' EXIT
-
- printf '%s\n' "$pf_pid"
-}
-```
-
-이 코드를 한 줄씩 해부합니다.
-
-**`... &` (백그라운드 실행)** — `kubectl port-forward`는 끝나지 않는 프로세스입니다. 포트 포워딩을 유지하려고 계속 대기합니다. 이것을 `&`로 백그라운드로 보내야 스크립트의 다음 줄이 실행됩니다. `&` 없이 실행하면 스크립트가 여기서 영원히 멈춥니다.
-
-**`$!` (마지막 백그라운드 PID)** — 방금 `&`로 보낸 프로세스의 PID(프로세스 번호)를 캡처합니다. 나중에 이 프로세스를 죽이려면 PID를 알아야 합니다.
-
-**`trap 'kill "$pf_pid" ...' EXIT`** — 이것이 핵심입니다. `trap`은 "특정 신호를 받으면 이 명령을 실행하라"는 뜻입니다. `EXIT`는 "스크립트가 종료될 때"입니다.
-
-```mermaid
-flowchart TD
- A["스크립트 시작"] --> B["port-forward 프로세스 시작
PID 저장"]
- B --> C["trap 등록:
'종료 시 PID를 kill해라'"]
- C --> D["Terraform apply 등
메인 작업 실행"]
- D --> E{"결과는?"}
- E -->|"성공"| F["스크립트 정상 종료"]
- E -->|"에러 발생 (-e로 즉시 종료)"| G["스크립트 에러 종료"]
- F --> H["trap 발동:
port-forward 프로세스 kill"]
- G --> H
- H --> I["깔끔하게 종료됨
좀비 프로세스 없음"]
-```
-
-trap이 없으면 어떻게 될까요?
-
-스크립트가 에러로 중단되면 `kill` 명령이 실행되지 않고, **port-forward 프로세스가 좀비처럼 살아남습니다.** CI 러너에서 이런 좀비 프로세스가 쌓이면 포트 충돌이 발생하고, 다음 CI 실행이 실패합니다.
-
-`|| true`는 "kill이 실패해도(이미 프로세스가 죽어있어도) 에러로 취급하지 마라"는 뜻입니다. `-e`가 켜져 있으니, `kill` 실패가 스크립트 종료를 유발하는 것을 방지합니다.
-
-#### `require_cmd` / `require_env` — 사전 조건 검증 패턴
-
-```bash
-require_cmd() {
- local cmd="$1"
- if ! command -v "$cmd" >/dev/null 2>&1; then
- echo "$cmd is required" >&2
- exit 1
- fi
-}
-
-require_env() {
- local name="$1"
- if [[ -z "${!name:-}" ]]; then
- echo "$name must be set" >&2
- exit 1
- fi
-}
-```
-
-이 함수들은 스크립트의 **맨 처음**에 호출됩니다:
-
-```bash
-reconcile_transit() {
- require_cmd kubectl
- require_cmd vault
- require_cmd terraform
- require_cmd curl
- require_cmd jq
- require_env TF_STATE_DIR
- require_env TRANSIT_VAULT_ADDR
- # ... 여기서 필수 도구와 환경변수가 있는지 먼저 확인
-```
-
-이 패턴의 목적: **10분 동안 실행한 뒤에 "jq가 없습니다"로 실패하는 것을 방지**합니다. 필수 조건을 스크립트 시작 시점에 전부 확인하고, 하나라도 빠지면 즉시 종료합니다. 디버깅 시간을 극적으로 줄여줍니다.
-
-`${!name:-}`의 의미: `${!name}`는 **변수 간접 참조**입니다. `name` 변수에 "VAULT_ADDR"가 들어있으면, `${!name}`는 `$VAULT_ADDR`의 값을 가져옵니다. `:-`는 "변수가 없으면 빈 문자열을 반환"하는 기본값 구문으로, `-u` 옵션에 의한 에러를 방지합니다.
-
-#### `kubectl wait` vs `sleep` — 조건 기반 대기의 중요성
-
-```bash
-# 이 프로젝트의 방식 (올바른 방법)
-kubectl -n vault-transit wait --for=condition=available \
- deployment/vault-transit --timeout=300s
-
-# 나쁜 방법
-sleep 60 # 60초면 되겠지...?
-```
-
-| | `kubectl wait` | `sleep` |
-|---|---|---|
-| 대기 방식 | **조건을 지속 확인**. 조건 만족 즉시 통과 | 고정 시간만큼 무조건 대기 |
-| 리소스가 30초에 준비되면 | 30초에 즉시 다음 단계 진행 | 60초 다 기다림 (30초 낭비) |
-| 리소스가 90초에 준비되면 | 90초에 즉시 다음 단계 진행 | 60초에 성공으로 간주... **실제로는 아직 안 됨!** |
-| timeout 처리 | `--timeout=300s` 초과 시 에러 코드 반환 → `-e`로 스크립트 종료 | timeout 개념 자체가 없음 |
-
-`sleep`은 **"이 정도면 되겠지"라는 추측**에 기반합니다. CI 환경의 부하 상태에 따라 리소스 준비 시간이 달라지니, 어떤 때는 되고 어떤 때는 안 되는 **불안정한(flaky) 파이프라인**이 됩니다.
-
-`kubectl wait`는 **실제 상태를 확인**합니다. Deployment가 Available 조건을 만족하는 순간 통과하고, timeout 안에 안 되면 명확하게 실패합니다.
-
-#### 이 주제에서 스스로 던져봐야 할 질문들
-
-- `trap`이 여러 번 호출되면 어떤 것이 실행되는가? 마지막에 등록된 것만? 전부? (힌트: 같은 신호에 대한 trap은 덮어쓰기됨)
-- `-e`가 켜져 있을 때 `if ! command ...` 구문에서 command가 실패하면 스크립트가 종료되는가? (힌트: `if`문 안에서는 `-e`가 일시 중단됨)
-- `2>&1`의 의미는 무엇인가? `>&2`와는 무엇이 다른가? (힌트: 표준 에러 리다이렉션 방향의 차이)
-- `terraform apply -auto-approve`는 plan 확인 없이 바로 적용한다. CI에서는 왜 이것을 쓰는가? 사람이 직접 실행할 때는 왜 위험한가?
-- `reconcile-vault-dev.sh`에서 `transit_tf_token="$(transit_login)"`을 왜 두 번 호출하는가? 한 번이면 안 되는가? (힌트: policy 업데이트 후 새 토큰이 필요)
-
-
+image 승격은 Gitea `Promote Dev Image by Pull Request` workflow에 정확한
+`sha256:` digest를 전달합니다. workflow는 전용 브랜치와 PR을 만들며
+`main`에 직접 쓰지 않습니다.
+## 읽는 순서
+1. `README.md`
+2. `docs/architecture/deployment.md`
+3. `docs/architecture/secret-trust.md`
+4. `docs/adr/`
+5. 수행하려는 작업의 runbook
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..43c4735
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,34 @@
+.PHONY: bootstrap validate vault-init terraform-init terraform-plan terraform-apply check-terraform-inputs
+
+KUBE_CONTEXT ?=
+TF_ROOT ?=
+BACKEND_CONFIG ?=
+TF_DIR = iac/terraform/live/dev-k3s/$(TF_ROOT)
+
+bootstrap:
+ @test -n "$(KUBE_CONTEXT)" || (echo "KUBE_CONTEXT is required" >&2; exit 1)
+ ./hack/bootstrap-argocd.sh --context "$(KUBE_CONTEXT)"
+
+validate:
+ ./hack/validate.sh
+
+vault-init:
+ ./hack/vault-init.sh init
+
+check-terraform-inputs:
+ @case "$(TF_ROOT)" in \
+ vault-core|vault-database) ;; \
+ *) echo "TF_ROOT must be vault-core or vault-database" >&2; exit 1 ;; \
+ esac
+ @test -f "$(BACKEND_CONFIG)" || (echo "BACKEND_CONFIG must point to a readable backend file" >&2; exit 1)
+
+terraform-init: check-terraform-inputs
+ terraform -chdir="$(TF_DIR)" init -input=false -reconfigure -backend-config="$(abspath $(BACKEND_CONFIG))"
+
+terraform-plan: terraform-init
+ terraform -chdir="$(TF_DIR)" plan -input=false -lock-timeout=5m
+
+terraform-apply: terraform-init
+ @test "$(APPROVE_APPLY)" = "dev-k3s/$(TF_ROOT)" || \
+ (echo "Set APPROVE_APPLY=dev-k3s/$(TF_ROOT) to continue" >&2; exit 1)
+ terraform -chdir="$(TF_DIR)" apply -input=false -lock-timeout=5m
diff --git a/README.md b/README.md
index 31681d1..ab18ce2 100644
--- a/README.md
+++ b/README.md
@@ -1,1601 +1,100 @@
-# Project-Auth-GitOps
-GitOps repo에서는 앱/인프라별 공통(base)과 환경별 차이(overlay)를 관리하고, 실제 운영 선언만 둡니다.
+# Project GitOps
-## 현재 최신 Dev 아키텍처
+Project Auth의 `dev-k3s` 배포 상태를 관리하는 GitOps configuration
+저장소입니다. 배포 기준 저장소는 내부 Gitea 한 곳입니다.
-최상단 아키텍처는 **항상 최신 dev 기준**만 유지합니다.
-아키텍처가 변경되면 이 섹션은 최신 상태로 갱신하고, 변경 이유와 전후 비교는 아래 cycle에 누적 기록합니다.
-
-```mermaid
-flowchart TD
- subgraph APP_REPOS[Application Repos]
- R1[Project-Auth-Server CI]
- R2[Project-Api-Server CI]
- end
-
- subgraph REPO[Project-Auth-GitOps]
- subgraph AUTOMATION[CD automation]
- U1[.github/workflows/update-image-tag.yaml]
- U2[.github/workflows/vault-dev-reconcile.yaml]
- end
-
- subgraph ARGO[argocd/dev]
- P1[projects/apps-project.yaml]
- P2[projects/infra-project.yaml]
- A1[applications/apps/auth-server.yaml]
- A2[applications/apps/api-server.yaml]
- A3[applications/infra/vault-transit.yaml]
- A4[applications/infra/vault.yaml]
- A5[applications/infra/platform.yaml]
- A6[applications/infra/vault-agent-injector.yaml]
- A7[applications/infra/sealed-secrets.yaml]
- end
-
- subgraph MANIFESTS[manifests]
- subgraph APPS[apps]
- M1[auth-server/overlays/dev]
- M2[api-server/overlays/dev]
- end
- subgraph INFRA[infra]
- M3[vault-transit/overlays/dev]
- M4[vault/overlays/dev]
- M5[platform/overlays/dev]
- end
- end
- end
-
- subgraph CI[Self-hosted Runner]
- C1[reconcile transit provider policy + workflow role]
- C2[read workload bootstrap AppRole from provider Vault]
- C3[reconcile workload Vault policy + runtime KV + db roles]
- C4[apply Argo CD dev apps]
- end
-
- subgraph OPERATOR[Operator Workstation]
- O1[runbooks/vault-transit/dev bootstrap]
- O2[runbooks/vault/dev bootstrap]
- end
-
- subgraph CLUSTER[Dev Cluster]
- N1[namespace: auth-dev]
- N2[namespace: api-dev]
- N3[namespace: platform]
- N4[namespace: vault]
- N5[namespace: vault-transit]
- N6[namespace: kube-system]
- W1[auth-server]
- W2[api-server]
- W3[postgres]
- W4[keycloak]
- W5["vault-transit
Transit unseal provider"]
- W6["vault
KV + Kubernetes auth + Transit JWT"]
- W7[vault-agent-injector]
- W8[sealed-secrets-controller]
- W9[traefik ingress]
- end
-
- R1 -->|new auth image tag| U1
- R2 -->|new api image tag| U1
- U1 --> M1
- U1 --> M2
- U2 --> C1
- C1 --> C2
- C2 --> C3
- C3 --> C4
-
- P1 --> A1
- P1 --> A2
- P2 --> A3
- P2 --> A4
- P2 --> A5
- P2 --> A6
- P2 --> A7
-
- A1 --> M1
- A2 --> M2
- A3 --> M3
- A4 --> M4
- A5 --> M5
- A6 --> N4
-
- M1 --> N1
- M2 --> N2
- M3 --> N5
- M4 --> N4
- M5 --> N3
- A7 --> N6
- O1 --> W5
- O2 --> W6
- C1 --> W5
- C2 --> W5
- C3 --> W6
- C4 --> A1
- C4 --> A2
-
- N1 --> W1
- N2 --> W2
- N3 --> W3
- N3 --> W4
- N4 --> W6
- N4 --> W7
- N6 --> W8
- N6 --> W9
- N5 --> W5
-
- O2 -. browser/API access via local hosts mapping .-> W9
- W9 --> W1
- W9 --> W2
- W9 --> W4
- W2 -. JWT issuer .-> W1
- W1 -. datasource .-> W3
- W1 -. oauth2 provider .-> W4
- W6 -. transit auto-unseal .-> W5
- W1 -. kubernetes auth + transit .-> W6
- W3 -. injector secret render .-> W6
- W4 -. injector secret render .-> W6
+```text
+https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
```
-## 이 저장소의 역할
+애플리케이션 소스 저장소는 이미지를 빌드해 GHCR에 올리고, 이 저장소에는
+배포할 immutable digest를 변경하는 PR만 생성합니다. Kubernetes 리소스는
+Argo CD만 반영하며, Terraform은 Vault API만 관리합니다.
-이 저장소는 **CI가 아니라 CD 중심 GitOps repo**입니다.
+## 지원 범위
-- Kubernetes manifest 관리
-- Argo CD `Application` / `AppProject` 관리
-- 이미지 태그 업데이트 반영
-- 환경별 overlay 관리
-- 실제 배포 반영
+| 대상 | 상태 |
+|---|---|
+| `dev-k3s` | 지원하는 단일 노드 개발 환경 |
+| production | 설계되지 않았으며 manifest/Application이 존재하지 않음 |
+| Istio | 보류; 향후 ambient mode 후보 |
-## 현재 CD 반영 흐름
+현재 dev Vault와 PostgreSQL, ingress는 TLS가 적용되지 않은 개발 프로파일입니다.
+production 용도로 사용할 수 없습니다.
-1. 앱 repo(`Project-Auth-Server`, `Project-Api-Server`)에서 `feature -> main/develop` 병합 후 CI를 실행합니다.
-2. CI가 테스트 통과 뒤 이미지를 build/push하고 새 이미지 태그를 만듭니다.
-3. 앱 repo CI는 이미지 push 뒤 `repository_dispatch`로 이 저장소의 `.github/workflows/update-image-tag.yaml`을 호출해 dev overlay 태그를 갱신합니다.
-4. 최초 1회 bootstrap 또는 복구가 필요할 때는 운영자가 runbook으로 privileged token을 사용해 transit/workload Vault bootstrap을 수행합니다.
-5. 평상시에는 self-hosted runner의 `.github/workflows/vault-dev-reconcile.yaml` 이 **bootstrap readiness 확인 후** transit/workload Vault reconcile 과 Argo CD dev 정의 적용을 자동 수행합니다.
-6. Argo CD가 GitOps repo와 Application 변경을 감지합니다.
-7. `vault-transit` provider가 workload Vault의 transit auto-unseal을 지원합니다.
-8. Argo CD가 cluster에 실제 배포를 반영합니다.
+## 제어 흐름
-즉, 앱 repo는 **CI 책임**, GitOps repo는 **CD 책임**을 갖고, 이미지 태그는 앱 repo가 자기 repo manifest를 수정하는 대신 **GitOps repo를 갱신하는 방식**으로 반영합니다.
+```text
+app CI -> GHCR digest -> GitOps PR -> validation -> main
+ |
+ v
+ Argo CD
+ |
+ v
+ Kubernetes
-## Bootstrap vs Reconcile
-
-- manual bootstrap runbook
- - 목적: privileged token으로 transit/workload Vault를 최초 1회 bootstrap 하거나 provider bootstrap path를 복구
- - 실행 방식: 운영자 로컬/관리자 터미널 수동 실행
-- `vault-dev-reconcile`
- - 목적: 이미 bootstrap이 끝난 Vault를 workflow AppRole 기준으로 안전하게 reconcile
- - 전제: `kv/dev/workload/bootstrap` 과 provider seed path가 이미 준비돼 있어야 함
-
-즉 routine CI는 bootstrap을 “대신 수행”하지 않고, bootstrap이 끝났는지 확인한 뒤 그 상태를 유지/동기화하는 역할만 맡습니다.
-
-## 현재 Secret Lifecycle
-
-- `ghcr-regcred`처럼 **image pull secret**이 필요한 항목만 `SealedSecret`을 유지합니다.
-- `auth-server`, `postgres`, `keycloak`의 **runtime secret**은 더 이상 Git이나 workflow secret에 넣지 않고 workload Vault KV(`kv/dev/...`)에 저장합니다.
-- workload KV의 seed 값과 workload Vault bootstrap token은 `vault-transit` provider Vault KV가 source of truth 역할을 합니다.
-- `vault-transit` provider와 workload Vault는 각각 클러스터 밖 runbook으로 1회 init/bootstrap 합니다.
-- 이후 dev 자동화는 self-hosted runner의 CI secret store에 저장한 **transit provider workflow AppRole 정보**만 사용하고, 실제 workload secret 값은 provider Vault에서 읽습니다.
-- root token은 bootstrap 직후 revoke하는 것을 기본값으로 두고, Kubernetes 안에는 저장하지 않습니다.
-- 애플리케이션과 플랫폼 워크로드는 Vault Agent Injector와 Kubernetes auth로 인증하고 secret file을 렌더링받습니다.
-- workload Vault는 `vault-transit` provider가 발급한 최소 권한 transit token으로 auto-unseal 합니다.
-- `auth-server`는 Injector가 공유한 Vault token file을 사용해 workload Vault Transit을 계속 호출합니다.
-
-## Auto-unseal 상태
-
-- 현재 dev 환경은 **Vault 2개 구조의 Transit auto-unseal** 을 전제로 합니다.
-- `vault-transit` provider가 `workload-vault-dev-unseal` transit key를 제공하고, workload Vault는 `vault-transit-seal` secret의 최소 권한 token으로 auto-unseal 합니다.
-- CI secret store는 provider Vault workflow AppRole 같은 최소 자동화 정보만 보관합니다.
-- 이 구조는 단일 Vault보다 운영 난이도는 높지만, root token 직접 사용 최소화와 trust boundary 분리에 더 유리합니다.
-
-## 현재 Dev 접근 경로
-
-- `auth-server`, `api-server`, `keycloak` 은 여전히 `ClusterIP` 로 유지하고, dev에서는 `traefik` `Ingress` 를 north-south 진입점으로 둡니다.
-- public host는 `auth-public.auth-dev.svc.cluster.local`, `api-public.api-dev.svc.cluster.local`, `keycloak-public.platform.svc.cluster.local` 로 분리하고, namespace 내부 DNS에서는 `ExternalName -> traefik` 경로로 같은 호스트를 해석합니다.
-- 외부 접근이 필요할 때는 운영자 노트북의 `hosts` 파일을 현재 Traefik `LoadBalancer` IP 또는 dev node IP로 매핑해 callback/redirect 와 브라우저 접근을 엽니다.
-- 예시: ` auth-public.auth-dev.svc.cluster.local api-public.api-dev.svc.cluster.local keycloak-public.platform.svc.cluster.local`
-- dev public ingress는 HTTP만 사용합니다.
-- `auth-server` 와 `api-server` 는 namespace 내부에서도 같은 public host를 HTTP로 호출합니다.
-- east-west는 `auth-dev`, `api-dev`, `platform`, `vault` namespace에 `default deny + allowlist` `NetworkPolicy` 를 적용해 필요한 흐름만 열어둡니다.
-
-## README 작성 원칙
-
-이 저장소의 README에는 `ops` 관련 내용만 기록합니다.
-
-문서 작성은 1회성 정리가 아니라 **변경 이력 누적 방식**으로 관리합니다.
-즉, 기존에 작성한 구조/문제점/개선 내용을 지우고 새로 덮어쓰지 않고, **항상 기존 내용 아래에 이어서 추가**합니다.
-다만 README 최상단의 `현재 최신 Dev 아키텍처` 섹션은 예외적으로 **항상 최신 상태로 갱신**합니다.
-
-이 README에는 아래 사이클을 반복해서 계속 누적 작성합니다.
-
-1. 처음 구조 `mermaid`
-2. 해당 구조의 문제점
-3. 변경된 후 구조 `mermaid`
-4. 이전 구조 대비 변경된 점
-5. 변경으로 해결된 내용
-
-## 기록 규칙
-
-- 이전 사이클은 삭제하거나 수정해서 덮어쓰지 않습니다.
-- README 최상단의 `현재 최신 Dev 아키텍처`는 최신 상태만 유지하고, 예전 아키텍처는 cycle로 추적합니다.
-- 새로운 `ops` 변경이 생기면 README의 가장 아래에 새 사이클을 추가합니다.
-- 아키텍처 변경이 생기면 먼저 최상단 `현재 최신 Dev 아키텍처`를 갱신하고, 같은 변경을 새 cycle에 기록합니다.
-- 각 사이클은 당시의 구조, 문제, 개선 결과가 모두 보이도록 독립적으로 작성합니다.
-- 구조 설명은 가능하면 `mermaid` 다이어그램으로 남깁니다.
-- 변경된 점은 반드시 **이전 구조와 비교**해서 작성합니다.
-- 해결 내용은 어떤 문제가 어떻게 해소되었는지 명확하게 작성합니다.
-- 런타임 장애나 수동 운영 이슈를 해결했으면, README 하단 cycle에 **재현 명령, 핵심 관찰값, 판단 근거, 수정 내용, 검증 명령** 을 함께 남깁니다.
-- 트러블슈팅 명령은 가능하면 실제로 사용한 형태 그대로 남기고, 왜 그 명령을 쳤는지 한 줄로 설명합니다.
-- secret, token, kubeconfig 본문처럼 민감한 값은 절대 그대로 기록하지 않고, 값의 존재 여부나 길이만 요약합니다.
-
-## 작성 템플릿
-
-아래 형식을 반복해서 README 하단에 계속 추가합니다.
-
-````md
-## Cycle N
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- A[example]
+IaC PR -> Terraform plan -> approval -> Terraform apply -> Vault API
```
-### 2. 문제점
-- 문제 1
-- 문제 2
+Argo CD 최초 설치와 root Application seed만 클러스터에 직접 적용합니다.
+그 이후 routine deployment는 Git 변경으로만 수행합니다.
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- A[changed-example]
+## 저장소 구조
+
+```text
+bootstrap/argocd/ 최초 Argo CD 설치 버전과 단일 root Application
+clusters/dev-k3s/
+ projects/ AppProject 권한 경계
+ applications/ foundation, platform, workload child Applications
+ manifests/ dev-k3s가 실제 소비하는 최종 Kustomize 구성
+platform/ Vault와 auth platform의 환경 중립 base
+workloads/ auth-server와 api-server의 환경 중립 base
+iac/terraform/
+ modules/ 재사용 Vault 모듈
+ live/dev-k3s/ vault-core, vault-database state
+ backend/dev-k3s/ secret을 포함하지 않는 backend 예시
+policies/ Terraform이 소비하는 Vault ACL
+hack/ bootstrap, validation, 조건부 Vault init entrypoint
+docs/ architecture, ADR, recovery/operation runbook
```
-### 4. 이전 구조 대비 변경점
-- 변경점 1
-- 변경점 2
+cluster-specific namespace, host, image, secret reference는
+`clusters/dev-k3s/manifests/`에서 완결합니다. `platform/`과 `workloads/`의
+base는 클러스터를 알지 못합니다.
-### 5. 해결된 내용
-- 해결 1
-- 해결 2
+## Terraform state
-### 6. 트러블슈팅 메모
-- 재현/확인 명령
-- 핵심 관찰값
-- 판단 근거
-- 수정 또는 조치
-- 검증 명령
-````
+| Root | 소유 범위 | 실행 권한 |
+|---|---|---|
+| `vault-core` | mounts, auth backends, policies, Kubernetes/JWT roles, app Transit key | 제한된 관리자 |
+| `vault-database` | PostgreSQL connection과 dynamic database roles | `vault-database-automation-dev` |
-## 트러블슈팅 메모 작성 예시
+Vault API 객체 하나는 정확히 한 state만 소유합니다. KV secret payload,
+Vault init JSON, token, PostgreSQL password는 Git이나 Terraform state에
+저장하지 않습니다. backend는 암호화, versioning, access control,
+locking을 제공해야 합니다.
-- 재현/확인 명령: `kubectl -n argocd describe application vault-transit-dev`
-- 핵심 관찰값: `authentication required: Repository not found`
-- 판단 근거: Argo CD app spec 자체는 존재하지만 repo-server가 source repo를 읽지 못해 manifest generation 전에 실패한다고 봤습니다.
-- 수정 또는 조치: `argocd` namespace에 `repo-creds` secret을 선언형으로 적용했습니다.
-- 검증 명령: `kubectl -n argocd annotate application vault-transit-dev argocd.argoproj.io/refresh=hard --overwrite`
+## 주요 명령
-## Cycle 1
+```bash
+make validate
+make bootstrap KUBE_CONTEXT=
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph AUTH[Project-Auth-Server]
- A1[argocd/*.yaml]
- A2[k8s/dev]
- A3[k8s/platform-dev]
- end
-
- subgraph API[Project-Api-Server]
- B1[argocd/*.yaml]
- B2[k8s/dev]
- end
-
- subgraph GITOPS_BEFORE[Project-Auth-GitOps]
- C1[README only]
- end
+make terraform-plan \
+ TF_ROOT=vault-core \
+ BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
```
-### 2. 문제점
-- 운영 선언이 `Project-Auth-Server`와 `Project-Api-Server`에 분산되어 있어서 GitOps 저장소가 실제 단일 운영 기준점이 아니었습니다.
-- `auth-server`는 `k8s/dev`와 `k8s/platform-dev`가 분리돼 있었지만, 현재 GitOps 저장소 기준의 공통 `base`와 환경별 `overlay` 구조가 없었습니다.
-- Argo CD `Application`의 source repo가 각 서비스 repo를 가리키고 있어, 운영 경로를 한 저장소에서 일관되게 추적하기 어려웠습니다.
-- 두 서비스 모두 `prod`를 수용할 고정 overlay 진입점이 없어 이후 환경 확장 시 구조가 다시 흔들릴 수 있었습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph GITOPS_AFTER[Project-Auth-GitOps]
- subgraph APPS[apps]
- subgraph AUTH_APP[auth-server]
- D1[base]
- D2[overlays/dev]
- D3[overlays/dev/platform]
- D4[overlays/prod]
- end
-
- subgraph API_APP[api-server]
- E1[base]
- E2[overlays/dev]
- E3[overlays/prod]
- end
- end
-
- subgraph ARGO[argocd]
- F1[projects/*.yaml]
- F2[applications/*.yaml]
- end
- end
-```
-
-### 4. 이전 구조 대비 변경점
-- `auth-server`와 `api-server`의 Kubernetes 운영 매니페스트를 현재 GitOps repo의 `apps/` 아래로 이관했습니다.
-- 앱 공통 리소스는 `base`로 분리하고, namespace/configmap/sealed secret/image tag 같은 환경 값은 `overlays/dev`로 분리했습니다.
-- `auth-server`의 `platform-dev` 리소스는 `apps/auth-server/overlays/dev/platform`으로 옮겨 기존 dev platform 운영 구성을 유지했습니다.
-- Argo CD `AppProject`와 `Application`도 현재 GitOps repo 기준으로 재배치하고, `repoURL`과 `path`를 새 구조에 맞게 변경했습니다.
-- 원본 repo에 `prod` 운영 매니페스트는 없었기 때문에, 이번 변경에서는 `overlays/prod`에 namespace와 kustomization 골격만 먼저 추가했습니다.
-
-### 5. 해결된 내용
-- 이제 `Project-Auth-GitOps`가 `auth-server`, `api-server`, `platform-dev`의 운영 선언을 모으는 단일 저장소 역할을 하게 되었습니다.
-- 서비스마다 서로 다른 운영 경로를 읽지 않아도 되어, 변경 리뷰와 Argo CD 추적 기준이 단순해졌습니다.
-- 이후 환경이 늘어나더라도 `apps//base`와 `apps//overlays/` 패턴으로 같은 방식의 확장이 가능해졌습니다.
-- `auth-dev` 프로젝트에 `SealedSecret` 허용 리소스를 추가해, 이관된 sealed secret 리소스가 Argo CD 정책과 맞지 않던 문제도 함께 정리했습니다.
-
-## Cycle 2
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph ARGOCD_BEFORE[argocd]
- A1[applications/auth-server-dev.yaml]
- A2[applications/api-server-dev.yaml]
- A3[applications/platform-dev.yaml]
- A4[applications/sealed-secrets-dev.yaml]
- B1[projects/auth-dev-project.yaml]
- B2[projects/api-dev-project.yaml]
- B3[projects/infra-dev-project.yaml]
- end
-```
-
-### 2. 문제점
-- `applications`와 `projects`가 파일 단위로 평평하게 놓여 있어서 `dev/prod` 경계와 `apps/infra` 경계가 디렉터리 구조에 드러나지 않았습니다.
-- 앱용 프로젝트가 `auth-dev`, `api-dev`로 분산돼 있어, 같은 성격의 애플리케이션을 한 번에 파악하기 어려웠습니다.
-- `prod`용 Argo CD 진입점이 구조상 준비돼 있지 않아 환경 확장 시 다시 디렉터리 재정리가 필요했습니다.
-- 파일 수가 늘어날수록 어떤 선언이 서비스용인지 인프라용인지 찾는 비용이 계속 커질 구조였습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph ARGOCD_AFTER[argocd]
- subgraph APPS[applications]
- subgraph DEV_APPS[dev]
- C1[apps/auth-server.yaml]
- C2[apps/api-server.yaml]
- C3[infra/platform.yaml]
- C4[infra/sealed-secrets.yaml]
- end
- subgraph PROD_APPS[prod]
- C5[apps/]
- C6[infra/]
- end
- end
-
- subgraph PROJECTS[projects]
- subgraph DEV_PROJECTS[dev]
- D1[apps-project.yaml]
- D2[infra-project.yaml]
- end
- subgraph PROD_PROJECTS[prod]
- D3[apps-project.yaml]
- D4[infra-project.yaml]
- end
- end
- end
-```
-
-### 4. 이전 구조 대비 변경점
-- Argo CD 선언을 `argocd/applications//`와 `argocd/projects/` 구조로 재배치했습니다.
-- `auth-server`와 `api-server`는 `dev/apps` 아래로, `platform`과 `sealed-secrets`는 `dev/infra` 아래로 나눠 목적별 경계를 디렉터리에서 바로 보이게 했습니다.
-- 기존 `auth-dev`와 `api-dev` AppProject는 `apps-dev` 하나로 통합하고, `platform`과 `sealed-secrets`는 `infra-dev` 프로젝트로 정리했습니다.
-- `prod`는 아직 실제 Application이 없지만, `applications/prod`와 `projects/prod` 골격을 미리 만들어 이후 추가 위치를 고정했습니다.
-- `argocd/README.md`를 추가해 이 구조 규칙을 디렉터리 안에서도 바로 확인할 수 있게 했습니다.
-
-### 5. 해결된 내용
-- 이제 Argo CD 선언만 보더라도 환경별 구분과 성격별 구분이 동시에 드러나서 탐색 비용이 줄었습니다.
-- 서비스 애플리케이션과 공용 인프라가 각자 어떤 AppProject를 쓰는지 일관되게 정리되어 관리 포인트가 단순해졌습니다.
-- `prod` 확장 시 새 파일을 어디에 둬야 하는지 미리 정해져 있어, 다음 변경에서도 구조를 다시 흔들 필요가 없어졌습니다.
-- `argocd` 자체도 README 기반의 누적 관리 대상이 되면서, 구조 변경 이유를 README와 디렉터리 문서에서 함께 추적할 수 있게 됐습니다.
-
-## Cycle 3
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- A[README 소개]
- B[README 작성 원칙]
- C[Cycle 1]
- D[Cycle 2]
-
- A --> B --> C --> D
-```
-
-### 2. 문제점
-- 현재 운영 중인 dev 아키텍처를 README 최상단에서 바로 볼 수 있는 기준 그림이 없었습니다.
-- 변경 이력은 누적되고 있었지만, 최신 구조를 한 번에 확인하려면 여러 cycle을 직접 읽어야 했습니다.
-- README의 누적 기록 규칙만 있고, `최신 아키텍처는 어디를 기준으로 볼지`에 대한 별도 원칙이 없었습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- A[README 소개]
- B[현재 최신 Dev 아키텍처]
- C[README 작성 원칙]
- D[Cycle 1]
- E[Cycle 2]
- F[Cycle 3]
-
- A --> B --> C --> D --> E --> F
-```
-
-### 4. 이전 구조 대비 변경점
-- README 최상단에 현재 기준의 **최신 dev 아키텍처**를 `mermaid`로 추가했습니다.
-- 최상단 아키텍처는 항상 최신 상태로 갱신하고, 이전 구조 변화는 cycle로 누적 기록한다는 규칙을 명시했습니다.
-- 현재 단계에서는 요청하신 대로 `prod`는 제외하고 `dev` 운영 구조만 아키텍처에 반영했습니다.
-- 아키텍처 그림 안에는 Argo CD project/application, GitOps manifest 경로, dev cluster 주요 namespace와 런타임 의존 관계를 함께 드러내도록 정리했습니다.
-
-### 5. 해결된 내용
-- 이제 README를 열면 가장 먼저 현재 dev 운영 구조를 확인할 수 있어 최신 상태 파악이 훨씬 빨라졌습니다.
-- 최신 구조와 변경 이력을 분리해, 상단은 현재 기준점으로 쓰고 하단 cycle은 히스토리로 쓰는 역할이 명확해졌습니다.
-- 이후 dev 아키텍처가 바뀌더라도 어떤 내용을 갱신하고 어떤 내용을 누적해야 하는지 README 규칙만 보고 바로 따라갈 수 있게 됐습니다.
-
-## Cycle 4
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph APP_REPOS[Application Repos]
- A1[CI builds image]
- end
-
- subgraph GITOPS_BEFORE[Project-Auth-GitOps]
- B1[apps/*/overlays/*/kustomization.yaml]
- B2[argocd/dev/*]
- end
-
- APP_REPOS -. image tag info .-> B1
-```
-
-### 2. 문제점
-- GitOps repo가 CD 중심 저장소라는 역할은 정리됐지만, 이미지 태그를 **어떤 진입점으로 갱신할지**가 이 저장소 안에 아직 명시돼 있지 않았습니다.
-- 앱 repo가 이미지를 push한 뒤 GitOps repo를 어떻게 업데이트해야 하는지 표준 스크립트나 workflow가 없어, 저장소마다 방식이 달라질 수 있었습니다.
-- README에도 이 저장소가 `CI`가 아니라 `CD`를 담당한다는 운영 원칙과 실제 반영 흐름이 구조적으로 정리돼 있지 않았습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph APP_REPOS[Application Repos]
- A1[Project-Auth-Server CI]
- A2[Project-Api-Server CI]
- end
-
- subgraph GITOPS_AFTER[Project-Auth-GitOps]
- B1[.github/workflows/update-image-tag.yaml]
- B2[scripts/update-image-tag.sh]
- B3[apps/*/overlays/*/kustomization.yaml]
- B4[argocd/dev/*]
- end
-
- A1 -->|new image tag| B1
- A2 -->|new image tag| B1
- B1 --> B2
- B2 --> B3
- B4 --> B3
-```
-
-### 4. 이전 구조 대비 변경점
-- GitOps repo에 이미지 태그 갱신용 스크립트 `scripts/update-image-tag.sh`를 추가했습니다.
-- GitOps repo 내부에서 직접 태그 갱신 commit/push를 수행할 수 있도록 `.github/workflows/update-image-tag.yaml` workflow를 추가했습니다.
-- workflow는 `workflow_dispatch`와 `repository_dispatch` 둘 다 받을 수 있게 구성해, 수동 실행과 앱 repo CI 연동 둘 다 가능하도록 했습니다.
-- README 최상단 dev 아키텍처에도 앱 repo CI에서 GitOps repo로 태그가 반영되는 흐름을 함께 반영했습니다.
-- README에 이 저장소의 역할과 현재 CD 반영 흐름을 별도 섹션으로 정리했습니다.
-
-### 5. 해결된 내용
-- 이제 이 저장소 안에 `이미지 태그 업데이트`를 수행하는 공식 진입점이 생겨, CD 반영 방식이 문서와 파일 기준으로 일치하게 되었습니다.
-- 앱 repo는 자기 저장소의 manifest를 다시 수정하지 않고, GitOps repo를 갱신하는 방식으로 역할이 명확히 분리되었습니다.
-- 이후 앱 repo CI는 새 이미지 태그만 전달하면 되고, 실제 배포 반영은 GitOps repo와 Argo CD 흐름 안에서 일어나도록 정리되었습니다.
-
-## Cycle 5
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph APPS_BEFORE[apps]
- A1[auth-server/base]
- A2[auth-server/overlays/dev]
- A3[auth-server/overlays/dev/platform]
- A4[auth-server/overlays/prod]
- B1[api-server/base]
- B2[api-server/overlays/dev]
- B3[api-server/overlays/prod]
- end
-```
-
-### 2. 문제점
-- `platform`이 `apps/auth-server` 하위에 있어, 앱 배포와 공용 인프라 배포의 책임 경계가 디렉터리 구조상 섞여 있었습니다.
-- `postgres`, `keycloak`, `vault`는 `auth-server`의 일부라기보다 공용 infra인데도 앱 overlay에 포함돼 있어 탐색과 확장이 불편했습니다.
-- `dev`와 `prod`를 분리할 때도 `platform`이 앱 트리 안에 있으면 인프라 확장 경로가 일관되지 않았습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph APPS_AFTER[apps]
- A1[auth-server/base]
- A2[auth-server/overlays/dev]
- A3[auth-server/overlays/prod]
- B1[api-server/base]
- B2[api-server/overlays/dev]
- B3[api-server/overlays/prod]
- end
-
- subgraph INFRA_AFTER[infra]
- C1[platform/base]
- C2[platform/overlays/dev]
- C3[platform/overlays/prod]
- end
-```
-
-### 4. 이전 구조 대비 변경점
-- `apps/auth-server/overlays/dev/platform`에 있던 리소스를 `infra/platform/base`와 `infra/platform/overlays/dev`로 분리했습니다.
-- `postgres`, `keycloak`, `vault` 워크로드와 공통 생성 파일은 `infra/platform/base`로 옮기고, namespace/configmap/sealed secret은 `infra/platform/overlays/dev`로 분리했습니다.
-- `infra/platform/overlays/prod`도 함께 추가해 `platform-prod` namespace와 prod용 config skeleton을 둘 수 있게 했습니다.
-- Argo CD `platform-dev` Application의 source path를 새 infra 경로로 변경했습니다.
-- README 최상단 최신 dev 아키텍처도 `apps`와 `infra`가 분리된 현재 구조 기준으로 갱신했습니다.
-
-### 5. 해결된 내용
-- 이제 `platform`은 앱 하위 부속이 아니라 독립된 infra 영역으로 보이기 때문에 구조 해석이 훨씬 자연스러워졌습니다.
-- 앱 배포 경로와 인프라 배포 경로가 분리되어, 이후 `infra` 확장이나 세분화로 이어가기가 쉬워졌습니다.
-- `dev`뿐 아니라 `prod`도 같은 `infra/platform/base -> overlays/` 패턴으로 관리할 준비가 갖춰졌습니다.
-
-## Cycle 6
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[GitOps secret lifecycle]
- A1[auth-server SealedSecret]
- A2[platform SealedSecret]
- A3[app/platform pod env from Secret]
- A4[sealed-secrets-controller]
- end
-
- A1 --> A3
- A2 --> A3
- A4 --> A1
- A4 --> A2
-```
-
-### 2. 문제점
-- `SealedSecret`으로 평문을 Git에 직접 넣지는 않았지만, secret source 자체가 여전히 GitOps 저장소 안의 정적 파일이었습니다.
-- secret rotation과 변경 이력이 결국 Git commit 중심이 되어, 운영형 secret lifecycle이라고 보기 어려웠습니다.
-- `auth-server`는 Vault Transit을 사용하면서도 접근 토큰을 정적 Kubernetes Secret으로 주입받고 있어 Kubernetes auth 기반 접근으로 전환되지 못했습니다.
-- `postgres`, `keycloak`도 `platform-secret` 하나에 묶인 채 정적 secret에 의존하고 있어, 역할별 최소 권한 분리가 어려웠습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[GitOps secret lifecycle]
- B1["vault-bootstrap Secret
manual / out-of-git"]
- B2[Vault KV kv/dev/platform/postgres/*]
- B3[Vault KV kv/dev/platform/keycloak/*]
- B4[Vault Kubernetes auth roles per workload]
- B5[Vault Agent Injector]
- B6[auth-server / auth-db-migration / postgres / keycloak / keycloak-client-sync]
- B7[ghcr-regcred SealedSecret only]
- end
-
- B1 --> B2
- B1 --> B3
- B1 --> B4
- B1 --> B5
- B5 --> B6
- B6 --> B4
- B4 --> B2
- B4 --> B3
-```
-
-### 4. 이전 구조 대비 변경점
-- `auth-server-secret.sealedsecret.yaml`과 `platform-secret.sealedsecret.yaml`을 제거하고, runtime secret source를 Vault KV로 전환했습니다.
-- HashiCorp 공식 Helm chart를 사용하는 `vault-agent-injector` Argo CD Application을 추가했습니다.
-- `auth-server`, `auth-db-migration`, `postgres`, `keycloak`, `keycloak-client-sync`에 Vault Agent Injector annotation을 적용하고 Kubernetes auth role 기반으로 secret을 주입받도록 변경했습니다.
-- runtime KV path를 `platform/postgres/superuser`, `platform/postgres/auth-server`, `platform/postgres/keycloak`, `platform/keycloak/bootstrap-admin`, `platform/keycloak/client-auth-server`처럼 목적/소유권 기준으로 세분화했습니다.
-- `auth-db-migration`과 `keycloak-client-sync`를 전용 ServiceAccount와 Vault role로 분리해 app/runtime 권한을 job과 분리했습니다.
-- base/prod 매니페스트의 Kubernetes Secret 계약도 `postgres-superuser-credentials`, `postgres-auth-server-credentials`, `postgres-keycloak-credentials`, `keycloak-bootstrap-admin`, `keycloak-client-auth-server`처럼 목적별 이름으로 분해했습니다.
-- `vault-bootstrap`은 Git에 넣지 않는 수동 bootstrap secret으로 분리하고, Vault server는 부팅 시 Kubernetes auth/policy/role을 자동 구성하도록 바꿨습니다.
-- 현재 범위는 dev 운영 환경이므로 Vault role/policy 이름도 `*-dev` 기준으로만 구성했습니다.
-- `ghcr-regcred`는 image pull secret 특성상 Injector로 대체할 수 없어서 SealedSecret으로 유지했습니다.
-
-### 5. 해결된 내용
-- 이제 앱/플랫폼 runtime secret의 기준점이 Git의 암호화 파일이 아니라 Vault가 되어, secret lifecycle이 Git commit 중심 구조에서 벗어났습니다.
-- Vault Kubernetes auth와 role 분리를 통해 `auth-server`, `auth-db-migration`, `postgres`, `keycloak`, `keycloak-client-sync`가 각자 필요한 범위만 읽도록 최소 권한 구조를 만들었습니다.
-- 정적 Kubernetes Secret 계약도 blob 두세 개 대신 목적별 credential 단위로 나뉘어, rotation과 접근 제어 범위를 더 좁힐 수 있게 됐습니다.
-- `auth-server`는 Injector가 제공한 Vault token file을 통해 Vault Transit을 계속 사용할 수 있게 되어, 정적 Vault token SealedSecret 없이도 동작할 기반이 생겼습니다.
-- 현재 구조에서 Git에 남는 비밀 항목은 bootstrap과 image pull 같은 예외 케이스로 좁혀졌고, 운영 secret 흐름과 예외 secret 흐름을 구분할 수 있게 됐습니다.
-
-## Cycle 7
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Vault bootstrap in cluster]
- A1[vault-bootstrap Secret]
- A2[vault postStart bootstrap script]
- A3[policies mounted by ConfigMap]
- A4[Vault dev mode]
- end
-
- A1 --> A2
- A3 --> A2
- A2 --> A4
-```
-
-### 2. 문제점
-- root token이 Kubernetes Secret 형태로 클러스터 안에 남아 있어, 운영자가 원한 `root token out of cluster` 조건을 만족하지 못했습니다.
-- Vault policy/role bootstrap이 pod lifecycle에 묶여 있어, 초기화 작업이 GitOps 런타임과 섞여 있었습니다.
-- Vault server가 `-dev` 모드로 실행되고 있어, 수동 init/unseal과 root token revoke 기반 운영 절차를 적용할 수 없었습니다.
-- bootstrap 절차와 KV 입력 절차가 overlay 파일 안에 섞여 있어, 실제 운영 runbook과 배포 manifest의 경계가 불분명했습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Vault bootstrap outside cluster]
- B1["Vault server
raft + PVC"]
- B2[scripts/vault/dev/bootstrap-runbook.sh]
- B3[runbooks/vault/dev/policies/*.hcl]
- B4[runbooks and scripts]
- B5["root token
operator local only"]
- B6[platform-admin-dev token]
- end
-
- B5 --> B2
- B3 --> B2
- B2 --> B1
- B2 --> B6
- B6 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- Vault bootstrap용 `vault-bootstrap` Kubernetes Secret과 in-cluster `postStart` bootstrap 흐름을 제거했습니다.
-- Vault server는 `-dev` 모드 대신 config file 기반 단일-node raft 저장소와 PVC를 사용하도록 변경했습니다.
-- Vault policy와 bootstrap 로직을 GitOps overlay 밖의 `runbooks/vault/dev/`로 이동해, 운영자가 클러스터 밖에서 직접 실행하는 구조로 바꿨습니다.
-- `bootstrap-runbook.sh`는 root token으로 1회 bootstrap을 수행한 뒤 `platform-admin-dev` orphan token을 만들고 root token revoke까지 처리하도록 바꿨습니다.
-- KV 값 입력 예시도 overlay에서 제거하고 runbook 디렉터리로 이동시켜, manifest와 운영 절차를 분리했습니다.
-
-### 5. 해결된 내용
-- 이제 root token이 Kubernetes 안에 저장되지 않고, 초기 bootstrap에만 클러스터 밖에서 사용되도록 구조가 정리되었습니다.
-- Vault bootstrap이 pod 기동 과정과 분리되어, GitOps manifest는 런타임 배포에만 집중하고 초기 운영 절차는 runbook으로 분리되었습니다.
-- Vault 운영 흐름이 `init/unseal -> bootstrap -> admin token 발급 -> root revoke` 순서로 명확해져 dev 운영 환경 기준에 더 가까워졌습니다.
-- 이후에는 `platform-admin-dev` 같은 제한된 운영 토큰으로 KV 갱신과 정책 보조 작업을 할 수 있어, root token을 상시 들고 있을 필요가 없어졌습니다.
-
-## Cycle 8
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Dev Vault automation]
- A1[Operator runbook only]
- A2[manual bootstrap script]
- A3[manual seed input]
- A4[Argo CD apps applied separately]
- end
-
- A1 --> A2 --> A3 --> A4
-```
-
-### 2. 문제점
-- runbook만으로는 dev Vault KV 입력과 auth/policy/role reconcile이 계속 운영자 수동 절차에 묶여 있었습니다.
-- GitOps repo 안에 자동 파이프라인이 없어, KV 준비와 Argo CD dev Application 적용 순서를 일관되게 맞추기 어려웠습니다.
-- 현재 단일 Vault 구조에서는 진짜 Transit auto-unseal을 바로 적용할 수 없는데, 문서상으로는 그 경계가 충분히 드러나지 않았습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Dev Vault automation]
- B1[self-hosted runner]
- B2[vault-dev-reconcile workflow]
- B3[ensure-unsealed.sh]
- B4[reconcile.sh]
- B5[populate-kv.sh]
- B6[apply-argocd-dev-*.sh]
- B7[runbooks only for first bootstrap / recovery]
- end
-
- B1 --> B2
- B2 --> B3
- B3 --> B4
- B4 --> B5
- B5 --> B6
- B7 -. fallback .-> B2
-```
-
-### 4. 이전 구조 대비 변경점
-- self-hosted runner 전용 workflow `.github/workflows/vault-dev-reconcile.yaml` 을 추가했습니다.
-- workflow는 당시 CI secret store에 저장된 비밀값을 사용해 unseal/reconcile/populate/apply를 자동 수행했습니다.
-- 기존 runbook의 정책과 절차를 재사용할 수 있도록 `scripts/vault/dev/` 아래에 automation용 스크립트를 분리했습니다.
-- README와 runbook에 자동화에 필요한 CI secret 목록과 현재 자동화 범위를 명시했습니다.
-- Transit auto-unseal은 적용했다고 표기하지 않고, 별도 unseal provider Vault/HSM/KMS가 필요한 후속 아키텍처 작업임을 분명히 남겼습니다.
-
-### 5. 해결된 내용
-- 이제 dev 환경에서는 Vault KV 준비와 Argo CD dev 정의 적용이 self-hosted runner workflow로 자동 수행될 수 있게 되었습니다.
-- 운영자는 최초 bootstrap 또는 복구 시에만 runbook을 보고 개입하면 되고, 평소 dev reconcile은 CI secret store 기반 자동화로 넘길 수 있습니다.
-- 현재 구조에서 자동화된 부분과 아직 별도 아키텍처가 필요한 부분(Transit auto-unseal)이 명확히 분리되어, 다음 변경 방향을 혼동하지 않게 됐습니다.
-
-## Cycle 9
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Single Vault dev]
- A1[workload Vault only]
- A2[CI secret store based unseal]
- A3[KV + Transit JWT in same Vault]
- A4[no dedicated unseal provider]
- end
-
- A2 --> A1
- A3 --> A1
-```
-
-### 2. 문제점
-- 단일 Vault 구조에서는 runtime secret은 Vault로 옮길 수 있어도, Vault 서버 자체의 unseal trust boundary는 여전히 CI secret store에 크게 의존했습니다.
-- `root token 직접 사용 금지`, `bootstrap 최소화`, `trust boundary를 Vault 쪽으로 이동` 같은 현업형 dev 운영 방향을 만족시키려면 별도 unseal provider가 필요했습니다.
-- README와 자동화 흐름도 아직 단일 Vault 기준 설명이 남아 있어, 실제 운영 구조와 설명이 어긋날 위험이 있었습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Two Vault dev]
- B1[vault-transit provider]
- B2[workload Vault]
- B3[Vault Agent Injector]
- B4[CI runner automation]
- B5[runbooks for first bootstrap]
- end
-
- B1 -->|transit auto-unseal| B2
- B2 -->|KV + Kubernetes auth + Transit JWT| B3
- B4 --> B1
- B4 --> B2
- B5 --> B1
- B5 --> B2
-```
-
-### 4. 이전 구조 대비 변경점
-- `infra/vault-transit/overlays/dev` 와 `argocd/applications/dev/infra/vault-transit.yaml` 을 추가해 unseal provider Vault를 별도 infra로 분리했습니다.
-- workload Vault config에 transit seal stanza를 추가하고, `vault-transit-seal` 최소 권한 token으로 auto-unseal 하도록 변경했습니다.
-- self-hosted runner workflow는 provider bootstrap -> workload reconcile -> KV populate -> app apply 순서로 재구성했습니다.
-- `scripts/vault-transit/dev/` 와 `runbooks/vault-transit/dev/` 를 추가해 provider Vault 전용 bootstrap/policy 경로를 분리했습니다.
-- README 최상단 최신 아키텍처, secret lifecycle, auto-unseal 설명을 2-Vault 구조 기준으로 갱신했습니다.
-
-### 5. 해결된 내용
-- 이제 workload Vault의 unseal trust boundary가 단순 CI secret store 의존에서 `vault-transit` provider Vault 기반 구조로 한 단계 올라갔습니다.
-- runtime secret, JWT transit signing, workload Vault 운영, unseal provider 역할이 분리되어 현업형 dev 운영 방향에 더 가까워졌습니다.
-- 단일 Vault보다 운영 난이도는 높아졌지만, root token 직접 사용 최소화와 운영 신뢰 경계 분리 측면에서는 더 나은 dev 구조를 갖추게 됐습니다.
-
-## Cycle 10
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[2-Vault but workflow secrets remain]
- A1[CI secret store]
- A2[provider Vault]
- A3[workload Vault]
- end
-
- A1 -->|app secret values + bootstrap tokens| A3
- A1 -->|provider token| A2
-```
-
-### 2. 문제점
-- Vault를 2개로 나눴어도, workflow에 앱 비밀값과 workload Vault bootstrap token이 남아 있으면 여전히 GitOps CI가 secret source처럼 보일 수 있었습니다.
-- 다른 앱 repo workflow에는 없는 민감값이 이 repo workflow에만 남아 있어, 운영 구조 일관성이 떨어졌습니다.
-- 목표였던 `비밀 배포의 신뢰 경계가 SealedSecret/CI가 아니라 Vault에 있어야 한다`는 방향이 완전히 충족되지 않았습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Provider Vault as source of truth]
- B1[CI secret store]
- B2[vault-transit provider Vault]
- B3[workload Vault]
- end
-
- B1 -->|provider unseal/bootstrap only| B2
- B2 -->|seed values + workload bootstrap token| B3
-```
-
-### 4. 이전 구조 대비 변경점
-- `vault-dev-reconcile` workflow에서 앱 비밀값과 workload Vault bootstrap token을 제거했습니다.
-- `scripts/vault/dev/populate-kv.sh` 와 `scripts/vault/dev/reconcile.sh` 는 provider Vault KV에서 값을 읽어 workload Vault에 반영하도록 변경했습니다.
-- provider Vault에 workload seed 값을 넣는 실행 스크립트 `scripts/vault-transit/dev/populate-workload-seeds.sh` 와 참고용 `runbooks/vault-transit/dev/populate-workload-seeds.example.sh` 를 분리했습니다.
-- README와 runbook에서 CI secret store에는 provider Vault 접근용 최소값만 남고, 실제 workload secret source는 provider Vault라는 점을 명시했습니다.
-
-### 5. 해결된 내용
-- 이제 workflow는 orchestration만 담당하고, 실제 앱/플랫폼 secret 값은 provider Vault에서 workload Vault로 흘러가는 구조가 되었습니다.
-- CI secret store에 남는 민감값 범위가 줄어들어, `Vault 안에 secret source of truth를 두자`는 목표에 더 가까워졌습니다.
-- 다른 앱 repo 기준으로 봐도 이 저장소만 workflow에 앱 비밀값을 직접 쥐고 있던 불균형이 해소되었습니다.
-
-## Cycle 11
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Infra layout]
- A1[infra/platform = postgres + keycloak + workload vault]
- A2[infra/vault-transit = transit provider]
- end
-```
-
-### 2. 문제점
-- `vault-transit` 만 따로 빠져 있고 workload Vault는 `platform` 안에 남아 있어, infra 경계가 namespace/역할 기준으로 일관되지 않았습니다.
-- `platform` 이라는 이름만 보면 postgres/keycloak 묶음으로 이해되는데, 여기에 workload Vault까지 들어 있어 해석이 애매했습니다.
-- 구조가 애매하면 문서와 운영 흐름을 볼 때도 `platform` 과 `vault` 의 책임이 섞여 보이기 쉬웠습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Infra layout]
- A1[infra/platform = postgres + keycloak]
- A2[infra/vault = workload vault]
- A3[infra/vault-transit = transit provider]
- end
-```
-
-### 4. 이전 구조 대비 변경점
-- workload Vault 리소스를 `infra/platform` 에서 분리해 `infra/vault/base|overlays` 로 이동했습니다.
-- Argo CD infra app도 `vault-transit`, `vault`, `platform` 3개로 역할이 드러나도록 나눴습니다.
-- workload Vault service 주소를 `vault.vault.svc.cluster.local` 기준으로 정리하고, 관련 config/script/document를 모두 새 namespace 기준으로 갱신했습니다.
-- `platform` 은 이제 postgres/keycloak 영역만 담당하고, `vault` 는 workload secret/runtime auth/transit JWT를 담당하도록 구조를 고정했습니다.
-
-### 5. 해결된 내용
-- 이제 infra 디렉터리와 namespace가 역할 기준으로 일치해, 구조 해석과 운영 설명이 훨씬 자연스러워졌습니다.
-- `platform`, `vault`, `vault-transit` 이 각각 무엇을 위한 스택인지 경로만 봐도 바로 드러납니다.
-- 이후 더 깊게 파고들 때도 어떤 변경이 어느 스택의 책임인지 구분하기 쉬워졌습니다.
-
-## Cycle 12
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Internal-only dev app network]
- A1[auth-server ClusterIP]
- A2[api-server ClusterIP]
- A3[keycloak ClusterIP]
- A4[cluster-local URLs only]
- end
-
- A4 --> A1
- A4 --> A2
- A4 --> A3
-```
-
-### 2. 문제점
-- 앱 접근 경로가 사실상 cluster 내부 DNS에만 묶여 있어, 브라우저 callback/redirect 를 거는 dev 플로우를 노트북 관점에서 설명하기 어려웠습니다.
-- `auth-server`, `api-server`, `keycloak` 이 모두 포트가 드러난 내부 URL에 결합돼 있어, north-south 진입점을 붙이기 전제도 약했습니다.
-- 리뷰에서 지적한 `연결만 되면 되는 구조` 에서 최소한의 ingress 경계와 외부 접근 경로가 부족했습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Ingress-backed dev app network]
- B1[traefik ingress]
- B2[auth-server ClusterIP:80]
- B3[api-server ClusterIP:80]
- B4[keycloak ClusterIP:80]
- B5[local hosts mapping on operator laptop]
- end
-
- B5 --> B1
- B1 --> B2
- B1 --> B3
- B1 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- `apps/auth-server/overlays/dev`, `apps/api-server/overlays/dev`, `infra/platform/overlays/dev` 에 `Ingress` 를 추가해 north-south 진입점을 만들었습니다.
-- `auth-server`, `api-server`, `keycloak` 서비스 포트를 각각 `80 -> targetPort` 형태로 정리해 내부/외부에서 같은 호스트 표기를 쓰기 쉽게 맞췄습니다.
-- dev 설정의 issuer/base URL 에서 `:8080/:8081/:8082` 포트 결합을 제거하고, ingress 가능한 host 기준으로 정리했습니다.
-- 노트북 dev 한계를 감안해 별도 split-horizon DNS 대신 현재 서비스 FQDN 을 ingress host로 재사용하고, 운영자 로컬 `hosts` 매핑으로 외부 접근을 여는 절충안을 택했습니다.
-
-### 5. 해결된 내용
-- 이제 dev도 최소한 `ClusterIP 뒤 Ingress` 구조가 되어, callback/redirect 가 필요한 앱 접근 경로를 north-south 관점에서 설명할 수 있게 됐습니다.
-- 앱 설정이 내부 포트에 덜 결합돼, 이후 별도 dev 사설 도메인을 붙일 때도 변경 폭이 줄어듭니다.
-- namespace 간 통신 제한은 이후 사이클에서 더 세분화할 수 있도록 기반만 먼저 깔았습니다.
-
-## Cycle 13
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[HTTP ingress only]
- A1[auth/api/keycloak ingress]
- A2[internal service DNS]
- A3[no namespace traffic policy]
- end
-
- A1 --> A2
- A3 --> A2
-```
-
-### 2. 문제점
-- public host와 internal host가 완전히 분리되지 않아, dev 기준 public 경로를 일관되게 설명하기 어려웠습니다.
-- east-west 제한이 전혀 없으면, namespace를 나눠도 실제 통신 경계가 거의 없는 상태와 다르지 않았습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[HTTP + east-west guardrail]
- B1[public ExternalName host]
- B2[traefik web ingress]
- B3[namespace NetworkPolicy allowlist]
- end
-
- B1 --> B2
- B3 --> B2
-```
-
-### 4. 이전 구조 대비 변경점
-- `auth-public`, `api-public`, `keycloak-public` `ExternalName` 서비스를 추가해 cluster 내부에서도 public host를 `traefik` 경유로 해석할 수 있게 했습니다.
-- `Ingress` 는 `web` entrypoint 기반의 HTTP 경로로 단순화했습니다.
-- `keycloak` 은 public hostname과 proxy header를 인지하도록 패치했습니다.
-- `auth-dev`, `api-dev`, `platform` 에는 `default deny + allowlist` `NetworkPolicy` 를 추가해 ingress, DNS, Vault, Postgres, Traefik 경로만 열어두었습니다.
-
-### 5. 해결된 내용
-- 이제 dev도 north-south 경로를 public host 하나로 일관되게 쓸 수 있습니다.
-- namespace 분리가 단순 디렉터리/리소스 분리만이 아니라 실제 통신 허용 범위로도 반영되기 시작했습니다.
-
-## Cycle 14
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Ingress bootstrap leftovers]
- A1[plain Secret in Git]
- A2[vault namespace out of policy scope]
- A3[local access steps undocumented]
- end
-```
-
-### 2. 문제점
-- ingress 관련 secret이 일반 Secret 평문으로 repo에 남아 있으면 Git에 올릴 수 있는 상태라고 보기 어려웠습니다.
-- `vault` namespace는 가장 민감한 통신 경계 중 하나인데, 정책 범위에서 빠져 있으면 east-west 제한이 덜 완성된 상태였습니다.
-- 운영자 로컬 접근 절차가 문서화되지 않으면 브라우저/CLI 검증이 사람마다 달라질 수 있었습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Git-safe ingress config]
- B1[Git-safe ingress secret handling]
- B2[vault server allowlist policy]
- end
-
- B1 --> B2
-```
-
-### 4. 이전 구조 대비 변경점
-- `vault` overlay에 workload Vault ingress/egress 정책과 injector webhook ingress 정책을 추가했습니다.
-
-### 5. 해결된 내용
-- `vault` 도 최소한 서버 트래픽과 webhook ingress 경계가 정책에 반영돼, east-west 제한 범위가 더 자연스러워졌습니다.
-
-## Cycle 15
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Dev bootstrap blockers]
- A1[Argo CD repo auth missing]
- A2[self-hosted runner tool mismatch]
- A3[vault-transit raft config incomplete]
- end
-
- A1 --> A3
- A2 --> A3
-```
-
-### 2. 문제점
-- Argo CD가 `Project-Auth-GitOps` private repo를 읽지 못해 `vault-transit-dev`, `platform-dev`, `auth-server-dev`, `api-server-dev` 가 모두 `ComparisonError` 상태에 머물렀습니다.
-- self-hosted runner는 등록됐지만 `vault`, `terraform` 같은 필수 CLI가 없어 workflow가 `Validate required tools` 단계에서 바로 실패했습니다.
-- `vault-transit` deployment가 생성된 뒤에도 Vault가 `Cluster address must be set when using raft storage` 에러로 죽어 bootstrap을 진행할 수 없었습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Diagnosable bootstrap flow]
- B1[argocd repo-creds secret]
- B2[self-hosted runner with required CLIs]
- B3[vault-transit raft api/cluster addr]
- B4[repeatable troubleshooting notes]
- end
-
- B1 --> B3
- B2 --> B3
- B3 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- Argo CD GitHub 인증은 UI 대신 `repo-creds` secret으로 선언형 등록하는 절차를 사용했습니다.
-- runner 이슈는 GitHub Actions 로그만 보지 않고, runner 호스트에서 `command -v ...` 로 실제 설치 여부를 확인하는 방식으로 정리했습니다.
-- `infra/vault-transit/base/files/vault/vault.hcl` 과 `infra/vault/base/files/vault/vault.hcl` 에 `api_addr`, `cluster_addr`, `cluster_address` 를 추가하고, 두 service/deployment에 `8201` cluster 포트를 열었습니다.
-- README에 트러블슈팅 메모 규칙을 추가해, 이후에도 명령과 판단 근거를 함께 누적 기록할 수 있게 했습니다.
-
-### 5. 해결된 내용
-- Argo CD repo 인증 문제는 선언형 secret 적용 후 `vault-transit-dev` 가 `Synced` 로 전환되는 것으로 원인을 분리할 수 있게 됐습니다.
-- self-hosted runner 이슈는 "workflow 코드 문제"와 "runner 환경 문제"를 구분해서 진단하는 기준이 생겼습니다.
-- `vault-transit` CrashLoopBackOff 는 raft 설정 누락이 원인임을 로그로 확인했고, 동일 패턴이 `vault` 에 재발하지 않도록 base config까지 함께 보완했습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: `kubectl -n argocd describe application vault-transit-dev`
- 핵심 관찰값: `Failed to load target state`, `authentication required: Repository not found`
-- 판단 근거: app/project 객체는 존재하지만 repo-server가 GitHub repo를 읽지 못해 sync 이전 단계에서 실패한다고 판단했습니다.
-- 수정 또는 조치: `/tmp/argocd-github-repo-creds.yaml` 로 `argocd.argoproj.io/secret-type=repo-creds` secret을 적용하고 `argocd.argoproj.io/refresh=hard` 로 강제 refresh 했습니다.
-- 검증 명령: `kubectl -n argocd describe application vault-transit-dev`
- 검증 결과: `OperationCompleted`, `Sync Status: Synced`, `namespace/vault-transit created`
-- 재현/확인 명령: runner 호스트에서 `command -v kubectl vault jq base64 curl terraform`
- 핵심 관찰값: `vault`, `terraform` 이 비어 있었고 workflow 로그도 `vault is required on the self-hosted runner` 에서 종료됐습니다.
-- 판단 근거: job이 GitHub-hosted가 아니라 runner 로컬 셸에서 실행되므로, 해당 머신에 CLI가 실제 설치돼 있어야 한다고 판단했습니다.
-- 수정 또는 조치: runner 호스트에 HashiCorp apt repo를 추가하고 `vault`, `terraform` 을 설치했습니다.
-- 검증 명령: `vault version`, `terraform version`
-- 재현/확인 명령: `kubectl -n vault-transit rollout status deploy/vault-transit --timeout=180s`, `kubectl -n vault-transit logs deploy/vault-transit --tail=200`
- 핵심 관찰값: `CrashLoopBackOff`, `Cluster address must be set when using raft storage`
-- 판단 근거: 이미지 pull/PVC 문제는 아니고 Vault 프로세스가 raft listener 설정 부족 때문에 바로 종료된다고 판단했습니다.
-- 수정 또는 조치: `vault-transit` 와 `vault` base `vault.hcl`, deployment, service에 raft cluster 주소와 `8201` 포트를 추가했습니다.
-- 검증 명령: `kubectl kustomize infra/vault-transit/overlays/dev`, `kubectl kustomize infra/vault/overlays/dev`
-
-## Cycle 16
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Vault transit unstable startup]
- A1[raft address incomplete]
- A2[image entrypoint touching read-only config]
- A3[RollingUpdate on single PVC]
- end
-
- A1 --> A2
- A2 --> A3
-```
-
-### 2. 문제점
-- `vault-transit` application이 sync된 뒤에도 pod가 `CrashLoopBackOff` 에 빠져 bootstrap을 시작할 수 없었습니다.
-- 로그에는 `Cluster address must be set when using raft storage` 와 `Could not chown /vault/config` 가 함께 보여, 설정 누락과 기동 방식 문제가 섞여 있었습니다.
-- 단일 replica와 단일 PVC를 쓰는 `vault`/`vault-transit` 을 `RollingUpdate` 로 굴리면 old/new pod가 겹치면서 rollout 안정성이 떨어졌습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Stable single-node Vault startup]
- B1[raft api_addr + cluster_addr]
- B2[listener cluster_address + 8201 port]
- B3[copy config to /tmp before start]
- B4[Recreate deployment strategy]
- end
-
- B1 --> B2
- B2 --> B3
- B3 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- `infra/vault-transit/base/files/vault/vault.hcl` 과 `infra/vault/base/files/vault/vault.hcl` 에 `api_addr`, `cluster_addr`, listener `cluster_address` 를 추가했습니다.
-- 두 service/deployment에 `8201` cluster 포트를 추가했습니다.
-- `vault` 와 `vault-transit` deployment를 `strategy: Recreate` 로 바꿨습니다.
-- 두 deployment 모두 ConfigMap의 `vault.hcl` 을 `/tmp/vault.hcl` 로 복사한 뒤 `vault server -config=/tmp/vault.hcl` 로 실행하도록 바꿨습니다.
-
-### 5. 해결된 내용
-- raft storage 필수 설정 누락으로 인한 즉시 종료 원인을 코드에서 제거했습니다.
-- read-only ConfigMap mount와 이미지 entrypoint 충돌 가능성을 줄여, 기동 경로가 더 단순해졌습니다.
-- 단일 PVC 기반 Vault rollout에서 old/new pod 겹침을 최소화하는 방향으로 배포 전략을 정리했습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: `kubectl -n vault-transit rollout status deploy/vault-transit --timeout=180s`
- 핵심 관찰값: `deployment "vault-transit" exceeded its progress deadline`
-- 재현/확인 명령: `kubectl -n vault-transit get deploy,pods -o wide`
- 핵심 관찰값: pod가 `CrashLoopBackOff`
-- 재현/확인 명령: `kubectl -n vault-transit logs deploy/vault-transit --tail=200`
- 핵심 관찰값: `Cluster address must be set when using raft storage`, `Could not chown /vault/config`
-- 판단 근거: config 값 부족만이 아니라, Vault 이미지 기본 entrypoint와 read-only ConfigMap mount 조합도 불안정 요인이라고 판단했습니다.
-- 수정 또는 조치: raft 주소/포트 보강, `Recreate` 전략 적용, `/tmp` 복사 후 실행 방식으로 deployment를 단순화했습니다.
-- 검증 명령: `kubectl kustomize infra/vault-transit/overlays/dev`, `kubectl kustomize infra/vault/overlays/dev`
-
-## Cycle 17
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Workload Vault auth and secret convergence]
- A1[vault healthy but auth login 403]
- A2[vault egress missing]
- A3[legacy DB state vs new Vault values]
- A4[Vault Agent template newline breakage]
- end
-
- A1 --> A2
- A2 --> A3
- A3 --> A4
-```
-
-### 2. 문제점
-- `postgres`, `auth-db-migration`, `keycloak` pod의 Vault Agent가 `auth/kubernetes/login` 에서 `403 permission denied` 를 내며 secret을 못 받았습니다.
-- `vault` namespace default-deny egress 때문에 workload Vault가 Kubernetes API와 PostgreSQL에 나가지 못했습니다.
-- Vault KV 값은 최신으로 바뀌었지만 PostgreSQL PVC는 기존 사용자 비밀번호를 유지하고 있어, 앱이 주입받은 값과 DB 내부 상태가 어긋났습니다.
-- Vault Agent template의 whitespace trim 때문에 `export` 문이 줄바꿈 없이 붙어서 잘못된 env 파일이 렌더링됐습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Restored workload secret path]
- B1[workload vault kubernetes auth restored]
- B2[vault -> kubernetes api egress]
- B3[vault -> postgres egress]
- B4[KV values aligned with runtime state]
- B5[template newlines preserved]
- end
-
- B1 --> B2
- B1 --> B3
- B2 --> B4
- B3 --> B4
- B4 --> B5
-```
-
-### 4. 이전 구조 대비 변경점
-- `terraform/vault/dev` 로 workload Vault의 `auth/kubernetes`, role, database, transit 구성을 다시 reconcile 했습니다.
-- `infra/vault/overlays/dev/networkpolicy.yaml` 에 Kubernetes API egress와 PostgreSQL egress를 추가했습니다.
-- provider/workload Vault KV와 실제 PostgreSQL 사용자 상태를 다시 맞추는 절차를 수행했습니다.
-- `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml`, `infra/platform/overlays/dev/postgres.vault-patch.yaml` 에서 Vault template 줄바꿈이 유지되도록 수정했습니다.
-
-### 5. 해결된 내용
-- Workload Vault가 앱 service account JWT를 받아들여 Vault Agent 인증이 진행되기 시작했습니다.
-- `postgres` init container는 Vault Agent 인증을 통과했고, DB credential 발급 단계로 넘어갈 수 있게 됐습니다.
-- Keycloak/Auth가 읽는 injected env 파일이 shell 문법상 유효한 형태로 렌더링되기 시작했습니다.
-- 남은 앱 health 문제를 “Vault auth 실패”가 아니라 “DB credential mismatch / runtime convergence” 단계로 좁힐 수 있게 됐습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: `kubectl -n platform logs postgres-0 -c vault-agent-init --tail=80`, `kubectl -n auth-dev logs -c vault-agent-init --tail=80`
- 핵심 관찰값: `auth/kubernetes/login` 에서 `403 permission denied`
-- 재현/확인 명령: `vault auth list`, `vault read auth/kubernetes/config`, `vault read auth/kubernetes/role/`
- 핵심 관찰값: `kubernetes` auth mount가 한때 사라졌고, backend/role을 다시 복구해야 했음
-- 재현/확인 명령: `curl .../tokenreviews` with reviewer token
- 핵심 관찰값: Kubernetes `TokenReview` 자체는 성공했고, 문제를 Vault backend / network 쪽으로 좁힐 수 있었음
-- 재현/확인 명령: `kubectl -n vault exec deploy/vault -- nslookup postgres.platform.svc.cluster.local`
- 핵심 관찰값: workload Vault pod에서는 headless service 대표 이름이 `NXDOMAIN` 이었고, `postgres-0.postgres.platform.svc.cluster.local` 은 해석됨
-- 판단 근거: `vault` 가 TokenReview와 DB dynamic credential 발급을 하려면 Kubernetes API / PostgreSQL egress가 모두 필요했고, 둘 중 하나라도 막히면 downstream pod가 전부 `Init` 단계에 머문다고 판단했습니다.
-- 수정 또는 조치:
- - `infra/vault/overlays/dev/networkpolicy.yaml` 에 Kubernetes API / PostgreSQL egress 추가
- - workload Vault auth backend 재생성 및 role 재적용
- - provider/workload Vault KV와 PostgreSQL 실제 사용자 비밀번호 재정렬
- - Vault template 줄바꿈 수정
-- 검증 명령:
- - `vault read database/creds/auth-db-migration-dev`
- - `kubectl -n platform exec postgres-0 -c postgres -- psql ...`
- - `kubectl -n platform exec -c vault-agent -- cat /vault/secrets/keycloak-env`
- - `kubectl -n argocd get applications platform-dev auth-server-dev api-server-dev -o wide`
-
-## Cycle 18
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[CI local state drift]
- A1[terraform local backend]
- A2[runner cannot see existing state]
- A3[workflow AppRole lacks bootstrap privileges]
- end
-
- A1 --> A2
- A2 --> A3
-```
-
-### 2. 문제점
-- CI의 `terraform/vault-transit/dev apply` 가 매번 `Plan: 11 to add` 로 시작하며 이미 존재하는 `kv/`, `transit/`, `approle` 을 다시 만들려 했습니다.
-- `vault-transit` workflow AppRole 토큰은 기존 리소스 reconcile 용이지, 최초 bootstrap 수준의 `sys/auth/*` / `auth/token/create` 권한까지 갖지 않아 `403 permission denied` 가 났습니다.
-- 원인은 runner가 `vault-transit-dev.tfstate` 를 못 보고 local backend state 없이 실행되고 있었기 때문이었습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Fail-fast CI reconcile]
- B1[persistent terraform state path]
- B2[init -reconfigure with explicit backend path]
- B3[empty/missing state guard]
- B4[workflow token only reconciles existing resources]
- end
-
- B1 --> B2
- B2 --> B3
- B3 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- `.github/workflows/vault-dev-reconcile.yaml` 에 `TF_STATE_DIR` 을 추가해 runner workspace의 `.terraform-state` 를 명시적으로 사용하도록 바꿨습니다.
-- `terraform init` 에 `-reconfigure -backend-config=path=...` 를 넣어 매 실행마다 state 경로를 명시적으로 고정했습니다.
-- `vault-transit` state 파일이 없거나 비어 있으면 bootstrap처럼 create 시도하지 않고, 명확한 에러로 중단하도록 가드를 추가했습니다.
-
-### 5. 해결된 내용
-- CI가 state 없이 기존 리소스를 다시 만들려다가 실패하는 패턴을 조기에 차단할 수 있게 됐습니다.
-- workflow AppRole 토큰이 “기존 리소스 reconcile” 용도라는 점을 workflow 자체에 반영해, bootstrap과 reconcile 경계를 분명히 했습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: workflow 로그에서 `path is already in use at kv/`, `path is already in use at transit/`, `permission denied` 확인
-- 재현/확인 명령: `terraform -chdir=terraform/vault-transit/dev state list`
- 핵심 관찰값: 로컬에는 state가 있지만 CI 실행 컨텍스트에서는 state를 못 보는 패턴이었음
-- 판단 근거: state가 없으니 Terraform이 기존 mount/auth backend를 신규 생성 대상으로 보고, workflow AppRole 토큰은 bootstrap 권한이 없어 403이 난다고 판단했습니다.
-- 수정 또는 조치: workflow에서 state path를 고정하고, missing/empty state일 때 fail-fast 하도록 변경했습니다.
-- 검증 명령: 다음 CI 실행에서 `Missing vault-transit Terraform state ...` 또는 정상 `state list` 통과 여부 확인
-
-## Cycle 19
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Post-bootstrap runtime drift]
- A1[vault auth backend drift]
- A2[vault egress gaps]
- A3[persisted postgres state]
- A4[malformed injected env files]
- A5[CI local state missing]
- end
-
- A1 --> A2
- A2 --> A3
- A3 --> A4
- A5 --> A1
-```
-
-### 2. 문제점
-- workload Vault는 살아 있었지만 `auth/kubernetes/login` 이 `403 permission denied` 를 내며 `postgres`, `auth-db-migration`, `keycloak` 의 Vault Agent init이 모두 막혔습니다.
-- `vault` namespace egress가 Kubernetes API와 PostgreSQL까지 열려 있지 않아 TokenReview와 DB dynamic credential 발급이 실패했습니다.
-- PostgreSQL PVC를 유지한 상태에서 Vault KV 값만 바꾸면 DB 내부 사용자 비밀번호와 새 주입값이 어긋나 Keycloak/Auth가 계속 로그인에 실패했습니다.
-- Vault Agent template에서 aggressive trim을 써서 `export` 문이 붙어 렌더링되고, 실제 injected env 파일이 shell 문법상 깨졌습니다.
-- CI는 local backend state를 못 본 채 기존 `vault-transit` 리소스를 다시 만들려 해서 `path is already in use` / `permission denied` 로 실패했습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Recoverable runtime convergence]
- B1[workload vault auth recreated]
- B2[vault -> kubernetes api egress]
- B3[vault -> postgres egress]
- B4[provider/workload KV re-aligned]
- B5[postgres/keycloak runtime state re-aligned]
- B6[template newlines preserved]
- B7[CI imports or reuses local state]
- end
-
- B1 --> B2
- B1 --> B3
- B2 --> B4
- B3 --> B5
- B4 --> B5
- B5 --> B6
- B7 --> B1
-```
-
-### 4. 이전 구조 대비 변경점
-- `terraform/vault/dev` 로 workload Vault의 `kubernetes` auth backend와 관련 role을 다시 복구했습니다.
-- `infra/vault/overlays/dev/networkpolicy.yaml` 에 Kubernetes API egress, PostgreSQL egress를 추가했습니다.
-- provider/workload Vault KV 값을 실제 persisted DB 상태와 비교해 다시 정렬하고, 필요 시 PostgreSQL 사용자 비밀번호도 직접 맞췄습니다.
-- `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml`, `infra/platform/overlays/dev/postgres.vault-patch.yaml` 의 Vault template 줄바꿈을 보존하도록 수정했습니다.
-- `.github/workflows/vault-dev-reconcile.yaml` 에 local state 경로 고정, empty state guard, import 준비 경로를 추가해 CI가 bootstrap 리소스를 새로 만들려 하지 않도록 정리했습니다.
-
-### 5. 해결된 내용
-- `postgres` Vault Agent init은 최종적으로 인증 성공까지 확인됐고, `postgres-0` 는 `2/2 Running` 으로 회복됐습니다.
-- `keycloak` 은 malformed env / DB auth 문제를 분리해서 볼 수 있게 됐고, bootstrap admin/DB credential 정합성까지 운영 관점에서 정리할 수 있게 됐습니다.
-- `auth-server` 는 DB 연결 성공과 Spring Boot 초기화 단계까지 올라와, Vault transit/JWT 쪽 남은 런타임 오류만 분리해 볼 수 있게 됐습니다.
-- CI는 최소한 state 부재를 모른 채 bootstrap을 다시 시도하는 패턴을 fail-fast 하도록 바뀌었습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: `kubectl -n platform logs postgres-0 -c vault-agent-init --tail=80`, `kubectl -n auth-dev logs -c vault-agent-init --tail=80`
- 핵심 관찰값: `auth/kubernetes/login` 에서 `403 permission denied`
-- 재현/확인 명령: `vault auth list`, `vault read auth/kubernetes/config`, `vault read auth/kubernetes/role/`
- 핵심 관찰값: `kubernetes` auth mount가 한때 없어졌고, role/config를 다시 복구해야 했음
-- 재현/확인 명령: `curl .../tokenreviews`
- 핵심 관찰값: Kubernetes `TokenReview` 는 성공하므로 SA JWT 자체보다 Vault auth/backend/network 문제로 좁혀졌음
-- 재현/확인 명령: `kubectl -n vault exec deploy/vault -- nslookup postgres.platform.svc.cluster.local`
- 핵심 관찰값: 대표 headless service 이름은 `NXDOMAIN`, `postgres-0.postgres.platform.svc.cluster.local` 은 해석 가능
-- 재현/확인 명령: `kubectl -n platform exec -c vault-agent -- cat /vault/secrets/keycloak-env`
- 핵심 관찰값: `export KC_DB_PASSWORD=...export KC_BOOTSTRAP_ADMIN_PASSWORD=...` 처럼 줄바꿈이 깨져 있었음
-- 재현/확인 명령: `kubectl -n platform exec postgres-0 -c postgres -- psql ...`, `vault kv get ...`
- 핵심 관찰값: PostgreSQL 내부 비밀번호와 Vault KV 주입값이 달라 PVC 기반 기존 상태와 새 입력값이 충돌하고 있었음
-- 재현/확인 명령: CI 로그에서 `Plan: 11 to add`, `path is already in use`, `permission denied`
- 핵심 관찰값: runner가 local backend state를 못 보고 기존 `vault-transit` 리소스를 신규 생성 대상으로 보고 있었음
-
-## Cycle 20
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Partial state drift]
- A1[self-hosted runner workspace]
- A2[local backend state exists but is incomplete]
- A3[terraform sees some resources, misses others]
- A4[workflow token tries create on missing state entries]
- end
-
- A1 --> A2
- A2 --> A3
- A3 --> A4
-```
-
-### 2. 문제점
-- CI는 더 이상 완전히 빈 state는 아니었지만, `vault-transit` state에 정책 일부만 남고 mount/auth/token 같은 핵심 리소스가 빠진 **partial state** 상태로 실행되고 있었습니다.
-- 기존 workflow는 `state list` 가 완전히 비어 있을 때만 import 하도록 되어 있어, partial state일 때는 import 분기가 전혀 돌지 않았습니다.
-- 그래서 Terraform은 빠진 리소스만 신규 생성 대상으로 보고 `kv/`, `transit/`, `approle/` 를 다시 만들려 했고, seal token 생성 단계에서는 `403 permission denied` 가 났습니다.
-- 추가로 `vault_approle_auth_backend_role_secret_id` 는 Terraform provider가 import를 지원하지 않아, partial state 복구 시 이 리소스만은 다른 managed resource처럼 state로 되살릴 수 없었습니다.
-- imported `vault_mount.kv` 는 live 상태에서 `type = "kv"` + `options.version = "2"` 로 읽히는데, 선언은 `type = "kv-v2"` 였기 때문에 partial state 복구 후에도 mount replacement가 다시 발생했습니다.
-- imported `vault_token.seal` 은 기존 accessor revoke가 필요한데, workflow 정책에 `auth/token/revoke-accessor` 권한이 빠져 있었습니다.
-- 더 근본적으로는 `vault_mount`, `vault_auth_backend`, `vault_token.seal` 같은 bootstrap 성격의 리소스를 routine CI reconcile에 계속 묶어두면, provider import/state round-trip 차이만으로도 불필요한 replacement가 반복될 수 있었습니다.
-- 같은 패턴은 이후 `terraform/vault/dev` 에도 다시 터질 수 있는 구조였습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Per-resource state reconciliation]
- B1[terraform init with explicit backend path]
- B2[state show per managed resource]
- B3[missing resources imported individually]
- B4[apply runs only after state convergence]
- end
-
- B1 --> B2
- B2 --> B3
- B3 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- `.github/workflows/vault-dev-reconcile.yaml` 의 transit 단계 import 로직을 **empty-state 전용 가드**에서 **리소스 단위 ensure/import** 방식으로 변경했습니다.
-- transit import 전에 live `vault-transit-automation-dev` policy를 현재 파일 내용으로 한 번 덮어쓰고, 새 토큰으로 다시 로그인하도록 바꿔 `auth/token/lookup-accessor` 같은 새 권한이 import 전에 즉시 반영되게 했습니다.
-- import를 지원하지 않는 `vault_approle_auth_backend_role_secret_id.workflow` 는 ensure/import 대상에서 제외하고, state에 없으면 `apply` 때 새 secret ID를 발급하도록 정리했습니다.
-- `terraform/vault-transit/dev/main.tf`, `terraform/vault/dev/main.tf` 의 KV mount 선언을 `type = "kv"` + `options = { version = "2" }` 로 바꾸고, mount에는 `prevent_destroy = true` 를 추가했습니다.
-- transit/workload KV mount에는 `ignore_changes = [type, options]` 를 추가해 import 표현 차이로 replacement가 반복되지 않게 했습니다.
-- `vault_token.seal` 은 routine CI에서 매번 rotation/replacement 하지 않도록 `lifecycle { ignore_changes = all }` 로 바꿨고, Kubernetes secret에는 `wait_for_service_account_token = true` 를 명시해 provider 기본값 드리프트를 줄였습니다.
-- `vault-transit` 에 대해 아래 리소스를 매 실행마다 `state show` 로 확인하고, 빠진 경우만 import 하도록 바꿨습니다.
- - `vault_mount.kv`, `vault_mount.transit`
- - `vault_auth_backend.approle`
- - `vault_policy.*`
- - `vault_approle_auth_backend_role.workflow`
- - `vault_approle_auth_backend_role_secret_id.workflow`
- - `vault_token.seal`
- - `vault_transit_secret_backend_key.workload_unseal`
- - `kubernetes_secret_v1.vault_transit_seal`
-- 같은 방식으로 `terraform/vault/dev` 에도 workload Vault managed resource별 import 보강을 추가했습니다.
-- `runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl` 에 `auth/token/lookup-accessor`, `auth/token/revoke-accessor` 권한을 추가해 token import/replacement cleanup이 가능하도록 보강했습니다.
-
-### 5. 해결된 내용
-- self-hosted runner가 이전 실패 실행 때문에 **부분적으로만 남은 state** 를 가지고 있어도, 다음 실행에서 빠진 리소스를 개별 import 하며 수렴할 수 있게 됐습니다.
-- CI가 “state가 조금이라도 있으니 안전하다”고 오판하고 bootstrap API를 다시 두드리는 경로를 막았습니다.
-- transit 단계뿐 아니라 workload 단계도 같은 형태의 local backend drift에 대비할 수 있게 됐습니다.
-- AppRole secret-id 리소스는 import 대신 재생성으로 수렴시키되, 기존 secret-id는 즉시 무효화되지 않으므로 현재 CI 로그인에 쓰는 값과 공존할 수 있게 했습니다.
-- KV mount 선언과 live import 결과를 맞춰 mount replacement를 제거했고, mount에는 `prevent_destroy` 를 걸어 CI가 provider/workload KV를 다시 지우지 못하게 했습니다.
-- seal token replacement가 필요한 경우에도 accessor revoke 권한이 있어 cleanup 단계까지 마칠 수 있게 했습니다.
-- bootstrap 성격의 리소스는 CI가 “계속 바꿔야 하는 대상”이 아니라 “존재를 확인하고 drift를 최소화해야 하는 대상”으로 취급하도록 방향을 바꿨습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: CI 로그에서 `Plan: 8 to add, 1 to change`, `path is already in use at kv/`, `path is already in use at approle/`, `permission denied`
- 핵심 관찰값: 완전 빈 state라면 `Plan: 11 to add` 이어야 하는데, 일부 정책만 state에 남아 있어 **partial state** 였음
-- 재현/확인 명령: `terraform -chdir=terraform/vault-transit/dev state list`
- 핵심 관찰값: 로컬 정상 state에는 11개 managed resource가 모두 있었음
-- 판단 근거: 기존 workflow는 `state list` 가 비었을 때만 import를 수행하므로, partial state에서는 import가 건너뛰어지고 빠진 리소스를 신규 생성 대상으로 보게 된다고 판단했습니다.
-- 판단 근거: `vault_approle_auth_backend_role_secret_id` 는 provider가 import 미지원이므로, 그 항목까지 import 대상으로 유지하면 partial state 복구가 그 단계에서 항상 멈춘다고 판단했습니다.
-- 판단 근거: `vault_mount.kv` plan에 `type "kv" -> "kv-v2"` replacement가 보인 것은 선언 방식 mismatch 때문이고, `vault_token.seal` 삭제 실패는 `auth/token/revoke-accessor` 권한 부재 때문이라고 판단했습니다.
-- 판단 근거: CI가 bootstrap 리소스를 계속 교체하려 들수록 state/import/provider 표현 차이의 영향을 크게 받으므로, 현업에서는 이런 리소스를 bootstrap 단계와 routine reconcile 단계로 분리하는 편이 안정적이라고 판단했습니다.
-- 수정 또는 조치:
- - workflow에 `ensure_transit_state_resource`, `ensure_workload_state_resource` 함수를 추가
- - 필요한 import ID를 accessor/path 기준으로 계산해 빠진 리소스만 import
- - transit import 전에 `vault policy write vault-transit-automation-dev ...` 후 재로그인
- - transit automation policy에 `auth/token/lookup-accessor`, `auth/token/revoke-accessor` 추가
- - import 미지원인 `vault_approle_auth_backend_role_secret_id` 는 ensure 대상에서 제외
- - transit/workload KV mount 선언을 `kv` + `options.version=2` 로 수정
- - transit/workload mount에 `prevent_destroy = true` 추가
- - transit/workload KV mount에 `ignore_changes = [type, options]` 추가
- - `vault_token.seal` 에 `ignore_changes = all` 추가
- - `kubernetes_secret_v1.vault_transit_seal` 에 `wait_for_service_account_token = true` 명시
-- 검증 명령: 다음 CI 실행에서 `Importing missing vault-transit state for ...` / `Importing missing workload-vault state for ...` 로그가 먼저 나오고, 그 뒤 `terraform apply` 가 create 대신 reconcile로 수렴하는지 확인
-
-## Cycle 21
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Mixed bootstrap + reconcile]
- A1[vault-dev-reconcile]
- A2[partial state import]
- A3[provider bootstrap path missing]
- A4[CI tries to continue with routine token]
- end
-
- A1 --> A2
- A2 --> A3
- A3 --> A4
-```
-
-### 2. 문제점
-- `vault-dev-reconcile` 가 bootstrap과 reconcile 책임을 같이 지다 보니, provider bootstrap path(`kv/dev/workload/bootstrap`) 가 없을 때도 routine workflow 안에서 해결하려는 구조였습니다.
-- 이 구조는 workflow AppRole과 local backend state 특성에 지나치게 민감했고, bootstrap 미완료/복구 상황에서 CI가 불필요하게 복잡해졌습니다.
-- 실제로 transit 단계가 통과된 뒤에도 workload bootstrap credential 부재 때문에 workflow가 중간에 실패했습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Separated bootstrap + reconcile]
- B1[manual bootstrap runbook]
- B2[privileged workload bootstrap]
- B3[vault-dev-reconcile]
- B4[routine CI reconcile only]
- end
-
- B1 --> B2
- B2 --> B3
- B3 --> B4
-```
-
-### 4. 이전 구조 대비 변경점
-- `scripts/vault/dev/bootstrap-runbook.sh`, `scripts/vault-transit/dev/bootstrap-runbook.sh` 가 `TF_STATE_DIR` override를 받아 runner/local 어디서든 같은 state 규칙으로 bootstrap 하도록 맞췄습니다.
-- `.github/workflows/vault-dev-reconcile.yaml` 은 transit reconcile 후 provider bootstrap path 존재 여부를 먼저 확인하고, 없으면 manual bootstrap runbook 으로 넘기도록 정리했습니다.
-
-### 5. 해결된 내용
-- routine CI가 bootstrap까지 억지로 끌고 가다 실패하는 구조를 끊고, “privileged bootstrap” 과 “least-privilege reconcile” 을 역할별로 분리했습니다.
-- 운영자는 bootstrap이 필요할 때만 수동 runbook을 실행하고, 평상시 CI는 bootstrap readiness 확인 뒤 안전한 reconcile만 수행하게 됐습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: CI 로그에서 `Workload Vault bootstrap AppRole is missing from provider Vault.`
- 핵심 관찰값: transit reconcile은 성공했지만 provider bootstrap path가 비어 있어 workload routine reconcile 토큰을 만들 수 없었음
-- 판단 근거: bootstrap credential 부재는 privileged bootstrap으로만 해결해야 하고, workflow AppRole 기반 reconcile 단계에서 해결하려고 하면 책임이 섞여 구조가 계속 복잡해진다고 판단했습니다.
-- 수정 또는 조치:
- - `scripts/vault/dev/bootstrap-runbook.sh`, `scripts/vault-transit/dev/bootstrap-runbook.sh` 에 `TF_STATE_DIR` 지원 추가
- - `vault-dev-reconcile.yaml` 에 manual bootstrap runbook 안내 문구 추가
-- 검증 명령:
- - 운영자 터미널에서 `scripts/vault/dev/bootstrap-runbook.sh` 실행
- - 이후 `vault-dev-reconcile` 재실행
- - `kubectl -n argocd get applications -o wide`
-
-## Cycle 22
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[CI still touched bootstrap-shaped resources]
- A1[full terraform roots in CI]
- A2[mount/auth/token drift handling]
- A3[bootstrap path dependency in reconcile]
- end
-
- A1 --> A2
- A2 --> A3
-```
-
-### 2. 문제점
-- `vault-dev-reconcile` 는 bootstrap path가 비어 있으면 명확히 멈추긴 했지만, 내부적으로는 여전히 full Terraform root와 비슷한 책임을 일부 끌고 있었습니다.
-- 현업식 권장 구조로 보려면 routine CI는 import 가능한 reconcile 리소스만 다루고, bootstrap-shaped resource는 아예 다른 루트/다른 절차로 분리되는 편이 더 안정적입니다.
-- bootstrap workflow를 GitHub에 남겨두는 것도 “privileged bootstrap은 로컬 수동” 원칙과 살짝 어긋났습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Manual bootstrap + reconcile-only CI]
- B1[terraform/vault-transit/dev]
- B2[terraform/vault/dev]
- B3[manual runbooks]
- B4[terraform/vault-transit/reconcile]
- B5[terraform/vault/reconcile]
- B6[vault-dev-reconcile workflow]
- end
-
- B3 --> B1
- B3 --> B2
- B1 --> B4
- B2 --> B5
- B4 --> B6
- B5 --> B6
-```
-
-### 4. 이전 구조 대비 변경점
-- routine CI 전용 root를 추가했습니다.
- - `terraform/vault-transit/reconcile`
- - `terraform/vault/reconcile`
-- `vault-dev-reconcile` 는 더 이상 bootstrap 전용 리소스(`mount`, `auth backend`, `secret_id`, `seal token`)를 관리하지 않고, reconcile-safe 리소스만 import/apply 합니다.
-- `vault-dev-bootstrap` workflow는 제거하고, bootstrap은 수동 runbook만 사용하도록 정리했습니다.
-
-### 5. 해결된 내용
-- CI가 bootstrap-shaped 리소스 때문에 계속 state/import/provider 차이에 흔들리던 구조를 끊었습니다.
-- routine CI는 least-privilege AppRole과 import 가능한 reconcile 리소스만 다루는 더 안정적인 경로로 수렴했습니다.
-- privileged bootstrap token은 GitHub secret store에 넣지 않고 운영자 로컬에서만 쓰는 쪽으로 구조를 정리했습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: reconcile workflow 로그에서 bootstrap path 부재 확인
- 핵심 관찰값: provider bootstrap path가 없으면 routine CI가 더 진행할 수 없음
-- 판단 근거: bootstrap과 reconcile을 코드 레벨의 Terraform root부터 분리해야 CI가 덜 흔들린다고 판단했습니다.
-- 수정 또는 조치:
- - `terraform/vault-transit/reconcile`, `terraform/vault/reconcile` 추가
- - `.github/workflows/vault-dev-bootstrap.yaml` 삭제
- - `vault-dev-reconcile.yaml` 을 reconcile-only 루트 기준으로 수정
- - runbook/README/AGENT 를 manual bootstrap 구조에 맞게 갱신
-- 검증 명령:
- - 수동 bootstrap 후 `vault-dev-reconcile` 재실행
- - CI plan에서 mount/auth backend/seal token 리소스가 더 이상 나오지 않는지 확인
-
-## Cycle 23
-
-### 1. 초기 구조
-```mermaid
-flowchart TD
- subgraph BEFORE[Wrapper-less CI]
- A1[vault-dev-reconcile YAML]
- A2[long inline bash]
- A3[full Terraform responsibility]
- A4[GitHub context mixed in bash]
- end
-
- A1 --> A2
- A2 --> A3
- A2 --> A4
-```
-
-### 2. 문제점
-- `vault-dev-reconcile.yaml` 안에 긴 bash 로직이 직접 들어 있어 테스트/리뷰/수정 포인트가 YAML과 강하게 결합돼 있었습니다.
-- routine CI가 bootstrap용 full Terraform root를 직접 만지면서 state/import 결합도가 높았습니다.
-- `update-image-tag.yaml` 는 GitHub context를 inline bash 안에서 분기 처리하고 있어 리뷰 시 변수 흐름을 파악하기 불편했습니다.
-- `for ... sleep` 폴링이 남아 있어 가시성과 타임아웃 해석이 불편했습니다.
-- 여전히 Vault 접근 경로는 `port-forward` 에 의존하므로, 네트워크 관점의 최종 권장 구조까지는 아직 가지 못했습니다.
-
-### 3. 변경 후 구조
-```mermaid
-flowchart TD
- subgraph AFTER[Thin workflow wrappers]
- B1[vault-dev-reconcile.yaml]
- B2[scripts/ci/reconcile-vault-dev.sh]
- B3[terraform/vault-transit/reconcile]
- B4[terraform/vault/reconcile]
- B5[update-image-tag.yaml]
- B6[scripts/ci/update-image-tag.sh]
- end
-
- B1 --> B2
- B2 --> B3
- B2 --> B4
- B5 --> B6
-```
-
-### 4. 이전 구조 대비 변경점
-- `scripts/ci/reconcile-vault-dev.sh` 를 추가하고, `vault-dev-reconcile.yaml` 은 단계별 wrapper(step)만 남기도록 줄였습니다.
-- routine CI가 쓰는 Terraform 루트를 `terraform/vault-transit/reconcile`, `terraform/vault/reconcile` 로 분리했습니다.
-- `kubectl wait --for=condition=available ...` 로 deployment 대기를 정리했습니다.
-- `scripts/ci/update-image-tag.sh` 를 추가하고, `update-image-tag.yaml` 은 GitHub context를 `env` 로만 전달하도록 바꿨습니다.
-- bootstrap workflow는 제거하고, bootstrap은 수동 runbook 전용으로 정리했습니다.
-- `vault-dev-reconcile.yaml` 은 `VAULT_DEV_RECONCILE_RUNS_ON`, `RECONCILE_USE_PORT_FORWARD`, `TRANSIT_VAULT_ADDR`, `WORKLOAD_VAULT_ADDR` GitHub Variables 로 실행 위치와 네트워크 방식을 바꿀 수 있게 정리했습니다.
-
-### 5. 해결된 내용
-- workflow YAML은 orchestration wrapper 역할에 집중하고, 실제 로직은 저장소 안의 테스트 가능한 스크립트로 이동했습니다.
-- routine CI가 bootstrap 리소스를 직접 다루지 않게 되어 Terraform state/import 결합도가 줄었습니다.
-- `update-image-tag` 의 변수 해석 흐름이 스크립트 기준으로 단순해져 협업/리뷰 가독성이 좋아졌습니다.
-- `kubectl wait` 기반으로 대기 로직이 조금 더 직관적으로 바뀌었습니다.
-- `scripts/ci/reconcile-vault-dev.sh` 는 `RECONCILE_USE_PORT_FORWARD=false` 와 in-cluster service URL을 주면 port-forward 없이도 실행할 수 있게 바꿔, 향후 ARC/Job 전환 시 재사용할 수 있게 했습니다.
-- 따라서 ARC 도입 시에는 workflow YAML을 다시 뜯기보다 GitHub Variables 만 바꿔 in-cluster service 경로와 runner scale set 이름으로 전환할 수 있게 됐습니다.
-
-### 6. 트러블슈팅 메모
-- 재현/확인 명령: `sed -n '1,260p' .github/workflows/vault-dev-reconcile.yaml`
- 핵심 관찰값: YAML 내부에 긴 inline bash가 남아 있으면 CI 디버깅과 변경 추적이 어려움
-- 판단 근거: GitHub Actions는 wrapper, 실제 로직은 repo의 script/terraform root가 맡는 편이 팀 협업과 유지보수에 유리하다고 판단했습니다.
-- 수정 또는 조치:
- - `scripts/ci/reconcile-vault-dev.sh` 추가
- - `scripts/ci/update-image-tag.sh` 추가
- - routine reconcile용 Terraform root 추가
- - `vault-dev-reconcile.yaml`, `update-image-tag.yaml` 을 wrapper형으로 축소
- - `reconcile-vault-dev.sh` 에 in-cluster direct access 모드(`RECONCILE_USE_PORT_FORWARD=false`) 추가
- - `vault-dev-reconcile.yaml` 에 runner label / Vault 주소 / port-forward 사용 여부를 GitHub Variables 로 주입하는 경로 추가
-- 검증 명령:
- - `git diff --check`
- - 다음 CI 실행에서 단계별 step 실패 지점이 UI에 분리되어 보이는지 확인
- - `update-image-tag` 수동 실행으로 env/입력 해석이 정상인지 확인
+`terraform-apply`는 동일한 입력과
+`APPROVE_APPLY=dev-k3s/`가 모두 있어야 실행되며 `auto-approve`를
+사용하지 않습니다.
+
+## 문서
+
+- [빈 dev 클러스터 bootstrap](docs/runbooks/dev-bootstrap.md)
+- [배포 구조](docs/architecture/deployment.md)
+- [secret trust boundary](docs/architecture/secret-trust.md)
+- [Terraform v2 state 이관](docs/runbooks/terraform-state-migration.md)
+- [Vault backup/recovery](docs/runbooks/vault-backup-restore.md)
+- [Sealed Secrets recovery](docs/runbooks/sealed-secrets-recovery.md)
+- [입문 가이드](INTERN_GUIDE.md)
diff --git a/apps/api-server/overlays/dev/configmap.yaml b/apps/api-server/overlays/dev/configmap.yaml
deleted file mode 100644
index 89c643d..0000000
--- a/apps/api-server/overlays/dev/configmap.yaml
+++ /dev/null
@@ -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
diff --git a/apps/api-server/overlays/prod/kustomization.yaml b/apps/api-server/overlays/prod/kustomization.yaml
deleted file mode 100644
index d6334b3..0000000
--- a/apps/api-server/overlays/prod/kustomization.yaml
+++ /dev/null
@@ -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
diff --git a/apps/api-server/overlays/prod/namespace.yaml b/apps/api-server/overlays/prod/namespace.yaml
deleted file mode 100644
index e400559..0000000
--- a/apps/api-server/overlays/prod/namespace.yaml
+++ /dev/null
@@ -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
diff --git a/apps/auth-server/overlays/dev/configmap.yaml b/apps/auth-server/overlays/dev/configmap.yaml
deleted file mode 100644
index 32636df..0000000
--- a/apps/auth-server/overlays/dev/configmap.yaml
+++ /dev/null
@@ -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
diff --git a/apps/auth-server/overlays/prod/kustomization.yaml b/apps/auth-server/overlays/prod/kustomization.yaml
deleted file mode 100644
index 8d80640..0000000
--- a/apps/auth-server/overlays/prod/kustomization.yaml
+++ /dev/null
@@ -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
diff --git a/apps/auth-server/overlays/prod/namespace.yaml b/apps/auth-server/overlays/prod/namespace.yaml
deleted file mode 100644
index 56deb73..0000000
--- a/apps/auth-server/overlays/prod/namespace.yaml
+++ /dev/null
@@ -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
diff --git a/argocd/README.md b/argocd/README.md
deleted file mode 100644
index 14832b6..0000000
--- a/argocd/README.md
+++ /dev/null
@@ -1,18 +0,0 @@
-## Argo CD Structure
-
-`argocd/` 디렉터리는 환경(`dev`, `prod`)과 성격(`apps`, `infra`) 기준으로 나눠 관리합니다.
-
-- `applications//apps`: 서비스 애플리케이션 선언
-- `applications//infra`: 공용 인프라/컨트롤러 선언
-- `projects//apps-project.yaml`: 서비스 애플리케이션용 AppProject
-- `projects//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 처리
diff --git a/argocd/applications/dev/infra/vault-transit.yaml b/argocd/applications/dev/infra/vault-transit.yaml
deleted file mode 100644
index ca2e7ef..0000000
--- a/argocd/applications/dev/infra/vault-transit.yaml
+++ /dev/null
@@ -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
diff --git a/argocd/applications/prod/apps/.gitkeep b/argocd/applications/prod/apps/.gitkeep
deleted file mode 100644
index 8b13789..0000000
--- a/argocd/applications/prod/apps/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/argocd/applications/prod/infra/.gitkeep b/argocd/applications/prod/infra/.gitkeep
deleted file mode 100644
index 8b13789..0000000
--- a/argocd/applications/prod/infra/.gitkeep
+++ /dev/null
@@ -1 +0,0 @@
-
diff --git a/argocd/projects/dev/infra-project.yaml b/argocd/projects/dev/infra-project.yaml
deleted file mode 100644
index e0f1214..0000000
--- a/argocd/projects/dev/infra-project.yaml
+++ /dev/null
@@ -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
diff --git a/argocd/projects/prod/apps-project.yaml b/argocd/projects/prod/apps-project.yaml
deleted file mode 100644
index bed65db..0000000
--- a/argocd/projects/prod/apps-project.yaml
+++ /dev/null
@@ -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
diff --git a/bootstrap/argocd/root-application.yaml b/bootstrap/argocd/root-application.yaml
new file mode 100644
index 0000000..f4d6af3
--- /dev/null
+++ b/bootstrap/argocd/root-application.yaml
@@ -0,0 +1,23 @@
+apiVersion: argoproj.io/v1alpha1
+kind: Application
+metadata:
+ name: project-gitops-dev-k3s
+ namespace: argocd
+spec:
+ project: default
+ source:
+ repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
+ targetRevision: main
+ path: 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
diff --git a/bootstrap/argocd/version.env b/bootstrap/argocd/version.env
new file mode 100644
index 0000000..321350a
--- /dev/null
+++ b/bootstrap/argocd/version.env
@@ -0,0 +1,2 @@
+ARGOCD_VERSION=v3.4.2
+ARGOCD_INSTALL_SHA256=69114b8c9eb48a1d08598e6f654a0869b10ae902456ea4b70796cb563760f5ec
diff --git a/clusters/dev-k3s/applications/foundation/kustomization.yaml b/clusters/dev-k3s/applications/foundation/kustomization.yaml
new file mode 100644
index 0000000..c3adfb7
--- /dev/null
+++ b/clusters/dev-k3s/applications/foundation/kustomization.yaml
@@ -0,0 +1,7 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - sealed-secrets.yaml
+ - vault.yaml
+ - vault-agent-injector.yaml
diff --git a/argocd/applications/dev/infra/sealed-secrets.yaml b/clusters/dev-k3s/applications/foundation/sealed-secrets.yaml
similarity index 52%
rename from argocd/applications/dev/infra/sealed-secrets.yaml
rename to clusters/dev-k3s/applications/foundation/sealed-secrets.yaml
index eb2adef..d20ad30 100644
--- a/argocd/applications/dev/infra/sealed-secrets.yaml
+++ b/clusters/dev-k3s/applications/foundation/sealed-secrets.yaml
@@ -3,17 +3,31 @@ kind: Application
metadata:
name: sealed-secrets-dev
namespace: argocd
+ annotations:
+ argocd.argoproj.io/sync-wave: "0"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
- project: infra-dev
+ project: cluster-addons-dev
source:
- repoURL: https://bitnami-labs.github.io/sealed-secrets
+ repoURL: https://bitnami.github.io/sealed-secrets
chart: sealed-secrets
targetRevision: 2.17.9
helm:
values: |
fullnameOverride: sealed-secrets-controller
+ keyrenewperiod: 720h
+ image:
+ repository: bitnami/sealed-secrets-controller
+ tag: "0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc"
+ resources:
+ requests:
+ cpu: 50m
+ memory: 64Mi
+ limits:
+ cpu: 250m
+ memory: 256Mi
destination:
server: https://kubernetes.default.svc
namespace: kube-system
@@ -23,11 +37,11 @@ spec:
prune: true
selfHeal: true
syncOptions:
- - CreateNamespace=true
- PruneLast=true
- - ApplyOutOfSyncOnly=true
+ - FailOnSharedResource=true
retry:
limit: 5
+ refresh: true
backoff:
duration: 5s
factor: 2
diff --git a/argocd/applications/dev/infra/vault-agent-injector.yaml b/clusters/dev-k3s/applications/foundation/vault-agent-injector.yaml
similarity index 74%
rename from argocd/applications/dev/infra/vault-agent-injector.yaml
rename to clusters/dev-k3s/applications/foundation/vault-agent-injector.yaml
index 39e925b..adfa1b0 100644
--- a/argocd/applications/dev/infra/vault-agent-injector.yaml
+++ b/clusters/dev-k3s/applications/foundation/vault-agent-injector.yaml
@@ -4,11 +4,12 @@ metadata:
name: vault-agent-injector-dev
namespace: argocd
annotations:
- argocd.argoproj.io/sync-wave: "10"
+ argocd.argoproj.io/sync-wave: "2"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
- project: infra-dev
+ project: cluster-addons-dev
source:
repoURL: https://helm.releases.hashicorp.com
chart: vault
@@ -23,6 +24,9 @@ spec:
injector:
enabled: true
authPath: auth/kubernetes
+ image:
+ repository: hashicorp/vault-k8s
+ tag: "1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"
webhook:
failurePolicy: Fail
namespaceSelector:
@@ -37,7 +41,7 @@ spec:
memory: 256Mi
agentImage:
repository: hashicorp/vault
- tag: "1.18"
+ tag: "1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"
destination:
server: https://kubernetes.default.svc
namespace: vault
@@ -47,11 +51,11 @@ spec:
prune: true
selfHeal: true
syncOptions:
- - CreateNamespace=true
- PruneLast=true
- - ApplyOutOfSyncOnly=true
+ - FailOnSharedResource=true
retry:
limit: 5
+ refresh: true
backoff:
duration: 5s
factor: 2
diff --git a/argocd/applications/dev/infra/vault.yaml b/clusters/dev-k3s/applications/foundation/vault.yaml
similarity index 63%
rename from argocd/applications/dev/infra/vault.yaml
rename to clusters/dev-k3s/applications/foundation/vault.yaml
index 6153fb9..a5a4c96 100644
--- a/argocd/applications/dev/infra/vault.yaml
+++ b/clusters/dev-k3s/applications/foundation/vault.yaml
@@ -4,15 +4,14 @@ metadata:
name: vault-dev
namespace: argocd
annotations:
- argocd.argoproj.io/sync-wave: "10"
- finalizers:
- - resources-finalizer.argocd.argoproj.io
+ argocd.argoproj.io/sync-wave: "1"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
- project: infra-dev
+ project: platform-dev
source:
- repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
+ repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
- path: infra/vault/overlays/dev
+ path: clusters/dev-k3s/manifests/vault
destination:
server: https://kubernetes.default.svc
namespace: vault
@@ -24,9 +23,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- - ApplyOutOfSyncOnly=true
+ - FailOnSharedResource=true
retry:
limit: 5
+ refresh: true
backoff:
duration: 5s
factor: 2
diff --git a/infra/vault/overlays/prod/kustomization.yaml b/clusters/dev-k3s/applications/kustomization.yaml
similarity index 57%
rename from infra/vault/overlays/prod/kustomization.yaml
rename to clusters/dev-k3s/applications/kustomization.yaml
index c7865ec..2057204 100644
--- a/infra/vault/overlays/prod/kustomization.yaml
+++ b/clusters/dev-k3s/applications/kustomization.yaml
@@ -1,8 +1,7 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
-namespace: vault-prod
-
resources:
- - ../../base
- - namespace.yaml
+ - foundation
+ - platform
+ - workloads
diff --git a/argocd/applications/dev/infra/platform.yaml b/clusters/dev-k3s/applications/platform/auth-system.yaml
similarity index 63%
rename from argocd/applications/dev/infra/platform.yaml
rename to clusters/dev-k3s/applications/platform/auth-system.yaml
index c2a25d2..821aef0 100644
--- a/argocd/applications/dev/infra/platform.yaml
+++ b/clusters/dev-k3s/applications/platform/auth-system.yaml
@@ -4,15 +4,14 @@ metadata:
name: platform-dev
namespace: argocd
annotations:
- argocd.argoproj.io/sync-wave: "20"
- finalizers:
- - resources-finalizer.argocd.argoproj.io
+ argocd.argoproj.io/sync-wave: "10"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
spec:
- project: infra-dev
+ project: platform-dev
source:
- repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
+ repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
- path: infra/platform/overlays/dev
+ path: clusters/dev-k3s/manifests/auth-system
destination:
server: https://kubernetes.default.svc
namespace: platform
@@ -24,9 +23,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- - ApplyOutOfSyncOnly=true
+ - FailOnSharedResource=true
retry:
limit: 5
+ refresh: true
backoff:
duration: 5s
factor: 2
diff --git a/infra/vault-transit/overlays/dev/kustomization.yaml b/clusters/dev-k3s/applications/platform/kustomization.yaml
similarity index 55%
rename from infra/vault-transit/overlays/dev/kustomization.yaml
rename to clusters/dev-k3s/applications/platform/kustomization.yaml
index 297af33..4098a31 100644
--- a/infra/vault-transit/overlays/dev/kustomization.yaml
+++ b/clusters/dev-k3s/applications/platform/kustomization.yaml
@@ -1,8 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
-namespace: vault-transit
-
resources:
- - ../../base
- - namespace.yaml
+ - auth-system.yaml
diff --git a/argocd/applications/dev/apps/api-server.yaml b/clusters/dev-k3s/applications/workloads/api-server.yaml
similarity index 68%
rename from argocd/applications/dev/apps/api-server.yaml
rename to clusters/dev-k3s/applications/workloads/api-server.yaml
index 99237f6..d17281b 100644
--- a/argocd/applications/dev/apps/api-server.yaml
+++ b/clusters/dev-k3s/applications/workloads/api-server.yaml
@@ -4,15 +4,16 @@ metadata:
name: api-server-dev
namespace: argocd
annotations:
- argocd.argoproj.io/sync-wave: "40"
+ argocd.argoproj.io/sync-wave: "20"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
- repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
+ repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
- path: apps/api-server/overlays/dev
+ path: clusters/dev-k3s/manifests/api-server
destination:
server: https://kubernetes.default.svc
namespace: api-dev
@@ -24,9 +25,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- - ApplyOutOfSyncOnly=true
+ - FailOnSharedResource=true
retry:
limit: 5
+ refresh: true
backoff:
duration: 5s
factor: 2
diff --git a/argocd/applications/dev/apps/auth-server.yaml b/clusters/dev-k3s/applications/workloads/auth-server.yaml
similarity index 68%
rename from argocd/applications/dev/apps/auth-server.yaml
rename to clusters/dev-k3s/applications/workloads/auth-server.yaml
index 7ca1f29..482b6ef 100644
--- a/argocd/applications/dev/apps/auth-server.yaml
+++ b/clusters/dev-k3s/applications/workloads/auth-server.yaml
@@ -4,15 +4,16 @@ metadata:
name: auth-server-dev
namespace: argocd
annotations:
- argocd.argoproj.io/sync-wave: "30"
+ argocd.argoproj.io/sync-wave: "20"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
project: apps-dev
source:
- repoURL: https://github.com/DongHyeonka/Project-Auth-GitOps
+ repoURL: https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
targetRevision: main
- path: apps/auth-server/overlays/dev
+ path: clusters/dev-k3s/manifests/auth-server
destination:
server: https://kubernetes.default.svc
namespace: auth-dev
@@ -24,9 +25,10 @@ spec:
syncOptions:
- CreateNamespace=true
- PruneLast=true
- - ApplyOutOfSyncOnly=true
+ - FailOnSharedResource=true
retry:
limit: 5
+ refresh: true
backoff:
duration: 5s
factor: 2
diff --git a/clusters/dev-k3s/applications/workloads/kustomization.yaml b/clusters/dev-k3s/applications/workloads/kustomization.yaml
new file mode 100644
index 0000000..e3acd63
--- /dev/null
+++ b/clusters/dev-k3s/applications/workloads/kustomization.yaml
@@ -0,0 +1,6 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - auth-server.yaml
+ - api-server.yaml
diff --git a/clusters/dev-k3s/kustomization.yaml b/clusters/dev-k3s/kustomization.yaml
new file mode 100644
index 0000000..d44d9ef
--- /dev/null
+++ b/clusters/dev-k3s/kustomization.yaml
@@ -0,0 +1,6 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - projects
+ - applications
diff --git a/clusters/dev-k3s/manifests/api-server/config.env b/clusters/dev-k3s/manifests/api-server/config.env
new file mode 100644
index 0000000..b1a5625
--- /dev/null
+++ b/clusters/dev-k3s/manifests/api-server/config.env
@@ -0,0 +1,2 @@
+APP_SERVER_PORT=8082
+APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI=http://auth-public.auth-dev.svc.cluster.local
diff --git a/apps/api-server/overlays/dev/ghcr-regcred.sealedsecret.yaml b/clusters/dev-k3s/manifests/api-server/ghcr-regcred.sealedsecret.yaml
similarity index 100%
rename from apps/api-server/overlays/dev/ghcr-regcred.sealedsecret.yaml
rename to clusters/dev-k3s/manifests/api-server/ghcr-regcred.sealedsecret.yaml
diff --git a/apps/api-server/overlays/dev/ingress.yaml b/clusters/dev-k3s/manifests/api-server/ingress.yaml
similarity index 91%
rename from apps/api-server/overlays/dev/ingress.yaml
rename to clusters/dev-k3s/manifests/api-server/ingress.yaml
index 504262e..5a54d33 100644
--- a/apps/api-server/overlays/dev/ingress.yaml
+++ b/clusters/dev-k3s/manifests/api-server/ingress.yaml
@@ -3,6 +3,7 @@ kind: Ingress
metadata:
name: api-server
annotations:
+ argocd.argoproj.io/sync-wave: "20"
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
diff --git a/apps/api-server/overlays/dev/kustomization.yaml b/clusters/dev-k3s/manifests/api-server/kustomization.yaml
similarity index 67%
rename from apps/api-server/overlays/dev/kustomization.yaml
rename to clusters/dev-k3s/manifests/api-server/kustomization.yaml
index 627dae5..132dc98 100644
--- a/apps/api-server/overlays/dev/kustomization.yaml
+++ b/clusters/dev-k3s/manifests/api-server/kustomization.yaml
@@ -4,16 +4,21 @@ kind: Kustomization
namespace: api-dev
resources:
- - ../../base
+ - ../../../../workloads/api-server/base
- namespace.yaml
- - configmap.yaml
- ingress.yaml
- public-access.yaml
- networkpolicy.yaml
- ghcr-regcred.sealedsecret.yaml
generatorOptions:
- disableNameSuffixHash: true
+ annotations:
+ argocd.argoproj.io/sync-wave: "0"
+
+configMapGenerator:
+ - name: api-server-config
+ envs:
+ - config.env
images:
- name: ghcr.io/donghyeonka/project-api-server
diff --git a/apps/api-server/overlays/dev/namespace.yaml b/clusters/dev-k3s/manifests/api-server/namespace.yaml
similarity index 100%
rename from apps/api-server/overlays/dev/namespace.yaml
rename to clusters/dev-k3s/manifests/api-server/namespace.yaml
diff --git a/apps/api-server/overlays/dev/networkpolicy.yaml b/clusters/dev-k3s/manifests/api-server/networkpolicy.yaml
similarity index 100%
rename from apps/api-server/overlays/dev/networkpolicy.yaml
rename to clusters/dev-k3s/manifests/api-server/networkpolicy.yaml
diff --git a/apps/api-server/overlays/dev/public-access.yaml b/clusters/dev-k3s/manifests/api-server/public-access.yaml
similarity index 100%
rename from apps/api-server/overlays/dev/public-access.yaml
rename to clusters/dev-k3s/manifests/api-server/public-access.yaml
diff --git a/clusters/dev-k3s/manifests/auth-server/config.env b/clusters/dev-k3s/manifests/auth-server/config.env
new file mode 100644
index 0000000..b3ec253
--- /dev/null
+++ b/clusters/dev-k3s/manifests/auth-server/config.env
@@ -0,0 +1,21 @@
+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
diff --git a/apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml b/clusters/dev-k3s/manifests/auth-server/db-migration-job.vault-patch.yaml
similarity index 81%
rename from apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml
rename to clusters/dev-k3s/manifests/auth-server/db-migration-job.vault-patch.yaml
index 30622da..4d5eba7 100644
--- a/apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml
+++ b/clusters/dev-k3s/manifests/auth-server/db-migration-job.vault-patch.yaml
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-secret-migration-env: database/creds/auth-db-migration-dev
vault.hashicorp.com/agent-inject-template-migration-env: |
{{- with secret "database/creds/auth-db-migration-dev" -}}
@@ -18,7 +19,16 @@ spec:
vault.hashicorp.com/agent-run-as-user: "10001"
vault.hashicorp.com/role: auth-db-migration-dev
spec:
- automountServiceAccountToken: true
+ automountServiceAccountToken: false
+ volumes:
+ - name: vault-token
+ projected:
+ defaultMode: 0444
+ sources:
+ - serviceAccountToken:
+ audience: vault
+ expirationSeconds: 3600
+ path: token
containers:
- name: auth-db-migration
command:
diff --git a/apps/auth-server/overlays/dev/deployment.vault-patch.yaml b/clusters/dev-k3s/manifests/auth-server/deployment.vault-patch.yaml
similarity index 80%
rename from apps/auth-server/overlays/dev/deployment.vault-patch.yaml
rename to clusters/dev-k3s/manifests/auth-server/deployment.vault-patch.yaml
index 54caea2..6457579 100644
--- a/apps/auth-server/overlays/dev/deployment.vault-patch.yaml
+++ b/clusters/dev-k3s/manifests/auth-server/deployment.vault-patch.yaml
@@ -8,6 +8,7 @@ spec:
annotations:
vault.hashicorp.com/agent-cache-enable: "true"
vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-secret-runtime-env: kv/data/dev/platform/postgres/auth-server
vault.hashicorp.com/agent-inject-template-runtime-env: |
{{ with secret "kv/data/dev/platform/postgres/auth-server" }}
@@ -23,7 +24,16 @@ spec:
vault.hashicorp.com/agent-run-as-user: "10001"
vault.hashicorp.com/role: auth-server-dev
spec:
- automountServiceAccountToken: true
+ automountServiceAccountToken: false
+ volumes:
+ - name: vault-token
+ projected:
+ defaultMode: 0444
+ sources:
+ - serviceAccountToken:
+ audience: vault
+ expirationSeconds: 3600
+ path: token
containers:
- name: auth-server
command:
diff --git a/apps/auth-server/overlays/dev/ghcr-regcred.sealedsecret.yaml b/clusters/dev-k3s/manifests/auth-server/ghcr-regcred.sealedsecret.yaml
similarity index 100%
rename from apps/auth-server/overlays/dev/ghcr-regcred.sealedsecret.yaml
rename to clusters/dev-k3s/manifests/auth-server/ghcr-regcred.sealedsecret.yaml
diff --git a/apps/auth-server/overlays/dev/ingress.yaml b/clusters/dev-k3s/manifests/auth-server/ingress.yaml
similarity index 92%
rename from apps/auth-server/overlays/dev/ingress.yaml
rename to clusters/dev-k3s/manifests/auth-server/ingress.yaml
index 53ddbc6..84f6d21 100644
--- a/apps/auth-server/overlays/dev/ingress.yaml
+++ b/clusters/dev-k3s/manifests/auth-server/ingress.yaml
@@ -3,6 +3,7 @@ kind: Ingress
metadata:
name: auth-server
annotations:
+ argocd.argoproj.io/sync-wave: "20"
traefik.ingress.kubernetes.io/router.entrypoints: web
spec:
ingressClassName: traefik
diff --git a/apps/auth-server/overlays/dev/kustomization.yaml b/clusters/dev-k3s/manifests/auth-server/kustomization.yaml
similarity index 52%
rename from apps/auth-server/overlays/dev/kustomization.yaml
rename to clusters/dev-k3s/manifests/auth-server/kustomization.yaml
index 9496b49..e342a8d 100644
--- a/apps/auth-server/overlays/dev/kustomization.yaml
+++ b/clusters/dev-k3s/manifests/auth-server/kustomization.yaml
@@ -4,9 +4,8 @@ kind: Kustomization
namespace: auth-dev
resources:
- - ../../base
+ - ../../../../workloads/auth-server/base
- namespace.yaml
- - configmap.yaml
- ingress.yaml
- public-access.yaml
- networkpolicy.yaml
@@ -17,9 +16,15 @@ patches:
- path: db-migration-job.vault-patch.yaml
generatorOptions:
- disableNameSuffixHash: true
+ annotations:
+ argocd.argoproj.io/sync-wave: "0"
+
+configMapGenerator:
+ - name: auth-server-config
+ envs:
+ - config.env
images:
-- name: ghcr.io/donghyeonka/project-auth-server
- newName: ghcr.io/donghyeonka/project-auth-server
- newTag: 1f47f2c
+ - name: ghcr.io/donghyeonka/project-auth-server
+ newName: ghcr.io/donghyeonka/project-auth-server
+ newTag: 1f47f2c
diff --git a/apps/auth-server/overlays/dev/namespace.yaml b/clusters/dev-k3s/manifests/auth-server/namespace.yaml
similarity index 100%
rename from apps/auth-server/overlays/dev/namespace.yaml
rename to clusters/dev-k3s/manifests/auth-server/namespace.yaml
diff --git a/apps/auth-server/overlays/dev/networkpolicy.yaml b/clusters/dev-k3s/manifests/auth-server/networkpolicy.yaml
similarity index 100%
rename from apps/auth-server/overlays/dev/networkpolicy.yaml
rename to clusters/dev-k3s/manifests/auth-server/networkpolicy.yaml
diff --git a/apps/auth-server/overlays/dev/public-access.yaml b/clusters/dev-k3s/manifests/auth-server/public-access.yaml
similarity index 100%
rename from apps/auth-server/overlays/dev/public-access.yaml
rename to clusters/dev-k3s/manifests/auth-server/public-access.yaml
diff --git a/clusters/dev-k3s/manifests/auth-system/config.env b/clusters/dev-k3s/manifests/auth-system/config.env
new file mode 100644
index 0000000..f588117
--- /dev/null
+++ b/clusters/dev-k3s/manifests/auth-system/config.env
@@ -0,0 +1,9 @@
+POSTGRES_SUPERUSER=postgres
+POSTGRES_DEFAULT_DB=postgres
+AUTH_DB_NAME=project_auth
+AUTH_DB_USER=project_auth
+KEYCLOAK_DB_NAME=keycloak
+KEYCLOAK_DB_USER=keycloak
+KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME=admin
+KEYCLOAK_CLIENT_ID=project-auth-server
+AUTH_SERVER_BASE_URL=http://auth-public.auth-dev.svc.cluster.local
diff --git a/infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml b/clusters/dev-k3s/manifests/auth-system/keycloak-client-sync.vault-patch.yaml
similarity index 74%
rename from infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml
rename to clusters/dev-k3s/manifests/auth-system/keycloak-client-sync.vault-patch.yaml
index 28fa567..b91caa6 100644
--- a/infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml
+++ b/clusters/dev-k3s/manifests/auth-system/keycloak-client-sync.vault-patch.yaml
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-keycloak-sync-env: "0644"
vault.hashicorp.com/agent-inject-secret-keycloak-sync-env: kv/data/dev/platform/keycloak/bootstrap-admin
vault.hashicorp.com/agent-inject-template-keycloak-sync-env: |
@@ -20,7 +21,16 @@ spec:
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/role: keycloak-client-sync-dev
spec:
- automountServiceAccountToken: true
+ automountServiceAccountToken: false
+ volumes:
+ - name: vault-token
+ projected:
+ defaultMode: 0444
+ sources:
+ - serviceAccountToken:
+ audience: vault
+ expirationSeconds: 3600
+ path: token
containers:
- name: keycloak-client-sync
command:
@@ -31,13 +41,19 @@ spec:
. /vault/secrets/keycloak-sync-env
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.platform.svc.cluster.local \
+ --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 \
diff --git a/infra/platform/overlays/dev/keycloak-ingress.yaml b/clusters/dev-k3s/manifests/auth-system/keycloak-ingress.yaml
similarity index 100%
rename from infra/platform/overlays/dev/keycloak-ingress.yaml
rename to clusters/dev-k3s/manifests/auth-system/keycloak-ingress.yaml
diff --git a/infra/platform/overlays/dev/keycloak.public-url-patch.yaml b/clusters/dev-k3s/manifests/auth-system/keycloak.public-url-patch.yaml
similarity index 100%
rename from infra/platform/overlays/dev/keycloak.public-url-patch.yaml
rename to clusters/dev-k3s/manifests/auth-system/keycloak.public-url-patch.yaml
diff --git a/infra/platform/overlays/dev/keycloak.vault-patch.yaml b/clusters/dev-k3s/manifests/auth-system/keycloak.vault-patch.yaml
similarity index 82%
rename from infra/platform/overlays/dev/keycloak.vault-patch.yaml
rename to clusters/dev-k3s/manifests/auth-system/keycloak.vault-patch.yaml
index f965913..ff8b531 100644
--- a/infra/platform/overlays/dev/keycloak.vault-patch.yaml
+++ b/clusters/dev-k3s/manifests/auth-system/keycloak.vault-patch.yaml
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-keycloak-env: "0644"
vault.hashicorp.com/agent-inject-secret-keycloak-env: kv/data/dev/platform/postgres/keycloak
vault.hashicorp.com/agent-inject-template-keycloak-env: |
@@ -19,7 +20,16 @@ spec:
{{ end }}
vault.hashicorp.com/role: keycloak-dev
spec:
- automountServiceAccountToken: true
+ automountServiceAccountToken: false
+ volumes:
+ - name: vault-token
+ projected:
+ defaultMode: 0444
+ sources:
+ - serviceAccountToken:
+ audience: vault
+ expirationSeconds: 3600
+ path: token
containers:
- name: keycloak
command:
diff --git a/infra/platform/overlays/dev/kustomization.yaml b/clusters/dev-k3s/manifests/auth-system/kustomization.yaml
similarity index 65%
rename from infra/platform/overlays/dev/kustomization.yaml
rename to clusters/dev-k3s/manifests/auth-system/kustomization.yaml
index f5aaf9a..c04234d 100644
--- a/infra/platform/overlays/dev/kustomization.yaml
+++ b/clusters/dev-k3s/manifests/auth-system/kustomization.yaml
@@ -4,13 +4,21 @@ kind: Kustomization
namespace: platform
resources:
- - ../../base
+ - ../../../../platform/auth-system/base
- namespace.yaml
- - configmap.yaml
- keycloak-ingress.yaml
- public-access.yaml
- networkpolicy.yaml
+generatorOptions:
+ annotations:
+ argocd.argoproj.io/sync-wave: "0"
+
+configMapGenerator:
+ - name: platform-config
+ envs:
+ - config.env
+
patches:
- path: postgres.vault-patch.yaml
- path: keycloak.vault-patch.yaml
diff --git a/infra/platform/overlays/dev/namespace.yaml b/clusters/dev-k3s/manifests/auth-system/namespace.yaml
similarity index 86%
rename from infra/platform/overlays/dev/namespace.yaml
rename to clusters/dev-k3s/manifests/auth-system/namespace.yaml
index f101fca..9ecfb54 100644
--- a/infra/platform/overlays/dev/namespace.yaml
+++ b/clusters/dev-k3s/manifests/auth-system/namespace.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: Namespace
metadata:
name: platform
+ annotations:
+ argocd.argoproj.io/sync-options: Prune=false
labels:
vault-injection: enabled
pod-security.kubernetes.io/enforce: baseline
diff --git a/infra/platform/overlays/dev/networkpolicy.yaml b/clusters/dev-k3s/manifests/auth-system/networkpolicy.yaml
similarity index 100%
rename from infra/platform/overlays/dev/networkpolicy.yaml
rename to clusters/dev-k3s/manifests/auth-system/networkpolicy.yaml
diff --git a/infra/platform/overlays/dev/postgres.vault-patch.yaml b/clusters/dev-k3s/manifests/auth-system/postgres.vault-patch.yaml
similarity index 86%
rename from infra/platform/overlays/dev/postgres.vault-patch.yaml
rename to clusters/dev-k3s/manifests/auth-system/postgres.vault-patch.yaml
index fae496c..bb56bba 100644
--- a/infra/platform/overlays/dev/postgres.vault-patch.yaml
+++ b/clusters/dev-k3s/manifests/auth-system/postgres.vault-patch.yaml
@@ -7,6 +7,7 @@ spec:
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
+ vault.hashicorp.com/agent-service-account-token-volume-name: vault-token
vault.hashicorp.com/agent-inject-perms-postgres-env: "0644"
vault.hashicorp.com/agent-inject-secret-postgres-env: kv/data/dev/platform/postgres/superuser
vault.hashicorp.com/agent-inject-template-postgres-env: |
@@ -23,7 +24,16 @@ spec:
{{ end }}
vault.hashicorp.com/role: postgres-dev
spec:
- automountServiceAccountToken: true
+ automountServiceAccountToken: false
+ volumes:
+ - name: vault-token
+ projected:
+ defaultMode: 0444
+ sources:
+ - serviceAccountToken:
+ audience: vault
+ expirationSeconds: 3600
+ path: token
containers:
- name: postgres
command:
diff --git a/infra/platform/overlays/dev/public-access.yaml b/clusters/dev-k3s/manifests/auth-system/public-access.yaml
similarity index 100%
rename from infra/platform/overlays/dev/public-access.yaml
rename to clusters/dev-k3s/manifests/auth-system/public-access.yaml
diff --git a/infra/vault/overlays/dev/kustomization.yaml b/clusters/dev-k3s/manifests/vault/kustomization.yaml
similarity index 75%
rename from infra/vault/overlays/dev/kustomization.yaml
rename to clusters/dev-k3s/manifests/vault/kustomization.yaml
index 5bc665f..c2c4d8d 100644
--- a/infra/vault/overlays/dev/kustomization.yaml
+++ b/clusters/dev-k3s/manifests/vault/kustomization.yaml
@@ -4,6 +4,6 @@ kind: Kustomization
namespace: vault
resources:
- - ../../base
+ - ../../../../platform/security/vault/base
- namespace.yaml
- networkpolicy.yaml
diff --git a/infra/vault/overlays/dev/namespace.yaml b/clusters/dev-k3s/manifests/vault/namespace.yaml
similarity index 85%
rename from infra/vault/overlays/dev/namespace.yaml
rename to clusters/dev-k3s/manifests/vault/namespace.yaml
index de14352..e5d9708 100644
--- a/infra/vault/overlays/dev/namespace.yaml
+++ b/clusters/dev-k3s/manifests/vault/namespace.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: Namespace
metadata:
name: vault
+ annotations:
+ argocd.argoproj.io/sync-options: Prune=false
labels:
pod-security.kubernetes.io/enforce: baseline
pod-security.kubernetes.io/enforce-version: latest
diff --git a/infra/vault/overlays/dev/networkpolicy.yaml b/clusters/dev-k3s/manifests/vault/networkpolicy.yaml
similarity index 83%
rename from infra/vault/overlays/dev/networkpolicy.yaml
rename to clusters/dev-k3s/manifests/vault/networkpolicy.yaml
index 671bf26..24d7ed1 100644
--- a/infra/vault/overlays/dev/networkpolicy.yaml
+++ b/clusters/dev-k3s/manifests/vault/networkpolicy.yaml
@@ -36,28 +36,6 @@ spec:
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
-metadata:
- name: vault-server-allow-transit-egress
-spec:
- podSelector:
- matchLabels:
- app: vault
- policyTypes:
- - Egress
- egress:
- - to:
- - namespaceSelector:
- matchLabels:
- kubernetes.io/metadata.name: vault-transit
- podSelector:
- matchLabels:
- app: vault-transit
- ports:
- - protocol: TCP
- port: 8200
----
-apiVersion: networking.k8s.io/v1
-kind: NetworkPolicy
metadata:
name: vault-server-allow-postgres-egress
spec:
diff --git a/argocd/projects/dev/apps-project.yaml b/clusters/dev-k3s/projects/apps.yaml
similarity index 71%
rename from argocd/projects/dev/apps-project.yaml
rename to clusters/dev-k3s/projects/apps.yaml
index ecf1a30..d04104f 100644
--- a/argocd/projects/dev/apps-project.yaml
+++ b/clusters/dev-k3s/projects/apps.yaml
@@ -3,12 +3,15 @@ kind: AppProject
metadata:
name: apps-dev
namespace: argocd
+ annotations:
+ argocd.argoproj.io/sync-wave: "-10"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
description: Dev application workloads managed by Argo CD
sourceRepos:
- - https://github.com/DongHyeonka/Project-Auth-GitOps
+ - https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- namespace: auth-dev
server: https://kubernetes.default.svc
@@ -20,31 +23,19 @@ spec:
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
diff --git a/clusters/dev-k3s/projects/cluster-addons.yaml b/clusters/dev-k3s/projects/cluster-addons.yaml
new file mode 100644
index 0000000..bc0b68c
--- /dev/null
+++ b/clusters/dev-k3s/projects/cluster-addons.yaml
@@ -0,0 +1,48 @@
+apiVersion: argoproj.io/v1alpha1
+kind: AppProject
+metadata:
+ name: cluster-addons-dev
+ namespace: argocd
+ annotations:
+ argocd.argoproj.io/sync-wave: "-10"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
+ finalizers:
+ - resources-finalizer.argocd.argoproj.io
+spec:
+ description: Cluster-scoped controllers required by the dev GitOps stack
+ sourceRepos:
+ - https://bitnami.github.io/sealed-secrets
+ - https://helm.releases.hashicorp.com
+ destinations:
+ - namespace: kube-system
+ server: https://kubernetes.default.svc
+ - namespace: vault
+ 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: apps
+ kind: Deployment
+ - group: rbac.authorization.k8s.io
+ kind: Role
+ - group: rbac.authorization.k8s.io
+ kind: RoleBinding
+ orphanedResources:
+ warn: true
diff --git a/clusters/dev-k3s/projects/kustomization.yaml b/clusters/dev-k3s/projects/kustomization.yaml
new file mode 100644
index 0000000..d591a85
--- /dev/null
+++ b/clusters/dev-k3s/projects/kustomization.yaml
@@ -0,0 +1,7 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+resources:
+ - apps.yaml
+ - cluster-addons.yaml
+ - platform.yaml
diff --git a/argocd/projects/prod/infra-project.yaml b/clusters/dev-k3s/projects/platform.yaml
similarity index 65%
rename from argocd/projects/prod/infra-project.yaml
rename to clusters/dev-k3s/projects/platform.yaml
index 1f08cba..9dcb181 100644
--- a/argocd/projects/prod/infra-project.yaml
+++ b/clusters/dev-k3s/projects/platform.yaml
@@ -1,25 +1,25 @@
apiVersion: argoproj.io/v1alpha1
kind: AppProject
metadata:
- name: infra-prod
+ name: platform-dev
namespace: argocd
+ annotations:
+ argocd.argoproj.io/sync-wave: "-10"
+ argocd.argoproj.io/sync-options: Prune=confirm,Delete=confirm
finalizers:
- resources-finalizer.argocd.argoproj.io
spec:
- description: Prod shared infrastructure managed by Argo CD
+ description: Dev shared infrastructure managed by Argo CD
sourceRepos:
- - https://github.com/DongHyeonka/Project-Auth-GitOps
- - https://bitnami-labs.github.io/sealed-secrets
+ - https://git.learn.hyeonworks.com/donghyeon.kang/project-gitops
destinations:
- - namespace: platform-prod
+ - namespace: platform
server: https://kubernetes.default.svc
- - namespace: kube-system
+ - namespace: vault
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"
@@ -27,16 +27,12 @@ spec:
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"
@@ -45,17 +41,11 @@ spec:
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
diff --git a/docs/adr/0001-canonical-repository.md b/docs/adr/0001-canonical-repository.md
new file mode 100644
index 0000000..8fc588a
--- /dev/null
+++ b/docs/adr/0001-canonical-repository.md
@@ -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.
diff --git a/docs/adr/0002-terraform-ownership.md b/docs/adr/0002-terraform-ownership.md
new file mode 100644
index 0000000..c3186d1
--- /dev/null
+++ b/docs/adr/0002-terraform-ownership.md
@@ -0,0 +1,24 @@
+# ADR 0002: Terraform state ownership
+
+Status: accepted
+
+Terraform은 VM/네트워크뿐 아니라 provider가 제공되는 Vault API 객체도
+관리할 수 있다. 현재 저장소의 Terraform 범위는 Vault API이고 실제
+machine provisioning은 provider가 확정될 때 별도 root로 추가한다.
+
+`dev-k3s`는 두 state만 사용한다.
+
+- `vault-core`: mounts, auth backends, policies, Kubernetes/JWT roles,
+ application Transit key
+- `vault-database`: PostgreSQL connection과 dynamic roles
+
+resource/API path 하나는 한 state에만 속한다. state는 암호화, versioning,
+access control, locking이 가능한 remote backend에 저장한다.
+
+`vault-core`는 privilege-escalation 가능한 객체를 포함하므로 제한된
+관리자 실행만 허용한다. `vault-database`는 core가 생성한
+`vault-database-automation-dev` 정책의 short-lived identity로 실행한다.
+
+Secret payload는 Terraform resource/data source로 관리하지 않는다.
+필수 credential은 ephemeral variable과 provider write-only argument를
+통해서만 apply에 전달한다.
diff --git a/docs/adr/0003-vault-topology.md b/docs/adr/0003-vault-topology.md
new file mode 100644
index 0000000..1bf993f
--- /dev/null
+++ b/docs/adr/0003-vault-topology.md
@@ -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를
+만들지 않는다.
diff --git a/docs/adr/0004-argo-bootstrap-stages.md b/docs/adr/0004-argo-bootstrap-stages.md
new file mode 100644
index 0000000..3a76796
--- /dev/null
+++ b/docs/adr/0004-argo-bootstrap-stages.md
@@ -0,0 +1,18 @@
+# ADR 0004: Single Argo CD root
+
+Status: accepted
+
+Argo CD 설치 후 `bootstrap/argocd/root-application.yaml` 하나만 seed한다.
+root는 `clusters/dev-k3s`의 AppProject와 모든 child Application을 소유한다.
+
+반복 `kubectl apply`와 foundation/platform/application별 root wrapper는
+제거한다. routine deployment는 Git merge만으로 시작한다.
+
+Child Application의 sync wave는 객체 생성 순서를 가독성 있게 표현하지만
+서로 다른 Application의 readiness dependency로 간주하지 않는다.
+Workload와 hook은 Vault/DB가 늦게 준비되는 상황을 retry할 수 있어야 한다.
+
+Root가 child Application을 prune하거나 삭제하려면 확인이 필요하다.
+shared resource 소유권 충돌은 sync를 실패시킨다. 현재 규모에서는 명시적
+Application을 사용하고 두 번째 클러스터가 생길 때 ApplicationSet을
+검토한다.
diff --git a/docs/adr/0005-repository-layout.md b/docs/adr/0005-repository-layout.md
new file mode 100644
index 0000000..3cc9a18
--- /dev/null
+++ b/docs/adr/0005-repository-layout.md
@@ -0,0 +1,17 @@
+# ADR 0005: Cluster-first repository layout
+
+Status: accepted
+
+현재는 하나의 platform 팀, 하나의 dev cluster와 소수 workload를 가지므로
+GitOps configuration monorepo를 유지한다. application source repository와
+deployment configuration repository는 분리한다.
+
+- `platform/`, `workloads/`: 환경 중립 base
+- `clusters//manifests`: cluster-specific final composition
+- `clusters//applications`: Argo reconciliation inventory
+- `iac/terraform`: Kubernetes manifest와 분리된 external API IaC
+- `bootstrap`: controller가 존재하기 전의 최소 seed
+
+production 접근권한, 소유 팀, Terraform backend 또는 release cadence가
+실제로 갈라질 때 platform GitOps, workload GitOps, IaC repo 분리를
+재검토한다. 존재하지 않는 환경의 skeleton은 유지하지 않는다.
diff --git a/docs/adr/0006-gateway-and-istio.md b/docs/adr/0006-gateway-and-istio.md
new file mode 100644
index 0000000..c0c5446
--- /dev/null
+++ b/docs/adr/0006-gateway-and-istio.md
@@ -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 설정을 배포하지 않기 위함이다.
diff --git a/docs/architecture/argocd.md b/docs/architecture/argocd.md
new file mode 100644
index 0000000..7b0e9d3
--- /dev/null
+++ b/docs/architecture/argocd.md
@@ -0,0 +1,38 @@
+# Argo CD layout
+
+`bootstrap/argocd/root-application.yaml`이 유일한 수동 seed입니다. 이
+Application은 `clusters/dev-k3s`를 source로 사용하고 다음 리소스를
+소유합니다.
+
+```text
+clusters/dev-k3s
+├── projects
+└── applications
+ ├── foundation
+ │ ├── sealed-secrets
+ │ ├── vault
+ │ └── vault-agent-injector
+ ├── platform
+ │ └── auth-system
+ └── workloads
+ ├── auth-server
+ └── api-server
+```
+
+AppProject는 root sync wave `-10`, foundation은 `0~1`, platform은 `10`,
+workload는 `20`입니다. 이 wave는 child Application 객체 생성 순서만
+표현하며 서로 다른 Application의 readiness dependency로 사용하지
+않습니다. Vault Agent와 workload는 필요한 Vault/DB API가 준비될 때까지
+자체 retry 가능한 형태여야 합니다.
+
+모든 child Application은 auto-sync, prune, self-heal을 사용합니다.
+Application 삭제와 parent prune은 확인이 필요하며, shared resource
+소유권 충돌은 `FailOnSharedResource=true`로 실패시킵니다.
+
+Sync hook이 있는 `auth-server`와 `auth-system`에는 selective sync 옵션을
+사용하지 않습니다. DB migration과 Keycloak client sync는 같은
+Application 내부 wave로 순서를 제어합니다.
+
+클러스터가 하나이고 child Application 수가 적으므로 현재는 명시적
+Application을 사용합니다. 두 번째 클러스터나 실제 production이 생길
+때 foundation/platform/workload별 ApplicationSet 도입을 검토합니다.
diff --git a/docs/architecture/deployment.md b/docs/architecture/deployment.md
new file mode 100644
index 0000000..c11accb
--- /dev/null
+++ b/docs/architecture/deployment.md
@@ -0,0 +1,55 @@
+# Deployment architecture
+
+## Reconciliation boundaries
+
+```text
+Gitea main
+ |
+ +-- Argo CD root -> AppProjects + child Applications -> Kubernetes
+ |
+ +-- approved Terraform runner -> Vault API
+```
+
+Argo CD는 Kubernetes desired state만 관리합니다. 최초 Argo 설치/root
+seed와 문서화된 recovery 외에는 직접 cluster mutation을 하지 않습니다.
+Terraform은 Config Management Plugin이나 Argo hook 안에서 실행하지
+않습니다.
+
+## Kustomize ownership
+
+- `platform/`, `workloads/`: 환경 중립 base
+- `clusters/dev-k3s/manifests/`: namespace, host, image, Vault role 및
+ NetworkPolicy를 포함하는 최종 cluster composition
+- Argo CD Application: final composition만 source로 사용
+
+지원하지 않는 production overlay는 존재하지 않습니다. production
+계약과 승인 경계가 확정될 때 별도로 생성합니다.
+
+## In-application ordering
+
+`auth-server`의 한 sync operation 안에서:
+
+- 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을 사용합니다.
+
+## Stateful lifecycle
+
+Vault와 PostgreSQL PVC는 `Prune=false`로 보호합니다. child Application
+prune/delete는 확인이 필요합니다. path 이동이나 Application rename 전에는
+새 owner가 동일 live resource를 정상적으로 추적하는지 확인한 후 이전
+owner를 non-cascading 방식으로 제거합니다.
+
+## Image promotion
+
+첫-party image는 애플리케이션 CI가 얻은 정확한 GHCR digest를 Gitea
+workflow에 전달합니다. workflow는 digest 변경 PR을 만들고, validation과
+승인을 거쳐 merge된 뒤 Argo CD가 배포합니다.
+
+현재 short-SHA tag는 migration 시점의 예외입니다. private GHCR을 읽을
+자격증명이 이 저장소 실행 환경에 없으므로 임의 digest로 바꾸지 않았고,
+다음 정상 promotion에서 `digest:`로 교체됩니다.
diff --git a/docs/architecture/secret-trust.md b/docs/architecture/secret-trust.md
new file mode 100644
index 0000000..481b6d4
--- /dev/null
+++ b/docs/architecture/secret-trust.md
@@ -0,0 +1,55 @@
+# Secret trust boundaries
+
+## Dev Vault
+
+`dev-k3s`는 단일 self-hosted Vault를 사용합니다. 동일 workload
+클러스터에 별도의 Transit Vault를 두지 않습니다. 단일 Vault는 다음을
+소유합니다.
+
+- 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을 사용해야 합니다.
+
+## Terraform
+
+`vault-core` state는 mounts, auth, policies, roles와 JWT key를 소유하며
+제한된 관리자만 적용합니다. `vault-database`는 PostgreSQL connection과
+dynamic roles만 소유하고 `vault-database-automation-dev` 정책을 사용합니다.
+
+Terraform variable로 전달되는 token과 PostgreSQL password는 ephemeral/
+write-only 경계를 사용합니다. KV payload는 Terraform resource/data
+source로 읽거나 쓰지 않습니다.
+
+## Workload authentication
+
+workload는 audience `vault`, TTL 1시간의 projected ServiceAccount token으로
+Vault Kubernetes auth에 로그인합니다. token은 Vault Agent가 사용하며
+application container에 Kubernetes bearer token을 직접 노출하지 않습니다.
+
+Secret payload는 승인된 운영자가 Vault에 직접 기록합니다. 값은 Git,
+Gitea Actions log, Terraform state, Kubernetes manifest에 남기지 않습니다.
+
+## Bootstrap material
+
+Vault init output은 기본적으로 `.local/vault/dev-k3s-init.json`에 mode
+`0600`으로 생성됩니다. encrypted custody로 이동한 후 working copy를
+제거합니다. initial root token은 `vault-core`와 operator auth 검증 직후
+폐기합니다.
+
+Sealed Secrets는 private GHCR pull credential에만 사용합니다. controller
+private key는 별도 복구 저장소에 백업해야 합니다.
+
+## Dev limitations
+
+- Vault, PostgreSQL, ingress가 아직 TLS를 사용하지 않음
+- single-node Vault와 PostgreSQL
+- Kubernetes API egress CIDR가 현재 dev cluster에 종속
+- 정적 bootstrap secret은 coordinated rotation 필요
+
+이 제약은 production에서 허용되지 않습니다.
diff --git a/AGENT.md b/docs/archive/AGENT-legacy.md
similarity index 100%
rename from AGENT.md
rename to docs/archive/AGENT-legacy.md
diff --git a/docs/archive/INTERN_GUIDE-legacy.md b/docs/archive/INTERN_GUIDE-legacy.md
new file mode 100644
index 0000000..3229d17
--- /dev/null
+++ b/docs/archive/INTERN_GUIDE-legacy.md
@@ -0,0 +1,3040 @@
+# 인턴 가이드: 개념, 아키텍처, 코드로 이해하는 Project Auth GitOps
+
+## 1. 이 문서의 목표
+
+이 문서의 목적은 **개념**, **현재 프로젝트 아키텍처**, **실제 코드**를 하나의 흐름으로 이해하게 만드는 것입니다.
+
+즉, 이 문서는 단순한 코드 해설서가 아니라 아래 3층 구조를 목표로 합니다.
+
+1. 개념 레이어
+ 이 프로젝트가 왜 Kubernetes, GitOps, Vault, Terraform, Keycloak 구조를 쓰는지 이해합니다.
+2. 아키텍처 레이어
+ 현재 dev 환경에서 각 구성요소가 어떤 책임을 가지며 어떻게 연결되는지 이해합니다.
+3. 코드 레이어
+ 실제로 `apps/`, `infra/`, `scripts/`, `terraform/`, `runbooks/` 안의 파일을 읽고 수정할 수 있게 합니다.
+
+이 문서를 다 읽고 나면 최소한 아래 질문에 스스로 답할 수 있어야 합니다.
+
+- 이 저장소는 왜 존재하는가?
+- `apps/`만 보면 왜 절반밖에 이해하지 못하는가?
+- `auth-server`는 왜 `Deployment`와 `Job`을 둘 다 가지는가?
+- 비밀값은 어디에서 생성되고, 어디를 지나서, 어떤 방식으로 Pod 안으로 들어가는가?
+- 왜 `vault-transit`과 `vault`를 둘 다 두었는가?
+- 어떤 값은 `ConfigMap`에 두고, 어떤 값은 Vault에 두는가?
+- 무엇을 `base`에 두고 무엇을 `overlay`에 두는가?
+- 어떤 변경이 다른 파일들까지 연쇄적으로 수정하게 만드는가?
+
+이 문서는 **현재 dev 환경 기준**으로 설명합니다.
+
+## 2. 가장 먼저 알아야 하는 사실
+
+이 저장소는 애플리케이션 소스 저장소가 아니라 **GitOps 저장소**입니다.
+
+- `Project-Auth-Server`, `Project-Api-Server` 같은 앱 저장소는 코드와 CI를 담당합니다.
+- 이 저장소는 Kubernetes에 반영할 선언과 CD를 담당합니다.
+- 그래서 `apps/` 안의 YAML만 읽으면 "앱이 어떻게 배포되는지"는 보이지만, "누가 이걸 적용하는지", "비밀값은 누가 준비하는지", "왜 이런 구조인지"는 보이지 않습니다.
+
+즉, 이 저장소를 제대로 이해하려면 아래를 **한 묶음**으로 봐야 합니다.
+
+- `apps/`: 앱 매니페스트
+- `infra/`: 공용 인프라 매니페스트
+- `argocd/`: Argo CD가 어떤 폴더를 감시할지 정의
+- `scripts/`: 실제 운영 절차를 자동화하는 Bash
+- `terraform/`: Vault 내부 상태를 선언적으로 맞추는 코드
+- `runbooks/`: 사람이 최초 bootstrap할 때 따르는 문서와 Vault 정책
+
+## 3. 파일 확장자부터 정확히 알고 가기
+
+| 확장자 | 뜻 | 이 저장소에서 하는 일 |
+| ------- | -------------------------------- | -------------------------------------------------------------------- |
+| `.yaml` | 선언형 데이터 포맷 | Kubernetes 리소스, Argo CD Application, GitHub Actions 워크플로 정의 |
+| `.sh` | Bash 스크립트 | 운영 절차를 순서대로 자동 실행 |
+| `.hcl` | HashiCorp Configuration Language | Vault 정책, Vault 서버 설정 |
+| `.tf` | Terraform 설정 파일 | Vault 내부 리소스를 선언적으로 생성/동기화 |
+
+중요한 점은 `*.tf`도 내부적으로 HCL 문법을 사용하지만, **의미가 다르다**는 것입니다.
+
+- `runbooks/vault/dev/policies/*.hcl`: "누가 어떤 경로를 읽거나 쓸 수 있는가"를 적는 **Vault 정책**
+- `infra/vault/base/files/vault/vault.hcl`: Vault 서버 자체가 어떻게 동작할지 적는 **Vault 서버 설정**
+- `terraform/**/*.tf`: Terraform이 어떤 Vault 리소스를 만들어야 하는지 적는 **IaC 코드**
+
+## 4. 용어 사전
+
+### 4-1. Kubernetes 용어
+
+| 용어 | 뜻 | 이 저장소에서 왜 중요한가 |
+| -------------------- | ---------------------------------------------- | --------------------------------------------------------------------------------------------- |
+| Manifest | Kubernetes에 적용할 선언 파일 | `apps/`, `infra/`, `argocd/` 대부분이 manifest다 |
+| Namespace | 리소스를 논리적으로 나누는 공간 | `auth-dev`, `api-dev`, `platform`, `vault`, `vault-transit`가 서로 다른 책임을 가진다 |
+| Pod | 실제 컨테이너가 뜨는 가장 작은 실행 단위 | Deployment나 Job이 결국 Pod를 만든다 |
+| Deployment | stateless 앱을 원하는 개수만큼 유지하는 리소스 | `auth-server`, `api-server`, `keycloak`, `vault`가 여기에 해당한다 |
+| StatefulSet | 이름, 저장소, 순서가 중요한 워크로드용 리소스 | `postgres`는 데이터가 있으므로 `Deployment`가 아니라 `StatefulSet`을 쓴다 |
+| Job | 한 번 실행하고 끝나는 작업 | DB migration, Keycloak client sync에 사용된다 |
+| Service | Pod 앞에 놓는 고정된 네트워크 진입점 | Pod IP가 바뀌어도 `auth-server`, `api-server`, `postgres`에 접속할 수 있다 |
+| Ingress | 클러스터 바깥 또는 north-south HTTP 진입 규칙 | Traefik을 통해 public host를 연결한다 |
+| NetworkPolicy | Pod 간 통신 허용/차단 규칙 | 기본 차단 후 필요한 통신만 허용하는 구조를 만든다 |
+| ConfigMap | 민감하지 않은 설정값 저장소 | 포트, 호스트, issuer URI 같은 값을 둔다 |
+| Secret | 민감한 값 저장소 | 이 저장소는 runtime secret을 가급적 Vault로 옮기고 image pull secret만 예외로 남긴다 |
+| SealedSecret | Git에 올려도 되는 암호화된 Secret 형태 | `ghcr-regcred`처럼 예외적으로 Git에 남겨야 하는 secret에 쓴다 |
+| ServiceAccount | Pod가 Kubernetes API 세계에서 갖는 신분 | Vault Kubernetes auth가 이 신분을 이용해 Pod를 검증한다 |
+| ExternalName Service | 다른 DNS 이름으로 트래픽을 넘기는 Service | `auth-public`, `api-public`처럼 Traefik 이름을 우회해 내부에서도 같은 public host를 쓰게 한다 |
+| Probe | 컨테이너 준비 상태/생존 상태 확인 | 준비 전 트래픽 차단, 비정상 재시작 판단에 쓰인다 |
+
+### 4-2. GitOps / Kustomize / Argo CD 용어
+
+| 용어 | 뜻 | 이 저장소에서 왜 중요한가 |
+| ------------------- | --------------------------------------------------------- | ------------------------------------------------------------------------------- |
+| GitOps | Git에 있는 선언을 실제 클러스터 상태의 기준으로 삼는 방식 | 사람이 직접 `kubectl edit` 하지 않고 Git을 수정한다 |
+| Source of truth | 최종 기준이 되는 원본 | 앱 배포 선언의 원본은 이 저장소이고, 일부 seed secret의 원본은 provider Vault다 |
+| Kustomize | YAML을 base + overlay 구조로 합성하는 도구 | 공통 뼈대와 환경별 차이를 분리한다 |
+| Base | 환경과 무관한 공통 정의 | 공통 Deployment, Service, ServiceAccount 등이 들어간다 |
+| Overlay | 특정 환경에만 적용되는 차이 | dev용 namespace, host, image tag, Vault patch가 들어간다 |
+| Patch | 기존 리소스 일부만 덮어쓰는 변경 조각 | `deployment.vault-patch.yaml`이 대표적이다 |
+| Argo CD Application | "이 경로를 이 namespace로 동기화하라"는 선언 | `argocd/applications/dev/**`가 담당한다 |
+| Sync wave | Argo CD 적용 순서를 정하는 숫자 | Vault, platform, apps 순서를 안정적으로 맞춘다 |
+| Hook / PreSync | 일반 리소스 적용 전/후에 특별하게 실행되는 리소스 | DB migration Job은 앱 배포보다 먼저 돈다 |
+
+### 4-3. Vault / Terraform 용어
+
+| 용어 | 뜻 | 이 저장소에서 왜 중요한가 |
+| --------------- | --------------------------------------------------------- | --------------------------------------------------------------------------- |
+| Vault | 비밀값과 암호 기능을 제공하는 시스템 | 이 프로젝트의 runtime secret 관리 중심이다 |
+| Provider Vault | 다른 Vault를 돕는 상위 Vault | 여기서는 `vault-transit`이 workload Vault를 돕는다 |
+| Workload Vault | 앱이 직접 사용하는 Vault | `vault`가 여기에 해당한다 |
+| Transit engine | 데이터를 직접 저장하지 않고 암호 연산만 해주는 Vault 엔진 | JWT 서명, auto-unseal에 사용된다 |
+| Auto-unseal | Vault가 재기동 시 자동으로 봉인을 풀 수 있게 하는 방식 | workload Vault는 provider Vault의 transit key로 자동 unseal된다 |
+| Mount | Vault 안의 기능이 걸린 경로 | `kv`, `database`, `transit`, `auth/kubernetes`, `auth/approle`이 대표적이다 |
+| Policy | Vault 경로별 권한 규칙 | `auth-server-dev.hcl` 같은 파일이 여기에 해당한다 |
+| Kubernetes auth | Pod의 ServiceAccount JWT로 Vault에 로그인하는 방식 | 앱/플랫폼 Pod가 이 방식으로 secret을 받는다 |
+| AppRole | 기계 대 기계 로그인 방식 | CI와 bootstrap 자동화가 사용한다 |
+| TTL | 토큰이나 동적 계정의 수명 | migration용 DB 계정 같은 것을 오래 살지 않게 만든다 |
+| Terraform state | Terraform이 "내가 무엇을 만들었는지" 기억하는 파일 | bootstrap 루트와 reconcile 루트가 분리되어 있다 |
+| Idempotent | 여러 번 실행해도 결과가 같게 유지되는 성질 | reconcile 스크립트와 Terraform 설계의 핵심이다 |
+
+## 5. 이 저장소를 이해하기 위한 핵심 개념
+
+이 섹션은 "왜 이런 구조가 필요한가"를 설명합니다.
+이 섹션을 먼저 이해해야 뒤에서 나오는 YAML, Bash, HCL, TF가 단순 문법이 아니라 **설계의 결과물**로 보입니다.
+
+### 5-1. 인증, 인가, OAuth2, OIDC, JWT, Keycloak
+
+이 프로젝트를 이해하려면 먼저 아래 개념을 구분해야 합니다.
+
+| 용어 | 뜻 | 이 프로젝트에서의 의미 |
+| -------------------- | ---------------------------------------- | -------------------------------------------------------- |
+| 인증(Authentication) | "너 누구냐?"를 확인하는 것 | 로그인 성공 여부, 토큰 발급 대상 식별 |
+| 인가(Authorization) | "너 여기 접근해도 되냐?"를 확인하는 것 | API 접근 권한, 역할(Role) 확인 |
+| OAuth2 | 권한 위임 프레임워크 | 소셜 로그인, 외부 로그인 연동의 큰 틀 |
+| OIDC(OpenID Connect) | OAuth2 위에 사용자 신원 확인을 얹은 표준 | 로그인 결과를 표준화된 토큰 형태로 다룸 |
+| JWT | 서명된 토큰 포맷 | auth-server가 발급하고 api-server가 검증 |
+| Issuer | 토큰을 발급한 주체 | `auth-server`, `Keycloak`, `Vault transit` 설정과 연결됨 |
+| Client | 인증 서버를 이용하는 애플리케이션 | Keycloak 안의 `project-auth-server` client |
+| Redirect URI | 로그인 후 다시 돌아올 주소 | Keycloak client sync Job이 코드로 맞춘다 |
+
+이 프로젝트에서는 인증 관련 역할이 셋으로 나뉩니다.
+
+- `Keycloak`
+ 외부 로그인 제공자와 연결되고, 로그인 브로커 역할을 한다
+- `auth-server`
+ 우리 서비스 관점의 인증 서버 역할을 하며 JWT를 발급한다
+- `api-server`
+ auth-server가 발급한 JWT를 검증하는 리소스 서버 역할을 한다
+
+이 셋이 실제로 어떻게 맞물려 동작하는지 하나의 흐름으로 보면 이렇습니다.
+
+```mermaid
+sequenceDiagram
+ participant User as 사용자 브라우저
+ participant KC as Keycloak
(로그인 브로커)
+ participant Google as 구글/GitHub 등
+ participant Auth as auth-server
(JWT 발급자)
+ participant API as api-server
(리소스 서버)
+
+ User->>Auth: "소셜 로그인할래요"
+ Auth->>KC: Keycloak 로그인 페이지로 리다이렉트
+ KC->>Google: 소셜 제공자와 OAuth2 통신
+ Google-->>KC: 사용자 정보 반환
+ KC-->>Auth: OIDC 표준 토큰으로 변환해서 콜백
+ Auth->>Auth: 사용자 DB 조회/가입 처리 + 우리 JWT 발급
+ Auth-->>User: 우리 서비스 JWT 토큰 반환
+ User->>API: JWT를 헤더에 담아 API 호출
+ API->>API: JWT 서명 검증 (Vault Transit 공개키로)
+ API-->>User: API 응답
+```
+
+이 그림에서 핵심은 **각자가 맡은 범위가 다르다**는 것입니다. Keycloak은 외부 제공자와의 복잡한 통신만 처리하고, auth-server는 우리 서비스의 JWT만 발급하고, api-server는 그 JWT를 검증만 합니다. 하나의 서비스가 모든 것을 하지 않기 때문에, 각 부분을 독립적으로 교체하거나 수정할 수 있습니다.
+
+왜 이렇게 나누는가?
+
+- 소셜 로그인 제공자별 차이를 Keycloak이 흡수하게 하기 위해
+- 우리 서비스의 토큰 정책과 외부 로그인 흐름을 분리하기 위해
+- API 서버가 로그인 로직과 토큰 발급 책임까지 모두 떠안지 않게 하기 위해
+
+> 💡 이 흐름의 각 단계가 **구체적으로 어떤 메커니즘**으로 동작하는지는 24장에서 심층적으로 다룹니다.
+
+이 개념을 이해해야 아래 파일들이 왜 존재하는지 자연스럽게 연결됩니다.
+
+- `infra/platform/base/keycloak-deployment.yaml`
+- `infra/platform/base/keycloak-client-sync-job.yaml`
+- `apps/auth-server/overlays/dev/configmap.yaml`
+- `apps/api-server/overlays/dev/configmap.yaml`
+
+### 5-2. 리눅스, 컨테이너, 프로세스, 파일
+
+이 저장소의 YAML을 읽을 때 사실상 리눅스 프로세스 개념을 알아야 합니다.
+특히 Vault patch를 읽을 때 이 이해가 없으면 `command`, `args`, `. /vault/secrets/runtime-env`, `exec java -jar ...` 같은 부분이 전부 주문처럼 보입니다.
+
+꼭 이해해야 하는 개념은 아래와 같습니다.
+
+| 개념 | 뜻 | 이 프로젝트에서 왜 중요한가 |
+| ------------------- | --------------------------------------------- | ----------------------------------------------------------------------- |
+| Process | 실행 중인 프로그램 | 컨테이너 안에서 결국 Java, Postgres, Keycloak도 모두 프로세스다 |
+| PID 1 | 컨테이너 안의 첫 번째 프로세스 | 신호 처리와 종료 동작에 영향이 크다 |
+| `command` / `args` | 컨테이너가 실제로 어떤 명령으로 시작할지 정의 | Vault secret을 읽고 나서 원래 앱을 띄우기 위해 자주 재정의한다 |
+| `source` (`. file`) | 파일 안의 셸 명령을 현재 셸에 적용 | Vault Agent가 만든 `export ...` 파일을 환경변수로 불러온다 |
+| `exec` | 현재 셸 프로세스를 실제 앱 프로세스로 교체 | PID 1을 셸이 아니라 Java/Postgres/Keycloak로 만들기 위해 중요하다 |
+| Volume mount | 파일이나 디렉터리를 컨테이너에 붙이는 것 | Vault secret file, init script, config file이 모두 이 방식으로 들어온다 |
+
+예를 들어 `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`의 핵심은 이 순서입니다.
+
+1. Vault Agent가 `/vault/secrets/runtime-env` 파일 생성
+2. `/bin/sh -ec` 셸 시작
+3. `. /vault/secrets/runtime-env`로 환경변수 로드
+4. `exec java -jar /app/application.jar`로 실제 앱 프로세스 시작
+
+이것을 프로세스 관점에서 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart TD
+ subgraph Container["auth-server 컨테이너 내부"]
+ direction TB
+ A["/bin/sh -ec 시작
PID 1 = 셸 프로세스"] --> B[". /vault/secrets/runtime-env
export 명령들이 현재 셸에 적용
→ 환경변수가 셸 메모리에 올라감"]
+ B --> C["exec java -jar /app/application.jar
셸 프로세스가 Java 프로세스로 교체
→ PID 1 = Java (셸은 사라짐)"]
+ end
+
+ subgraph 만약_exec_없이["만약 exec를 안 쓰면?"]
+ direction TB
+ D["PID 1 = 셸 (계속 살아있음)"] --> E["PID 2 = Java (자식 프로세스)"]
+ E --> F["K8s가 SIGTERM → 셸이 받음
셸은 자식에게 전달 안 할 수 있음
→ Java가 graceful shutdown 못 함"]
+ end
+```
+
+`exec`가 왜 중요한지 이 그림이 보여줍니다. K8s가 Pod를 종료할 때 **PID 1에게** SIGTERM 신호를 보냅니다. `exec` 없이 셸이 PID 1이면, 셸은 이 신호를 Java에게 전달하지 않을 수 있습니다. 결과적으로 Java가 연결을 정리하지 못한 채 강제 종료(SIGKILL)됩니다. `exec`를 쓰면 Java가 PID 1이 되어 직접 SIGTERM을 받고, 연결을 정리한 뒤 깔끔하게 종료합니다.
+
+즉, 여기서 중요한 것은 "Vault가 비밀값을 준다"는 사실만이 아닙니다.
+**비밀값을 파일로 렌더링하고, 셸이 그 파일을 읽고, 마지막에 앱 프로세스로 넘어간다**는 실행 모델 전체를 이해해야 합니다.
+
+이 개념이 없으면 아래 같은 질문에 답하기 어렵습니다.
+
+- 왜 secret을 환경변수 자체로 바로 안 넣고 파일로 렌더링하나?
+- 왜 `exec`를 쓰나?
+- 왜 base Deployment에서는 `command`가 없는데 overlay patch에서는 생기나?
+
+> 💡 PID 1와 시그널 처리, 컨테이너 내부 프로세스 모델에 대한 더 깊은 이해는 23장에서 다룹니다.
+
+### 5-3. Kubernetes는 "컨테이너 실행기"가 아니라 "원하는 상태를 유지하는 시스템"이다
+
+초보자는 Kubernetes를 "도커를 원격으로 띄우는 도구"처럼 이해하기 쉽습니다. 하지만 더 정확히는 **원하는 상태(desired state)를 유지하는 시스템**입니다.
+
+이 프로젝트에서 꼭 알아야 하는 핵심 리소스는 아래입니다.
+
+| 리소스 | 무엇을 위한 것인가 | 이 프로젝트의 예시 |
+| -------------- | ------------------------------------------- | --------------------------------------------------- |
+| Deployment | 계속 살아 있어야 하는 stateless 앱 | `auth-server`, `api-server`, `keycloak`, `vault` |
+| StatefulSet | 저장소와 정체성이 중요한 워크로드 | `postgres` |
+| Job | 한 번 실행하고 끝나야 하는 작업 | `auth-db-migration`, `keycloak-client-sync` |
+| Service | Pod 앞의 고정 네트워크 이름 | `auth-server`, `api-server`, `postgres`, `keycloak` |
+| Ingress | HTTP 요청의 진입 규칙 | public host와 Traefik 연결 |
+| NetworkPolicy | Pod 간 허용할 통신만 남기는 네트워크 방화벽 | 각 namespace의 default deny 구조 |
+| ServiceAccount | Pod의 신분 | Vault Kubernetes auth에서 핵심 |
+| ConfigMap | 비민감 설정 | 포트, URL, issuer, host |
+| Secret / Vault | 민감 설정 | DB 비밀번호, client secret, token |
+
+이 리소스들을 왜 구분해서 써야 할까요?
+
+- 앱은 계속 살아야 하므로 `Deployment`
+- DB는 디스크와 이름이 안정적이어야 하므로 `StatefulSet`
+- migration은 한 번만 돌고 끝나야 하므로 `Job`
+
+이 판단을 잘못하면 부작용이 큽니다.
+
+- DB를 `Deployment`로 만들면 저장소와 이름 안정성이 약해진다
+- migration을 `Deployment`로 만들면 계속 재시작될 수 있다
+- 앱을 `Job`로 만들면 정상 서비스가 유지되지 않는다
+
+K8s가 이 리소스들의 상태를 어떻게 유지하는지 핵심 루프를 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart LR
+ A["개발자가 선언
replicas: 3"] --> B["API Server에
Desired State 저장"]
+ B --> C{"Controller Manager
현재 vs 원하는 상태 비교"}
+ C -->|"Pod 2개 살아있음
1개 부족"| D["Pod 1개 추가 생성"]
+ C -->|"Pod 4개 살아있음
1개 초과"| E["Pod 1개 삭제"]
+ C -->|"Pod 3개 살아있음
일치 ✅"| F["아무것도 안 함"]
+ D --> C
+ E --> C
+ F -->|"계속 감시
(Reconciliation Loop)"| C
+```
+
+이 루프가 **끊임없이** 도는 것이 K8s의 핵심입니다. 사용자가 "3개 돌려라"라고 선언하면, K8s는 현재 상태를 계속 확인하면서 차이를 조정합니다. Pod가 죽어도 자동으로 새로 만듭니다. 이것이 "컨테이너 실행기"가 아니라 "상태 유지 시스템"인 이유입니다.
+
+> 💡 이 Reconciliation Loop, Watch 메커니즘, Control Plane 각 컴포넌트의 역할은 25장에서 프로세스 수준으로 상세히 다룹니다.
+
+### 5-4. GitOps, Kustomize, Argo CD
+
+이 프로젝트는 "좋은 YAML을 써놨다"에서 끝나지 않습니다.
+이 YAML을 **누가**, **어떤 기준으로**, **반복적으로** 적용하느냐가 중요합니다.
+
+#### GitOps
+
+GitOps는 "실제 클러스터 상태의 기준을 Git에 둔다"는 운영 방식입니다.
+
+장점:
+
+- 누가 무엇을 바꿨는지 Git 기록으로 남는다
+- 수동 클릭보다 재현 가능하다
+- 문제가 생기면 선언 기준으로 되돌리기 쉽다
+
+주의할 점:
+
+- 클러스터에서 직접 수정하면 Git과 드리프트가 생긴다
+- Git에 민감값을 넣으면 GitOps의 편의가 보안 리스크로 바뀐다
+
+#### Kustomize
+
+Kustomize는 공통(base)과 환경별 차이(overlay)를 분리합니다.
+
+- `base`: 환경과 무관한 공통 뼈대
+- `overlay`: dev/prod별 차이
+
+이 프로젝트에서 이 구조가 중요한 이유:
+
+- `auth-server`의 기본 보안 설정, 포트, 프로브는 공통이지만
+- Vault 경로, ingress host, image tag, namespace는 환경별로 다를 수 있기 때문입니다
+
+#### Argo CD
+
+Argo CD는 Git에 있는 선언을 실제 클러스터와 맞추는 실행 주체입니다.
+
+이 프로젝트에서 Argo CD가 하는 일:
+
+- `argocd/applications/dev/**`에 정의된 경로를 감시
+- 해당 경로의 manifest를 dev 클러스터에 동기화
+- 드리프트가 생기면 다시 선언 상태로 되돌리려 함
+
+즉, `apps/auth-server/overlays/dev`를 수정한다는 것은 단순히 파일을 고치는 것이 아니라
+**Argo CD가 나중에 실제 클러스터 상태를 바꾸게 될 선언을 수정하는 것**입니다.
+
+이 세 가지(GitOps, Kustomize, Argo CD)가 맞물리는 전체 흐름을 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart LR
+ subgraph Developer["개발자"]
+ A["base/deployment.yaml 수정
또는 overlay/configmap.yaml 수정"]
+ end
+
+ subgraph Git["Git 저장소 (Source of Truth)"]
+ B["base/ + overlay/
= 최종 선언"]
+ end
+
+ subgraph ArgoCD["Argo CD"]
+ C["Git 감시
변경 감지"] --> D["Kustomize로
base + overlay 합성"]
+ D --> E["합성 결과와
현재 클러스터 비교"]
+ end
+
+ subgraph Cluster["K8s 클러스터"]
+ F["실제 리소스
Deployment, Service 등"]
+ end
+
+ A -->|"git push"| B
+ B -->|"Watch"| C
+ E -->|"차이 있으면
kubectl apply"| F
+ F -->|"드리프트 발생 시
다시 선언으로 복원"| E
+```
+
+개발자는 Git만 수정합니다. 클러스터를 직접 건드리지 않습니다. Argo CD가 Git의 선언과 클러스터의 실제 상태를 계속 비교하고, 차이가 있으면 선언 쪽으로 맞춥니다. 만약 누군가 `kubectl edit`으로 클러스터를 직접 수정하면, Argo CD가 그것을 "드리프트"로 감지하고 Git 기준으로 되돌립니다.
+
+### 5-5. Secret 관리: Kubernetes Secret, SealedSecret, Vault
+
+이 프로젝트의 핵심 설계 중 하나는 "무엇을 어디에 저장할 것인가"입니다.
+
+#### Kubernetes Secret
+
+Kubernetes Secret은 Kubernetes 안에서 secret을 다루기 위한 기본 기능입니다.
+하지만 이 프로젝트에서는 runtime secret의 최종 해답으로 보지 않습니다.
+
+이유:
+
+- 클러스터 안에 secret 복사본이 많이 생기기 쉽다
+- GitOps 저장소에 그대로 두기 어렵다
+- 장기 자격증명을 쉽게 만들 수 있다
+
+#### SealedSecret
+
+SealedSecret은 "Git에 올릴 수 있게 암호화된 Secret"입니다.
+
+이 프로젝트에서 SealedSecret이 남아 있는 이유:
+
+- `ghcr-regcred` 같은 image pull secret은 **Pod가 뜨기 전**에 필요하다
+- Vault Agent는 Pod 생성 이후에 동작한다
+- 즉, 이미지 pull credential은 Vault injection만으로 해결할 수 없다
+
+그래서 이 프로젝트는 아래처럼 분리합니다.
+
+- image pull secret: SealedSecret
+- runtime secret: Vault
+
+#### Vault
+
+Vault는 "비밀값을 저장하는 곳"이면서 동시에 "권한을 기준으로 필요한 순간에만 비밀을 주는 곳"입니다.
+
+이 프로젝트에서 Vault를 쓰는 이유:
+
+- 앱마다 필요한 secret만 읽게 하기 위해
+- 장기 비밀번호를 Git에서 제거하기 위해
+- dynamic DB credential을 발급하기 위해
+- JWT 서명을 key file 없이 transit으로 처리하기 위해
+
+#### Secret Zero Problem
+
+"Vault에 로그인하려면 처음에 무엇으로 인증하나?"라는 질문이 바로 Secret Zero Problem입니다.
+
+이 프로젝트의 해법은 두 가지입니다.
+
+- 앱/플랫폼 Pod: Kubernetes auth 사용
+- CI / bootstrap automation: AppRole 사용
+
+즉,
+
+- Pod는 자기 ServiceAccount JWT로 신분을 증명하고
+- CI는 별도의 AppRole credential로 로그인합니다
+
+이 두 경로를 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart TB
+ subgraph Pod_경로["경로 1: Pod가 Vault에 접근할 때"]
+ direction LR
+ P1["Pod 내부의
ServiceAccount JWT"] -->|"자동 마운트됨"| P2["Vault Agent가
JWT를 들고 인증"]
+ P2 --> P3["Vault가 K8s API에
'이 JWT 진짜야?' 확인"]
+ P3 --> P4["Secret 발급"]
+ end
+
+ subgraph CI_경로["경로 2: CI가 Vault에 접근할 때"]
+ direction LR
+ C1["GitHub Actions
Secrets에 저장된
Role ID + Secret ID"] --> C2["AppRole 로그인"]
+ C2 --> C3["Vault 토큰 발급"]
+ C3 --> C4["Terraform 실행"]
+ end
+```
+
+Pod 경로에서는 **K8s가 이미 부여한 신분(ServiceAccount)**을 재활용합니다. 별도의 비밀번호가 필요 없습니다. CI 경로에서는 **GitHub Actions의 Secrets 기능**이 Secret Zero를 담당합니다. 완벽하지는 않지만, 비밀번호를 코드에 직접 쓰는 것보다 훨씬 안전합니다.
+
+> 💡 각 인증 방식의 핸드셰이크 상세는 바로 아래 5-6에서, 그리고 프로세스 수준의 동작은 26장에서 다룹니다.
+
+### 5-6. Kubernetes auth, AppRole, Transit, Dynamic Secret
+
+이 네 개념은 이 저장소를 이해할 때 반드시 구분해야 합니다.
+
+| 개념 | 무엇인가 | 누가 쓰는가 | 이 프로젝트의 예시 |
+| --------------- | ------------------------------------------- | ----------------------------- | -------------------------------------- |
+| Kubernetes auth | Pod의 ServiceAccount로 Vault 로그인 | 앱/플랫폼 Pod | `auth-server`, `postgres`, `keycloak` |
+| AppRole | 기계용 Vault 로그인 방식 | CI, bootstrap, 운영 자동화 | `vault-dev-reconcile` workflow |
+| Transit | 키를 직접 밖으로 꺼내지 않고 암호 연산 제공 | auth-server, auto-unseal 구조 | JWT signing, workload Vault unseal |
+| Dynamic secret | 일정 시간이 지나면 만료되는 자격증명 | migration Job, operator | `database/creds/auth-db-migration-dev` |
+
+이 네 가지를 섞어 생각하면 안 됩니다.
+
+- Kubernetes auth는 "Pod용 로그인 방식"
+- AppRole은 "비Pod 자동화용 로그인 방식"
+- Transit은 "암호 기능"
+- Dynamic secret은 "짧게 사는 계정"
+
+이 네 가지가 이 프로젝트의 어디에서 쓰이는지 한 눈에 보면 이렇습니다.
+
+```mermaid
+flowchart TB
+ subgraph Vault["Workload Vault"]
+ KV["KV Engine
(고정 secret 저장)"]
+ DB["Database Engine
(동적 계정 발급)"]
+ TR["Transit Engine
(암호 연산)"]
+ KA["K8s Auth
(Pod 로그인)"]
+ AR["AppRole Auth
(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으로
policy/role/secret 설정"| KV
+```
+
+auth-server는 K8s Auth로 로그인해서 KV(고정 비밀번호)와 Transit(JWT 서명)을 사용합니다. migration Job은 K8s Auth로 로그인해서 Database Engine(임시 계정)을 사용합니다. CI는 AppRole로 로그인해서 Terraform으로 설정을 관리합니다. **같은 Vault지만 로그인 방식과 사용하는 엔진이 다릅니다.**
+
+> 💡 각 엔진과 인증 방식의 내부 동작은 26장에서 상세히 다룹니다.
+
+### 5-7. Terraform, State, 멱등성, Bootstrap vs Reconcile
+
+Terraform은 단순히 "리소스를 만드는 도구"가 아닙니다.
+핵심은 **현재 상태와 원하는 상태의 차이를 계산한다**는 점입니다.
+
+#### State
+
+Terraform state는 Terraform이 "내가 무엇을 만들었는지" 기억하는 파일입니다.
+
+이 프로젝트에서 state가 중요한 이유:
+
+- Vault 안에는 정책, auth backend, AppRole, database role 등 많은 리소스가 있다
+- 이것을 사람이 매번 수동으로 비교하는 것은 어렵다
+- Terraform이 state를 바탕으로 diff를 계산해야 반복 적용이 안전해진다
+
+#### 멱등성(Idempotency)
+
+멱등성이란 "같은 작업을 여러 번 해도 결과가 같게 유지되는 성질"입니다.
+
+이 프로젝트에서 왜 중요한가?
+
+- GitHub Actions workflow는 반복 실행될 수 있다
+- Vault reconcile도 여러 번 돌아야 한다
+- 같은 apply가 다시 실행되더라도 리소스가 중복 생성되면 안 된다
+
+#### Bootstrap vs Reconcile
+
+이 프로젝트는 Terraform 루트를 일부러 나눴습니다.
+
+- bootstrap 루트
+ 최초 1회, 강한 권한으로 구조를 세우는 용도
+- reconcile 루트
+ 이미 세워진 구조를 반복적으로 안전하게 맞추는 용도
+
+왜 이렇게 나눌까?
+
+- CI에 root 수준 권한을 오래 주지 않기 위해
+- 최초 생성과 일상 동기화의 책임을 분리하기 위해
+- 사람이 승인해야 할 작업과 자동화가 해도 되는 작업을 구분하기 위해
+
+이 개념을 먼저 이해해야 뒤에서 나오는:
+
+- `terraform/vault/dev`
+- `terraform/vault/reconcile`
+- `terraform/vault-transit/dev`
+- `terraform/vault-transit/reconcile`
+
+이 네 디렉터리의 의미가 선명해집니다.
+
+이 분리를 권한 경계 관점에서 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart LR
+ subgraph Bootstrap["Bootstrap (최초 1회, 사람이 실행)"]
+ direction TB
+ B1["vault/dev
mount 생성, auth backend 활성화
root 수준 권한 필요"]
+ B2["vault-transit/dev
transit key 생성, AppRole 생성
root 수준 권한 필요"]
+ end
+
+ subgraph Reconcile["Reconcile (반복, CI가 실행)"]
+ direction TB
+ R1["vault/reconcile
policy 업데이트, role 업데이트
secret 복사, DB role 설정
제한된 권한으로 충분"]
+ R2["vault-transit/reconcile
policy 업데이트, role 업데이트
제한된 권한으로 충분"]
+ end
+
+ Bootstrap -->|"구조가 세워진 뒤
이후는 reconcile만"| Reconcile
+```
+
+핵심은 **CI(GitHub Actions)가 root 토큰을 갖지 않는다**는 것입니다. Bootstrap은 운영자가 직접, 한 번만 실행합니다. 이후 CI는 reconcile 루트만 반복 실행하며, 최소한의 권한으로 기존 구조를 유지보수합니다.
+
+> 💡 Terraform의 State 관리, Plan/Apply 사이클, Provider 이중 설정 등 내부 메커니즘은 27장에서 상세히 다룹니다.
+
+## 6. 이 저장소의 큰 흐름
+
+```mermaid
+flowchart TD
+ A[App repo CI] --> B[GitOps repo image tag update]
+ C[main push or infra change] --> D[.github/workflows/vault-dev-reconcile.yaml]
+ D --> E[scripts/ci/reconcile-vault-dev.sh]
+ E --> F[terraform/vault-transit/reconcile]
+ E --> G[terraform/vault/reconcile]
+ E --> H[argocd/applications/dev]
+ H --> I[Argo CD sync]
+ I --> J[apps/auth-server]
+ I --> K[apps/api-server]
+ I --> L[infra/platform]
+ I --> M[infra/vault]
+ I --> N[infra/vault-transit]
+ N --> M
+ M --> J
+ M --> L
+ J --> K
+```
+
+이 그림을 문장으로 풀면 아래와 같습니다.
+
+1. 앱 저장소 CI가 새 이미지를 만들면 이 저장소의 overlay 이미지 태그를 바꿉니다.
+2. 이 저장소의 `main`에 변경이 들어오면 `vault-dev-reconcile` 워크플로가 실행됩니다.
+3. 워크플로는 Bash 스크립트를 통해 먼저 Vault 관련 상태를 맞춥니다.
+4. 그 다음 Argo CD Application 정의를 적용합니다.
+5. Argo CD가 실제 `apps/`와 `infra/` 폴더를 읽어 클러스터 상태를 맞춥니다.
+6. 앱 Pod는 최종적으로 workload Vault에서 secret을 받아 기동합니다.
+
+## 7. 폴더 지도
+
+| 경로 | 역할 | 여기서 반드시 이해해야 하는 것 |
+| ----------------------------------- | --------------------------------------- | --------------------------------------------------- |
+| `apps/auth-server` | 인증 서버 배포 정의 | DB migration, Vault injection, OAuth/Keycloak 연동 |
+| `apps/api-server` | API 서버 배포 정의 | auth-server가 발급한 JWT를 검증하는 구조 |
+| `infra/platform` | Postgres, Keycloak, Keycloak sync 정의 | 앱이 의존하는 플랫폼 계층 |
+| `infra/vault` | workload Vault 배포 정의 | 앱이 직접 접근하는 Vault |
+| `infra/vault-transit` | provider Vault 배포 정의 | workload Vault auto-unseal 지원 |
+| `argocd/applications/dev` | Argo CD가 어떤 경로를 적용할지 정의 | sync 순서와 대상 namespace |
+| `scripts/ci` | GitHub Actions가 호출하는 자동화 진입점 | reconcile 순서와 안전장치 |
+| `scripts/vault/dev` | workload Vault bootstrap/reconcile 보조 | provider Vault에서 bootstrap 정보를 읽는 방식 |
+| `scripts/vault-transit/dev` | provider Vault bootstrap 보조 | seed secret 입력, seal token 준비 |
+| `terraform/vault/dev` | workload Vault 최초 bootstrap용 루트 | mount, auth backend, policy, role까지 만든다 |
+| `terraform/vault/reconcile` | workload Vault routine reconcile용 루트 | CI가 반복 적용하는 루트 |
+| `terraform/vault-transit/dev` | provider Vault 최초 bootstrap용 루트 | transit key, AppRole, seal Secret 생성 |
+| `terraform/vault-transit/reconcile` | provider Vault routine reconcile용 루트 | CI가 반복 적용하는 provider 쪽 루트 |
+| `runbooks/vault/**` | 사람이 직접 bootstrap할 때 보는 문서 | 왜 bootstrap과 reconcile이 분리됐는지 이해해야 한다 |
+
+## 8. 파일을 읽을 때 항상 던져야 하는 5가지 질문
+
+이 저장소의 어떤 파일이든 아래 5가지 질문으로 읽으면 이해가 훨씬 빨라집니다.
+
+1. 이 파일은 **누가 적용하는가**?
+2. 이 파일은 **누가 소비하는가**?
+3. 이 파일의 값은 **민감한 값인가 아닌가**?
+4. 이 파일은 **최초 1회 bootstrap용인가**, 아니면 **반복 실행되는 reconcile용인가**?
+5. 이 파일을 잘못 바꾸면 **어디가 먼저 깨지는가**?
+
+예를 들어 `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`을 볼 때는 이렇게 읽어야 합니다.
+
+- 누가 적용하는가: Argo CD
+- 누가 소비하는가: Vault Agent Injector와 최종적으로 auth-server 컨테이너
+- 민감한 값인가: 파일 자체는 secret을 담지 않지만 secret 경로를 지정한다
+- bootstrap용인가 reconcile용인가: reconcile 이후 실제 앱 배포에 쓰이는 runtime 정의다
+- 잘못 바꾸면 어디가 깨지는가: Vault secret render 실패, Pod 기동 실패, 로그인 기능 장애
+
+## 9. 대표 시나리오 1: auth-server가 DB 비밀번호를 받는 과정
+
+이 시나리오를 이해하면 이 프로젝트의 핵심을 절반 이상 이해한 것입니다.
+
+### 9-1. 실제 흐름
+
+1. 운영자 또는 초기 bootstrap 절차가 `scripts/vault-transit/dev/populate-workload-seeds.sh`를 실행합니다.
+2. 이 스크립트는 provider Vault의 `kv/dev/workload/platform/postgres/auth-server` 경로에 값을 넣습니다.
+3. `terraform/vault/reconcile/main.tf`는 provider Vault에서 이 값을 읽습니다.
+4. 같은 Terraform이 workload Vault의 `kv/data/dev/platform/postgres/auth-server` 경로로 값을 복사합니다.
+5. `runbooks/vault/dev/policies/auth-server-dev.hcl`는 auth-server가 그 경로를 읽을 수 있게 허용합니다.
+6. `terraform/vault/reconcile/main.tf`는 `auth-dev` namespace의 `auth-server` ServiceAccount에 이 정책을 연결하는 Kubernetes auth role도 만듭니다.
+7. `apps/auth-server/overlays/dev/namespace.yaml`에는 `vault-injection: enabled` 라벨이 있어서 injector webhook 대상이 됩니다.
+8. `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`는 Vault에게 어떤 secret을 어디서 읽을지, 어떤 role로 로그인할지 알려줍니다.
+9. Pod가 뜰 때 Vault Agent가 `/vault/secrets/runtime-env` 파일을 만들어 줍니다.
+10. auth-server 컨테이너는 그 파일을 `source`한 뒤 `java -jar /app/application.jar`로 기동합니다.
+
+이 10단계를 하나의 그림으로 보면 이렇습니다.
+
+```mermaid
+sequenceDiagram
+ participant OP as 운영자/Bootstrap
+ participant PV as Provider Vault
(vault-transit)
+ participant TF as Terraform
+ participant WV as Workload Vault
+ participant API as K8s API Server
+ participant INJ as Vault Agent Injector
+ participant POD as auth-server Pod
+
+ Note over OP,PV: 1~2단계: Seed 입력
+ OP->>PV: populate-workload-seeds.sh로
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 사이드카 추가
+ INJ-->>API: 수정된 Pod 정의 반환
+ API->>POD: Pod 생성 (원래 컨테이너 + Vault Agent)
+ POD->>WV: K8s SA JWT로 인증 → secret 요청
+ WV-->>POD: /vault/secrets/runtime-env 파일 생성
+ POD->>POD: source runtime-env → exec java
+```
+
+이 그림에서 왼쪽(1~2단계)은 **사람이 최초 1회 하는 일**, 가운데(3~6단계)는 **CI가 반복하는 일**, 오른쪽(7~10단계)는 **매 배포마다 자동으로 일어나는 일**입니다.
+
+> 💡 각 단계의 내부 동작(Mutating Webhook, K8s Auth Handshake, Go 템플릿 렌더링 등)은 26장에서 상세히 다룹니다.
+
+### 9-2. 여기서 꼭 이해해야 하는 파일
+
+| 파일 | 역할 | 왜 중요한가 |
+| ----------------------------------------------------------- | --------------------------------------- | ---------------------------------------------------------- |
+| `scripts/vault-transit/dev/populate-workload-seeds.sh` | 최초 seed 입력 | secret의 진짜 출발점이다 |
+| `terraform/vault/reconcile/main.tf` | provider Vault -> workload Vault 동기화 | secret이 어디로 복사되는지 결정한다 |
+| `runbooks/vault/dev/policies/auth-server-dev.hcl` | 읽기 권한 제한 | auth-server가 무엇을 읽을 수 있는지 최소 권한으로 제한한다 |
+| `apps/auth-server/overlays/dev/deployment.vault-patch.yaml` | Pod 기동 시 secret 주입 설정 | secret이 파일로 렌더되고 실행 시점에 읽힌다 |
+
+### 9-3. 왜 이렇게 복잡하게 하나
+
+단순히 Kubernetes Secret에 DB 비밀번호를 넣어도 앱은 뜹니다. 그런데 이 프로젝트는 그렇게 하지 않습니다.
+
+- secret 원본을 Git에 두지 않기 위해
+- 사람이 직접 장기 토큰을 여기저기 복붙하지 않기 위해
+- Pod마다 필요한 secret만 읽게 하기 위해
+- 이후 동적 계정, transit 서명, 최소 권한 같은 보안 정책을 일관되게 적용하기 위해
+
+### 9-4. 잘못 건드렸을 때 생기는 부작용
+
+| 잘못된 변경 | 바로 생기는 문제 | 더 큰 문제 |
+| ------------------------------------------------------- | ---------------------------------------- | --------------------------------------------------- |
+| policy 경로를 너무 좁게 적음 | auth-server가 secret을 못 읽고 기동 실패 | 배포는 성공한 것처럼 보여도 앱은 죽는다 |
+| policy 경로를 너무 넓게 적음 | 당장은 잘 동작 | auth-server 침해 시 읽을 수 있는 비밀 범위가 커진다 |
+| patch에서 `automountServiceAccountToken: true`를 빼먹음 | Vault Kubernetes auth 실패 | injector가 secret을 렌더링하지 못한다 |
+| secret을 `ConfigMap`으로 옮김 | 당장은 쉬워 보임 | 민감한 값이 GitOps 정의 안으로 들어온다 |
+
+## 10. 대표 시나리오 2: 왜 auth-db-migration은 Job인가
+
+`auth-server`는 앱이 하나만 있는 것이 아니라, **DB migration Job + 앱 Deployment** 두 가지로 구성됩니다.
+
+### 10-1. 관련 파일
+
+| 파일 | 역할 |
+| ----------------------------------------------------------------- | -------------------------------------------- |
+| `apps/auth-server/base/db-migration-job.yaml` | 실제 migration Job 정의 |
+| `apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml` | migration Job에 Vault 기반 동적 DB 계정 주입 |
+| `apps/auth-server/base/deployment.yaml` | 실제 API 서버 프로세스 |
+
+### 10-2. 왜 Job인가
+
+DB schema 변경은 "앱이 떠 있는 동안 계속 돌고 있어야 하는 프로세스"가 아닙니다.
+
+- 한 번 실행해서 끝나야 합니다.
+- 앱보다 먼저 끝나야 합니다.
+- 실패하면 앱이 뜨기 전에 문제를 알리는 것이 더 안전합니다.
+
+그래서 `db-migration-job.yaml`에는 아래 Argo CD 어노테이션이 들어 있습니다.
+
+- `argocd.argoproj.io/hook: PreSync`
+- `argocd.argoproj.io/sync-wave: "-1"`
+
+의미는 이렇습니다.
+
+- 일반 리소스보다 먼저 돈다
+- 성공하면 지워도 된다
+- 성공 전에는 뒤에 있는 앱 배포가 진행되지 않도록 앞단에서 멈춘다
+
+이 타이밍을 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart LR
+ subgraph PreSync["① PreSync 단계"]
+ M["db-migration Job
sync-wave: -1"] --> MC{"Migration
결과"}
+ MC -->|"성공 ✅"| NEXT["다음 단계로"]
+ MC -->|"실패 ❌"| STOP["전체 sync 중단
앱 배포 안 함"]
+ end
+
+ subgraph Sync["② Sync 단계"]
+ D["auth-server Deployment
앱 배포"]
+ end
+
+ NEXT --> D
+```
+
+Migration Job이 성공해야만 auth-server Deployment가 배포됩니다. Migration이 실패하면 앱 배포가 아예 진행되지 않아서, schema가 안 맞는 상태로 앱이 뜨는 위험을 방지합니다.
+
+> 💡 Flyway의 lock 메커니즘과 Job 분리 전략의 상세는 24장에서 다룹니다.
+
+
+### 10-3. 왜 동적 계정을 쓰는가
+
+`apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml`를 보면 migration Job은 `database/creds/auth-db-migration-dev`를 읽습니다.
+
+이것은 KV에 저장된 고정 비밀번호가 아니라, **Vault database engine이 짧은 TTL을 가진 계정을 그때그때 발급**한다는 뜻입니다.
+
+이 구조를 택한 이유는 다음과 같습니다.
+
+- migration은 고권한 작업일 수 있으므로 장기 계정을 남기고 싶지 않다
+- Job은 짧게 실행되므로 동적 계정과 잘 맞는다
+- 누가 언제 어떤 계정을 발급받았는지 추적하기 쉽다
+
+### 10-4. 이 설계에서 고민해야 할 점
+
+| 고민 | 왜 해야 하는가 | 이 저장소의 답 | 잘못 선택했을 때 |
+| ------------------------------------------ | ---------------------------------------------------------- | ------------------ | ------------------------------- |
+| migration을 앱 시작 로직에 넣을까? | 서버 여러 대가 동시에 뜨면 schema 변경 경쟁이 생길 수 있다 | 별도 Job으로 분리 | 동시 실행, 락 충돌, 배포 불안정 |
+| migration 계정을 고정할까 동적으로 만들까? | 장기 계정이 유출되면 피해가 길어진다 | 동적 계정 사용 | 유출 시 회수와 추적이 어려움 |
+| 실패 시 앱도 뜨게 할까? | schema가 안 맞는데 앱이 뜨면 더 큰 장애를 만든다 | PreSync에서 막는다 | 런타임 예외, 데이터 손상 가능성 |
+
+## 11. 대표 시나리오 3: auth-server가 JWT를 서명하는 과정
+
+이 부분은 처음 보면 특히 헷갈립니다. auth-server는 단순히 DB secret만 읽는 것이 아닙니다. **Vault Transit을 이용해 JWT 서명도 수행**합니다.
+
+### 11-1. 관련 파일
+
+| 파일 | 역할 |
+| ----------------------------------------------------------- | -------------------------------------------------------------------------- |
+| `apps/auth-server/overlays/dev/configmap.yaml` | auth-server가 Vault transit으로 JWT를 다룬다는 설정 제공 |
+| `apps/auth-server/overlays/dev/deployment.vault-patch.yaml` | Vault token file을 컨테이너로 전달 |
+| `runbooks/vault/dev/policies/auth-server-dev.hcl` | `transit/keys/project-auth-jwt`, `transit/sign/project-auth-jwt` 접근 허용 |
+| `terraform/vault/dev/main.tf` | `project-auth-jwt` transit key 생성 |
+
+### 11-2. 핵심 이해 포인트
+
+- auth-server는 로컬에서 개인키를 직접 생성하지 않습니다.
+- Vault transit engine에 "이 key로 서명해 달라"고 요청합니다.
+- 그래서 애플리케이션 컨테이너 안에 장기 서명 키 파일이 존재하지 않습니다.
+
+이 과정을 그림으로 보면 이렇습니다.
+
+```mermaid
+sequenceDiagram
+ participant User as 사용자
+ participant Auth as auth-server
+ participant Vault as Workload Vault
(Transit Engine)
+ participant API as api-server
+
+ User->>Auth: "로그인 해줘"
+ Auth->>Auth: 사용자 확인, JWT 페이로드 준비
+ Auth->>Vault: "transit/sign/project-auth-jwt로
이 페이로드에 서명해줘"
+ Note over Vault: 내부의 RSA 개인키로 서명
개인키는 Vault 밖으로 절대 안 나감
+ Vault-->>Auth: 서명된 JWT 반환
+ Auth-->>User: JWT 토큰 전달
+
+ User->>API: JWT를 담아 API 호출
+ API->>Vault: "transit/keys/project-auth-jwt로
공개키 읽기"
+ Vault-->>API: RSA 공개키 반환
+ API->>API: 공개키로 JWT 서명 검증
+ API-->>User: API 응답
+```
+
+핵심은 **auth-server가 개인키를 한 번도 본 적이 없다**는 것입니다. 서명이 필요할 때 Vault API를 호출하고, Vault가 내부에서 서명한 결과만 돌려줍니다. api-server는 공개키만 가져와서 검증합니다.
+
+이 방식은 운영 난이도는 조금 올라가지만, **키 보관을 중앙화**하고 **키 유출 위험을 줄이는 장점**이 있습니다.
+
+> 💡 Transit Engine의 키 Rotation, 이중 역할(JWT 서명 + Auto-Unseal), 성능 트레이드오프는 26장에서 상세히 다룹니다.
+
+## 12. auth-server 파일을 어떻게 읽어야 하는가
+
+### 12-1. `apps/auth-server/base/kustomization.yaml`
+
+이 파일은 auth-server를 구성하는 공통 리소스 목록입니다.
+
+- `serviceaccount.yaml`
+- `auth-db-migration-serviceaccount.yaml`
+- `service.yaml`
+- `db-migration-job.yaml`
+- `deployment.yaml`
+
+이 단계에서 꼭 봐야 하는 것은 "auth-server는 서비스 하나가 아니라 **서비스 + migration job**으로 구성된다"는 사실입니다.
+
+### 12-2. `apps/auth-server/base/deployment.yaml`
+
+이 파일은 secret 주입이 없는 기본 뼈대입니다. 여기서 눈여겨볼 지점은 다음과 같습니다.
+
+| 항목 | 의미 | 왜 이렇게 했는가 |
+| ------------------------------------------------- | ------------------------------------ | --------------------------------------------------------------- |
+| `serviceAccountName: auth-server` | 이 Pod의 Kubernetes 신분 | 나중에 Vault Kubernetes auth와 연결된다 |
+| `automountServiceAccountToken: false` | 기본적으로 SA 토큰을 Pod에 넣지 않음 | 필요한 경우에만 토큰을 노출하려는 보안 기본값 |
+| `runAsNonRoot`, `seccompProfile` | 보안 기본 설정 | root 실행과 과도한 시스템 호출을 줄인다 |
+| `envFrom`의 `configMapRef`와 `secretRef` | 기본 설계상 설정/비밀을 받는 자리 | dev에서는 overlay patch로 secret 부분이 Vault 방식으로 대체된다 |
+| `readinessProbe`, `livenessProbe`, `startupProbe` | 준비/생존/초기 부팅 상태 확인 | 느린 기동과 장애를 구분하기 위함 |
+
+중요한 포인트는 **base의 값이 최종값이 아닐 수 있다**는 것입니다. overlay patch가 들어오면 일부 항목은 바뀝니다.
+
+### 12-3. `apps/auth-server/overlays/dev/kustomization.yaml`
+
+이 파일은 dev 환경에서 auth-server가 실제로 어떤 모습으로 배포되는지 정합니다.
+
+주요 포인트:
+
+- namespace는 `auth-dev`
+- dev 전용 `configmap`, `ingress`, `networkpolicy`, `sealedsecret`를 추가
+- `deployment.vault-patch.yaml`, `db-migration-job.vault-patch.yaml`로 base를 덮어씀
+- 이미지 태그는 여기서 관리됨
+
+즉, 실제 dev 배포를 이해하려면 **base만 보면 안 되고 overlay까지 합쳐서 봐야** 합니다.
+
+이 합성 과정을 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart TD
+ subgraph Base["base/ (공통 뼈대)"]
+ B1["deployment.yaml
automountServiceAccountToken: false
command/args: 없음"]
+ B2["service.yaml"]
+ B3["serviceaccount.yaml"]
+ end
+
+ subgraph Overlay["overlays/dev/ (환경별 차이)"]
+ O1["deployment.vault-patch.yaml
automountServiceAccountToken: true
command/args: 추가"]
+ O2["configmap.yaml (새로 추가)"]
+ O3["networkpolicy.yaml (새로 추가)"]
+ O4["namespace: auth-dev"]
+ end
+
+ subgraph Result["최종 배포 결과 (Kustomize 합성)"]
+ R1["deployment.yaml
automountServiceAccountToken: true ← patch로 변경
command/args: Vault 시작 명령 추가"]
+ end
+
+ B1 --> R1
+ O1 -->|"패치 적용
(strategic merge)"| R1
+```
+
+base에서 `automountServiceAccountToken: false`이지만, overlay patch가 이것을 `true`로 덮어씁니다. **즉, base만 보고 "토큰이 안 들어가네"라고 판단하면 틀립니다.** overlay까지 합쳐야 실제 동작을 알 수 있습니다.
+
+### 12-4. `apps/auth-server/overlays/dev/configmap.yaml`
+
+이 파일은 민감하지 않은 값만 둡니다.
+
+대표 항목:
+
+- `APP_DATASOURCE_URL`
+- `APP_SECURITY_OAUTH2_KEYCLOAK_ISSUER_URI`
+- `APP_SECURITY_JWT_ISSUER`
+- `APP_SECURITY_JWT_VAULT_ENABLED`
+- `APP_SECURITY_JWT_VAULT_ADDRESS`
+- `APP_SECURITY_JWT_VAULT_MOUNT_PATH`
+
+여기서 중요한 기준은 간단합니다.
+
+- 값이 공개되어도 치명적이지 않으면 `ConfigMap`
+- 비밀번호, client secret, token처럼 노출되면 안 되면 Vault
+
+### 12-5. `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`
+
+이 파일은 dev auth-server의 핵심입니다.
+
+반드시 이해해야 하는 항목:
+
+| 항목 | 의미 | 놓치면 안 되는 이유 |
+| ------------------------------------------- | ----------------------------------------------- | -------------------------------------------------------- |
+| `vault.hashicorp.com/agent-inject: "true"` | injector가 이 Pod를 가로채 secret 파일을 만든다 | 이 값이 없으면 Vault 주입이 시작되지 않는다 |
+| `agent-inject-secret-runtime-env` | 어떤 Vault 경로에서 값을 읽을지 | secret 출처를 정확히 가리킨다 |
+| `agent-inject-template-runtime-env` | secret을 쉘 `export` 형식으로 렌더링 | 앱이 `. /vault/secrets/runtime-env` 로 바로 읽을 수 있다 |
+| `vault.hashicorp.com/role: auth-server-dev` | Vault 로그인 시 사용할 역할 이름 | policy와 Kubernetes auth role 이름이 연결된다 |
+| `automountServiceAccountToken: true` | 이 Pod만 예외적으로 SA 토큰 허용 | Vault가 Pod 신분을 검증하려면 필요하다 |
+| `command`, `args` 재정의 | Java 실행 전에 secret 파일과 token file을 읽음 | Vault 주입 결과를 프로세스 환경에 연결한다 |
+
+이 파일을 읽을 때 꼭 이해해야 하는 역설이 하나 있습니다.
+
+- base에서는 `automountServiceAccountToken: false`
+- overlay patch에서는 `automountServiceAccountToken: true`
+
+왜 이런 모순처럼 보이는 구조를 쓰는가?
+
+- 기본값은 "토큰을 넣지 않는다"가 더 안전하기 때문
+- 하지만 Vault Kubernetes auth를 쓰는 특정 Pod는 로그인에 토큰이 필요하기 때문
+- 그래서 **필요한 워크로드에서만 예외를 허용**한다
+
+이게 바로 "보안 기본값은 닫고, 필요한 곳만 연다"는 설계입니다.
+
+### 12-6. `apps/auth-server/overlays/dev/db-migration-job.vault-patch.yaml`
+
+이 파일은 더 중요합니다. migration Job은 고정 secret이 아니라 동적 DB 계정을 받기 때문입니다.
+
+핵심 포인트:
+
+- `database/creds/auth-db-migration-dev` 사용
+- `agent-pre-populate-only: "true"` 사용
+- 컨테이너 시작 전에 secret 파일을 만들어두고, 사이드카를 계속 띄우지 않음
+
+왜 `pre-populate-only`가 어울리는가?
+
+- Job은 짧게 실행되고 끝난다
+- secret을 한 번 받아 실행하면 충분하다
+- 장시간 sidecar를 유지할 필요가 없다
+
+### 12-7. `apps/auth-server/overlays/dev/networkpolicy.yaml`
+
+이 파일은 "막아두고 필요한 것만 연다"는 철학을 가장 잘 보여줍니다.
+
+구조는 다음과 같습니다.
+
+- 기본적으로 ingress, egress 모두 차단
+- DNS만 허용
+- Postgres와 Vault로 가는 egress 허용
+- Traefik에서 들어오는 ingress 허용
+
+이 파일을 수정할 때는 항상 이 질문을 해야 합니다.
+
+- 새로 필요한 네트워크 경로가 정말 있는가?
+- 그 경로는 어느 namespace, 어느 label, 어느 port인가?
+- DNS는 이미 열려 있는가?
+
+가장 흔한 실수는 "앱이 안 뜬다"는 문제를 보고 Deployment만 수정하는 것입니다. 실제 원인은 NetworkPolicy일 수 있습니다.
+
+### 12-8. `namespace.yaml`, `public-access.yaml`, `ghcr-regcred.sealedsecret.yaml`
+
+이 세 파일은 초보자가 자주 지나치지만, 실제로는 구조 이해에 매우 중요합니다.
+
+`apps/auth-server/overlays/dev/namespace.yaml`
+
+- `auth-dev` namespace를 만든다
+- `vault-injection: enabled` 라벨을 준다
+- Pod Security 관련 라벨도 같이 준다
+
+여기서 중요한 것은 `vault-injection: enabled`입니다.
+`argocd/applications/dev/infra/vault-agent-injector.yaml`를 보면 injector webhook은 **이 라벨이 있는 namespace에만** 동작합니다.
+
+즉, auth-server가 Vault injection을 받는 이유는 단순히 Deployment patch 때문만이 아니라, **namespace도 injector 대상 조건을 만족**하기 때문입니다.
+
+반대로 `apps/api-server/overlays/dev/namespace.yaml`에는 이 라벨이 없습니다. api-server는 Vault injection을 쓰지 않기 때문입니다.
+
+`apps/auth-server/overlays/dev/public-access.yaml`
+
+- `auth-public`이라는 `ExternalName` Service를 만든다
+- 실제로는 `traefik.kube-system.svc.cluster.local`을 가리킨다
+
+이 구조를 두는 이유는 "클러스터 내부에서도 public host 기준으로 접근하게 만들기 위해서"입니다.
+
+예를 들어 issuer URI나 callback URL은 public host 기준으로 맞추는 편이 일관성이 좋습니다. 그때 내부 Pod가 그 host를 해석했을 때도 Traefik으로 가도록 `ExternalName`을 둡니다.
+
+`apps/auth-server/overlays/dev/ghcr-regcred.sealedsecret.yaml`
+
+- GHCR 이미지 pull secret을 암호화된 형태로 Git에 저장한다
+
+왜 runtime secret은 Vault로 옮기면서 이건 SealedSecret으로 남겼는가?
+
+- 이미지 pull secret은 **Pod가 뜨기 전에** 필요하다
+- Vault Agent는 Pod 생성 후에 동작한다
+- 즉, 이미지를 받기도 전에 필요한 자격증명은 Vault injection으로 해결할 수 없다
+
+이 차이를 이해해야 "왜 어떤 secret은 Vault고 어떤 secret은 SealedSecret이지?"라는 질문이 풀립니다.
+
+## 13. api-server는 auth-server와 무엇이 다른가
+
+`api-server`는 구조가 더 단순합니다. DB migration도 없고 Vault injection도 없습니다.
+
+### 13-1. 관련 파일
+
+| 파일 | 역할 |
+| ------------------------------------------------- | ----------------------------- |
+| `apps/api-server/base/deployment.yaml` | API 서버 기본 Deployment |
+| `apps/api-server/overlays/dev/configmap.yaml` | JWT issuer URI 등 비민감 설정 |
+| `apps/api-server/overlays/dev/networkpolicy.yaml` | Traefik 및 DNS 관련 통신 제어 |
+| `argocd/applications/dev/apps/api-server.yaml` | Argo CD 적용 대상 선언 |
+
+### 13-2. 꼭 봐야 할 포인트
+
+- `APP_SECURITY_OAUTH2_RESOURCESERVER_JWT_ISSUER_URI`가 `auth-public.auth-dev.svc.cluster.local`을 가리킨다
+- 즉, api-server는 auth-server가 발급한 토큰을 **검증하는 소비자**다
+- auth-server가 죽거나 issuer 주소가 틀리면 api-server의 인증 기능도 깨질 수 있다
+
+이 저장소를 읽을 때 흔히 하는 실수는 `api-server`를 완전히 독립된 서비스로 보는 것입니다. 하지만 인증 관점에서는 **auth-server에 의존**합니다.
+
+이 의존 관계를 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart LR
+ USER["브라우저"] --> API["api-server
JWT 검증만 수행"]
+ USER --> AUTH["auth-server
JWT 발급"]
+ AUTH -->|"OIDC"| KC["Keycloak"]
+ AUTH -->|"JWT 서명"| VAULT["Vault Transit"]
+ AUTH -->|"DB R/W"| PG["Postgres"]
+ API -->|"공개키 읽기
(issuer URI 경유)"| AUTH
+
+ style API fill:#e8f5e9
+ style AUTH fill:#fff3e0
+```
+
+api-server는 auth-server의 issuer URI를 통해 공개키를 가져와 JWT를 검증합니다. auth-server가 죽거나 issuer 주소가 바뀌면, api-server도 인증을 못 합니다.
+
+## 14. platform 계층을 같이 봐야 하는 이유
+
+`auth-server`만 보면 "DB URL이 왜 저기지?", "Keycloak client secret은 어디서 쓰이지?"가 남습니다. 그래서 `infra/platform`도 같이 봐야 합니다.
+
+### 14-1. Postgres
+
+`infra/platform/base/postgres-statefulset.yaml`과 `infra/platform/overlays/dev/postgres.vault-patch.yaml`를 같이 봐야 합니다.
+
+이 조합에서 확인해야 할 것:
+
+- 왜 `StatefulSet`인가
+- `AUTH_DB_PASSWORD`, `KEYCLOAK_DB_PASSWORD`도 Vault에서 주입받는가
+- 초기 DB 생성 스크립트는 어떻게 들어가는가
+
+여기서 배울 점은 "앱만 Vault를 쓰는 게 아니라 **플랫폼 컴포넌트도 Vault를 쓴다**"는 것입니다.
+
+### 14-2. Keycloak
+
+`infra/platform/base/keycloak-deployment.yaml`과 `infra/platform/overlays/dev/keycloak.vault-patch.yaml`를 보면 Keycloak도 DB 비밀번호와 bootstrap admin 비밀번호를 Vault에서 받습니다.
+
+이 구조를 이해해야 `auth-server`의 OAuth 설정이 왜 Keycloak과 맞물리는지 보입니다.
+
+### 14-3. Keycloak client sync Job
+
+`infra/platform/base/keycloak-client-sync-job.yaml`과 `infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml`는 "Keycloak client 설정도 코드로 맞춘다"는 것을 보여줍니다.
+
+이 Job이 하는 일:
+
+- Keycloak admin 계정으로 로그인
+- `project-auth-server` client를 찾음
+- client secret, base URL, redirect URI, web origins를 업데이트
+
+즉, 사람 손으로 Keycloak 콘솔을 클릭하지 않고 **코드와 Job으로 클라이언트 설정을 맞추는 구조**입니다.
+
+이 platform 계층의 의존 관계를 그림으로 정리하면 이렇습니다.
+
+```mermaid
+flowchart TB
+ VAULT["Workload Vault
(secret 제공)"] --> PG["Postgres
(StatefulSet)"]
+ VAULT --> KC["Keycloak
(Deployment)"]
+ PG --> KC_SYNC["Keycloak Client
Sync Job"]
+ KC --> KC_SYNC
+
+ PG --> AUTH["auth-server"]
+ KC --> AUTH
+ VAULT --> AUTH
+ KC_SYNC -.->|"client secret 설정
redirect URI 설정"| AUTH
+
+ AUTH -->|"JWT issuer"| API["api-server"]
+```
+
+Vault가 모든 컴포넌트에 secret을 제공하고, Postgres와 Keycloak이 auth-server의 기반이 되며, Keycloak client sync Job이 auth-server와 Keycloak 사이의 설정을 자동으로 맞춥니다. **앱(auth-server)만 보면 이 기반이 보이지 않습니다.**
+
+## 15. Argo CD는 정확히 무엇을 하는가
+
+앱과 인프라 YAML을 실제로 클러스터에 반영하는 주체는 Argo CD입니다.
+
+### 15-1. 꼭 읽어야 할 파일
+
+| 파일 | 의미 |
+| --------------------------------------------------------- | ----------------------------- |
+| `argocd/applications/dev/infra/vault-transit.yaml` | provider Vault 배포 선언 |
+| `argocd/applications/dev/infra/vault.yaml` | workload Vault 배포 선언 |
+| `argocd/applications/dev/infra/platform.yaml` | Postgres, Keycloak 배포 선언 |
+| `argocd/applications/dev/apps/auth-server.yaml` | auth-server dev 배포 선언 |
+| `argocd/applications/dev/apps/api-server.yaml` | api-server dev 배포 선언 |
+| `argocd/applications/dev/infra/vault-agent-injector.yaml` | injector Helm chart 배포 선언 |
+
+### 15-2. sync wave를 꼭 이해해야 하는 이유
+
+현재 dev 기준 순서는 대략 아래입니다.
+
+- Vault, Vault Transit, Vault Agent Injector: `10`
+- Platform: `20`
+- auth-server: `30`
+- api-server: `40`
+
+왜 이런 순서가 필요한가?
+
+- Vault와 injector가 먼저 있어야 secret 주입이 가능하다
+- platform이 먼저 있어야 auth-server가 붙을 Postgres와 Keycloak이 준비된다
+- auth-server가 먼저 있어야 api-server가 issuer를 안정적으로 참조할 수 있다
+
+순서를 잘못 잡으면 "코드는 맞는데 배포만 실패하는" 문제가 생깁니다.
+
+이 순서를 타임라인으로 보면 이렇습니다.
+
+```mermaid
+flowchart LR
+ W10["wave 10
Vault Transit
Vault
Agent Injector"] --> W20["wave 20
Platform
(Postgres, Keycloak)"]
+ W20 --> W30["wave 30
auth-server"]
+ W30 --> W40["wave 40
api-server"]
+
+ W10 -.->|"이것 없이 다음 단계로 가면
secret 주입 실패"| W20
+ W20 -.->|"이것 없이 다음 단계로 가면
DB 연결 실패"| W30
+ W30 -.->|"이것 없이 다음 단계로 가면
JWT 검증 실패"| W40
+```
+
+각 wave는 **이전 wave가 완료된 후에** 적용됩니다. wave 10이 완료되어야 Vault가 준비되고, wave 20이 완료되어야 Postgres가 준비되고, 그래야 auth-server가 DB와 Vault에 연결할 수 있습니다.
+
+## 16. GitHub Actions와 Bash 스크립트는 무엇을 하는가
+
+이 저장소에서 YAML만큼 중요한 것이 `scripts/`와 `.github/workflows/`입니다.
+
+### 16-1. `.github/workflows/vault-dev-reconcile.yaml`
+
+이 워크플로는 dev 환경의 routine reconcile 진입점입니다.
+
+주요 단계:
+
+1. 저장소 checkout
+2. 필수 도구 확인
+3. kubeconfig 설정
+4. Argo CD infra 정의 적용
+5. provider Vault reconcile
+6. workload Vault reconcile
+7. Argo CD app 정의 적용
+
+즉, 이 워크플로는 "앱 배포만" 하는 것이 아니라 **Vault 상태를 먼저 맞춘 뒤 앱을 반영**합니다.
+
+### 16-2. `scripts/ci/reconcile-vault-dev.sh`
+
+이 스크립트는 실전 운영 로직의 핵심입니다.
+
+반드시 읽어야 할 이유:
+
+- `require_cmd`, `require_env`로 선행조건을 강제한다
+- `start_port_forward`와 `trap`으로 백그라운드 프로세스를 정리한다
+- `ensure_transit_state_resource`, `ensure_workload_state_resource`로 state import를 자동 보조한다
+- `prepare-infra`, `reconcile-transit`, `reconcile-workload`, `apply-apps`를 분리해 순서를 명확히 한다
+
+이 파일을 이해하지 못하면 "왜 Terraform이 두 번 돌지?", "왜 Argo CD 적용이 나중이지?"가 계속 헷갈립니다.
+
+이 워크플로 전체 흐름을 그림으로 보면 이렇습니다.
+
+```mermaid
+sequenceDiagram
+ participant GH as GitHub Actions
+ participant SH as reconcile-vault-dev.sh
+ participant TF1 as Terraform
(vault-transit/reconcile)
+ participant TF2 as Terraform
(vault/reconcile)
+ participant ARGO as Argo CD
+ participant K8S as K8s 클러스터
+
+ GH->>SH: 스크립트 실행
+ SH->>SH: require_cmd/require_env
사전조건 검증
+ SH->>K8S: Argo CD infra 정의 적용
(Vault, Platform 등)
+ SH->>K8S: kubectl wait
Vault Pod 준비 대기
+ SH->>TF1: Provider Vault reconcile
(policy, role 업데이트)
+ SH->>TF2: Workload Vault reconcile
(secret 복사, auth role 설정)
+ SH->>K8S: Argo CD app 정의 적용
(auth-server, api-server)
+ ARGO->>K8S: 앱 매니페스트 sync
+```
+
+핵심은 **Vault 상태가 먼저 준비되고, 그 다음에 앱이 배포된다**는 것입니다. 순서가 바뀌면 앱이 secret을 못 받고 기동 실패합니다.
+
+> 💡 이 스크립트의 방어적 프로그래밍(set -euo pipefail, trap, require_cmd)은 27장에서 한 줄씩 해부합니다.
+
+## 17. Terraform은 왜 bootstrap 루트와 reconcile 루트가 분리되어 있는가
+
+이 질문은 꼭 깊게 고민해야 합니다.
+
+> 💡 이 장의 내용은 5-7절에서 개념적으로 다뤄으며, 27장에서 Terraform State, import, Provider 이중 설정 등 내부 메커니즘을 상세히 해부합니다.
+
+### 17-1. 관련 디렉터리
+
+| 경로 | 목적 |
+| ----------------------------------- | -------------------------------- |
+| `terraform/vault-transit/dev` | provider Vault 최초 bootstrap |
+| `terraform/vault-transit/reconcile` | provider Vault routine reconcile |
+| `terraform/vault/dev` | workload Vault 최초 bootstrap |
+| `terraform/vault/reconcile` | workload Vault routine reconcile |
+
+### 17-2. 왜 분리하는가
+
+bootstrap과 reconcile은 필요한 권한이 다릅니다.
+
+- bootstrap은 auth backend 생성, mount 생성, 초기 root 수준 작업이 들어간다
+- reconcile은 이미 만들어진 구조를 반복적으로 맞추는 데 집중한다
+
+이 둘을 섞어버리면 어떤 문제가 생길까?
+
+- CI가 너무 강한 권한을 가져야 한다
+- 실수로 초기화 수준 작업을 routine workflow가 건드릴 수 있다
+- 운영자가 의도한 수동 승인 절차가 사라진다
+
+즉, 이 분리는 단순한 취향이 아니라 **권한 경계와 사고 범위를 줄이기 위한 설계**입니다.
+
+### 17-3. `terraform/vault/reconcile/main.tf`에서 꼭 봐야 하는 것
+
+이 파일은 크게 다섯 가지를 합니다.
+
+1. Vault policy 생성
+2. Kubernetes auth role 생성
+3. AppRole 생성
+4. provider Vault seed 값을 workload Vault KV로 복사
+5. database engine connection과 dynamic role 생성
+
+이 파일을 읽을 때는 리소스를 한 줄씩 보는 것보다, 아래 묶음으로 보는 것이 좋습니다.
+
+- policy 묶음
+- Kubernetes auth role 묶음
+- provider seed -> workload KV 복사 묶음
+- database backend 묶음
+
+### 17-4. `runbooks/vault/dev/policies/*.hcl`는 왜 따로 뒀는가
+
+정책을 Terraform 파일 안에 큰 문자열로 넣을 수도 있습니다. 그런데 이 저장소는 정책을 별도 `*.hcl` 파일로 분리했습니다.
+
+이렇게 한 이유:
+
+- 역할별 권한을 눈으로 검토하기 쉽다
+- PR 리뷰에서 "이 서비스가 읽는 경로가 넓어졌는가"를 바로 볼 수 있다
+- 정책만 따로 읽어도 서비스 권한 모델을 이해할 수 있다
+
+## 18. 왜 `vault-transit`과 `vault`를 둘 다 두는가
+
+이것도 반드시 고민해야 하는 포인트입니다.
+
+### 18-1. 현재 구조
+
+- `vault-transit`: provider Vault
+- `vault`: workload Vault
+
+provider Vault는 주로 아래를 담당합니다.
+
+- workload Vault auto-unseal 지원
+- workload용 seed secret의 source of truth 제공
+- workflow AppRole 정보 제공
+
+workload Vault는 주로 아래를 담당합니다.
+
+- 앱/플랫폼 Pod가 실제로 읽는 runtime secret 제공
+- Kubernetes auth 제공
+- JWT signing transit 제공
+- dynamic DB credential 발급
+
+### 18-2. 왜 하나로 합치지 않았는가
+
+하나로 합치면 구조는 단순해집니다. 하지만 이 저장소는 분리를 선택했습니다.
+
+이유:
+
+- auto-unseal trust boundary를 분리할 수 있다
+- runtime secret 저장소와 unseal provider 역할을 분리할 수 있다
+- CI가 직접 workload Vault root 수준 정보를 오래 들고 있지 않게 만들 수 있다
+
+대신 비용도 있습니다.
+
+- Vault가 2개라 운영 난이도가 올라간다
+- bootstrap 절차가 길어진다
+- 학습 난이도도 높아진다
+
+즉, 이 구조는 "무조건 더 좋다"가 아니라 **보안 경계를 얻는 대신 운영 복잡도를 감수한 선택**입니다.
+
+이 2-Vault 구조의 신뢰 관계를 그림으로 보면 이렇습니다.
+
+```mermaid
+flowchart TB
+ subgraph Provider["Provider Vault (vault-transit)"]
+ T_UNSEAL["Transit Key
workload-vault-dev-unseal
(Unseal용)"]
+ T_JWT["Transit Key
project-auth-jwt
(JWT 서명용)"]
+ SEED["Seed Secrets
(원본 비밀번호)"]
+ end
+
+ subgraph Workload["Workload Vault"]
+ KV["KV Engine
(복사된 runtime secret)"]
+ DB_ENG["Database Engine
(동적 계정 발급)"]
+ K8S_AUTH["K8s Auth
(Pod 인증)"]
+ end
+
+ T_UNSEAL -->|"Auto-Unseal
마스터키 복호화"| Workload
+ SEED -->|"Terraform이
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
+```
+
+Provider Vault는 **Workload Vault의 생명(unseal)**과 **원본 secret(seed)**를 관리합니다. Workload Vault는 **앱이 실제로 사용하는 인터페이스**를 제공합니다. auth-server의 JWT 서명은 Provider Vault의 Transit을 직접 사용하지만, DB 비밀번호 등 runtime secret은 Workload Vault의 KV를 사용합니다.
+
+> 💡 이 구조의 Seal/Unseal 메커니즘, Transit 이중 역할, Agent Injector 동작은 26장에서 상세히 다룹니다.
+
+
+## 19. 이 프로젝트에서 꼭 고민해야 하는 설계 포인트
+
+아래 표는 인턴이 실제로 생각해야 하는 지점을 일부러 늘려 정리한 것입니다.
+
+| 상황 | 왜 고민해야 하는가 | 이 저장소의 현재 선택 | 그렇게 해야 하는 이유 | 잘못 선택했을 때 부작용 |
+| --------------------------------------- | ------------------------------------------ | ---------------------------------------------- | ----------------------------------------------- | ---------------------------------------------------------------- |
+| 새 설정값 추가 | 설정마다 민감도가 다르다 | 비민감 값은 `ConfigMap`, 민감 값은 Vault | Git과 manifest에 secret을 남기지 않기 위해 | secret 유출 또는 값 추적 어려움 |
+| 새 secret 추가 | source of truth가 어디인지 정해야 한다 | provider Vault seed -> workload Vault 복사 | bootstrap과 runtime을 분리하기 위해 | 경로 불일치, CI와 runtime 동기화 실패 |
+| 앱이 DB schema를 건드림 | 앱과 schema 변경 타이밍이 충돌할 수 있다 | 별도 PreSync Job 사용 | 앱보다 먼저, 한 번만 실행되게 하려는 목적 | 락 경쟁, 앱 부팅 실패, 반쪽 배포 |
+| 새 워크로드가 Vault를 써야 함 | 기본 보안값과 예외를 정해야 한다 | base는 SA token off, 필요한 overlay만 on | 불필요한 토큰 노출을 막기 위해 | Vault 로그인 실패 또는 토큰 과노출 |
+| 새 권한 추가 | 권한 범위를 얼마나 넓힐지 결정해야 한다 | policy 경로를 최소화 | 침해 시 피해 범위를 줄이기 위해 | 과권한 부여 |
+| 새 네트워크 경로 추가 | 통신이 되게 하면서도 너무 열지 말아야 한다 | default deny 후 allowlist | 우연한 통신 의존성을 막기 위해 | 앱 기동 실패 또는 불필요한 개방 |
+| base와 overlay 어디에 둘지 | 환경 독립성과 환경 특수성을 구분해야 한다 | 공통 구조는 base, dev host/path/tag는 overlay | prod/dev 드리프트를 줄이기 위해 | 다른 환경까지 의도치 않게 바뀜 |
+| bootstrap과 reconcile 중 어디를 바꿀지 | 권한 수준과 실행 주체가 다르다 | 최초 생성은 bootstrap, 반복 동기화는 reconcile | CI 권한 최소화와 절차 분리를 위해 | routine workflow가 과도한 권한 요구 |
+| Job와 Deployment 중 무엇을 쓸지 | 실행 수명과 재시작 특성이 다르다 | 일회성은 Job, 지속 서비스는 Deployment | 워크로드의 본질에 맞추기 위해 | 완료돼야 할 작업이 계속 재시작되거나, 계속 살아야 할 앱이 종료됨 |
+| Deployment와 StatefulSet 중 무엇을 쓸지 | 저장소와 정체성 보장이 필요한지 다르다 | Postgres만 StatefulSet | 데이터 볼륨과 안정적 네트워크 식별자가 필요해서 | 데이터 유실, 스토리지 재연결 문제 |
+
+## 20. 실전에서 가장 많이 하는 변경과 수정 위치
+
+### 20-1. auth-server에 비민감 설정값 1개 추가
+
+수정 순서:
+
+1. `apps/auth-server/overlays/dev/configmap.yaml`에 값 추가
+2. 앱이 그 값을 실제로 읽는지 확인
+3. 필요하면 prod overlay에도 대응
+
+이 경우 Vault, Terraform, policy 수정은 보통 필요 없습니다.
+
+### 20-2. auth-server에 민감한 값 1개 추가
+
+수정 순서:
+
+1. `scripts/vault-transit/dev/populate-workload-seeds.sh`에 seed 입력 항목 추가
+2. `terraform/vault/dev/main.tf` 또는 `terraform/vault/reconcile/main.tf`에서 provider -> workload 복사 로직 추가
+3. `runbooks/vault/dev/policies/auth-server-dev.hcl`에 읽기 권한 추가
+4. `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`의 template에 `export` 추가
+5. 앱 코드가 그 환경변수를 읽는지 확인
+
+이 과정 중 하나라도 빠지면 Pod는 떠도 값이 비어 있거나, Vault 권한 오류가 납니다.
+
+### 20-3. auth-server가 새 DB 권한을 요구함
+
+확인 순서:
+
+1. 단순 런타임 읽기인지
+2. migration처럼 고권한 일회성 작업인지
+3. 사람 운영용 접근인지
+
+현재 저장소의 기준:
+
+- 런타임 앱: KV 기반 고정 secret
+- migration / operator: database engine 기반 동적 계정
+- 사람 운영: 짧은 TTL 운영자 토큰
+
+### 20-4. 외부 접근 host를 바꾸고 싶음
+
+수정 후보:
+
+- `apps/auth-server/overlays/dev/ingress.yaml`
+- `apps/auth-server/overlays/dev/public-access.yaml`
+- `apps/api-server/overlays/dev/ingress.yaml`
+- `infra/platform/overlays/dev/keycloak-ingress.yaml`
+- 관련 `ConfigMap`의 issuer/base URL
+- 필요 시 Keycloak client sync 관련 값
+
+host 변경은 단순 Ingress 한 파일 수정으로 끝나지 않는 경우가 많습니다.
+
+## 21. 이 문서를 읽은 뒤 실제로 해봐야 할 연습 문제
+
+아래 연습은 "이해했다"고 착각하지 않게 해줍니다.
+
+1. `APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET`가 어디서 시작해서 어느 파일을 거쳐 auth-server Pod까지 들어오는지 경로를 종이에 적어보세요.
+2. `auth-db-migration`이 왜 `Deployment`가 아니라 `Job`인지, 그리고 왜 `PreSync`인지 설명해보세요.
+3. workload Vault가 재시작되었을 때 누가 어떻게 unseal을 돕는지 설명해보세요.
+4. `api-server`가 왜 DB secret이 필요 없는지, 대신 어떤 서비스에 의존하는지 설명해보세요.
+5. auth-server에 새 secret을 추가해야 한다고 가정하고, 수정해야 할 파일을 빠짐없이 나열해보세요.
+
+이 다섯 개를 막힘 없이 말할 수 있으면, 이 저장소를 "눈으로 본 수준"이 아니라 "실제로 수정 가능한 수준"으로 이해한 것입니다.
+
+## 22. 마지막으로: 이 저장소를 배울 때 절대 잊지 말아야 할 관점
+
+이 저장소는 단순히 YAML 모음이 아닙니다. 각 파일은 아래 네 가지 중 하나의 책임을 갖습니다.
+
+- 클러스터에 무엇을 띄울지 정하는 파일
+- Vault 안에 어떤 권한과 비밀 경로를 만들지 정하는 파일
+- 그 선언을 어떤 순서로 적용할지 정하는 파일
+- 사람이 최초 1회 어떤 절차를 밟아야 하는지 정리한 파일
+
+초보자가 가장 많이 하는 실수는 **한 파일만 보고 이해하려는 것**입니다.
+
+이 저장소는 반드시 연결해서 봐야 합니다.
+
+- 앱 manifest는 Argo CD와 연결해서
+- Vault patch는 policy와 Terraform과 연결해서
+- ConfigMap은 실제 의존 서비스와 연결해서
+- workflow는 bootstrap/runbook과 연결해서
+
+이 관점으로 보면, `apps/`의 YAML, `scripts/`의 Bash, `runbooks/`의 HCL, `terraform/`의 TF가 서로 따로 있는 것이 아니라 **한 배포 시스템의 서로 다른 층**이라는 것이 보이기 시작합니다.
+
+---
+
+# Part II: 심층 해부 — 왜 이 기술을 쓰고, 내부에서 무슨 일이 벌어지는가
+
+Part I(1~22장)은 "이 저장소의 파일을 어떻게 읽고 수정하는가"에 집중했습니다.
+
+Part II는 한 걸음 더 들어가서 **"왜 이런 기술을 선택했고, 그 기술이 내부에서 어떤 원리로 돌아가는가"**를 설명합니다.
+
+이 파트를 읽고 나면 Part I에서 표면적으로만 이해했던 개념들이 입체적으로 연결되기 시작합니다.
+
+## 23. 운영체제와 서버 인프라의 근간
+
+### 23-1. Linux vs Windows: 왜 서버는 리눅스인가
+
+#### 서버에 윈도우를 안 쓰는 진짜 이유
+
+우리가 일상에서 쓰는 윈도우 PC를 떠올려 보세요. 부팅하면 바탕화면이 뜨고, 마우스 커서가 나타나고, 시작 메뉴가 보입니다. 이 모든 것이 실행 중인 **프로세스**입니다.
+
+바탕화면을 렌더링하는 프로세스, 마우스 커서를 따라 그리는 프로세스, 알림 영역을 관리하는 프로세스 등이 부팅 직후 수십 개가 올라옵니다. 이것들이 소비하는 메모리만 해도 최소 2~4GB에 달합니다.
+
+서버는 다릅니다. **모니터에 무언가를 보여줄 일이 없습니다.** 서버의 유일한 목적은 "네트워크로 들어온 요청을 처리하고 결과를 돌려주는 것"입니다. 그런데 이걸 하겠다고 바탕화면 렌더링에 몇 GB를 쓰는 것은 자원 낭비입니다.
+
+리눅스 서버는 **텍스트 터미널만** 있습니다. GUI가 없으니 그래픽 프로세스가 전혀 올라오지 않습니다. 같은 하드웨어에서 운영체제가 차지하는 메모리가 수백 MB 수준이라, 나머지 리소스를 전부 실제 서비스(Java, Postgres, Vault 등)에 쓸 수 있습니다.
+
+하지만 이것만이 리눅스를 서버로 쓰는 이유는 아닙니다. 더 근본적인 이유가 있습니다.
+
+#### "모든 것이 파일이다" — 리눅스의 설계 철학
+
+리눅스에는 **"Everything is a file"**이라는 설계 원칙이 있습니다.
+
+이게 무슨 뜻인지 구체적으로 보겠습니다.
+
+| 대상 | 윈도우에서는 | 리눅스에서는 |
+|---|---|---|
+| 하드디스크 | "디스크 관리" 프로그램으로 관리 | `/dev/sda` 라는 파일로 접근 |
+| USB 장치 | 장치 관리자에서 확인 | `/dev/usb/...` 파일로 접근 |
+| 네트워크 설정 | 제어판 → 네트워크 설정 GUI | `/proc/net/...` 파일을 읽으면 됨 |
+| 실행 중인 프로세스 정보 | 작업 관리자 GUI | `/proc/[PID]/...` 파일을 읽으면 됨 |
+| 시스템 로그 | 이벤트 뷰어 GUI | `/var/log/...` 텍스트 파일을 읽으면 됨 |
+
+하드디스크든, 네트워크 포트든, 실행 중인 프로세스의 정보든, 리눅스에서는 **전부 파일처럼 읽고 쓸 수 있습니다.** 이 통일성이 왜 중요할까요?
+
+Kubernetes는 컨테이너 안의 리소스 사용량을 모니터링하고, 네트워크를 설정하고, 볼륨을 마운트합니다. 이 모든 작업을 리눅스에서는 "파일을 읽고 쓰는 것"으로 통일해서 처리할 수 있습니다. 만약 하드디스크 접근은 A 방식, 네트워크 접근은 B 방식, 프로세스 접근은 C 방식이라면 Kubernetes 같은 오케스트레이터를 만드는 것 자체가 훨씬 어려워집니다.
+
+#### 그렇다면 우리 윈도우 개발 PC에서 Docker는 어떻게 돌아가는가?
+
+여기서 자연스러운 의문이 생깁니다. "서버는 리눅스인데, 내 개발 PC는 윈도우잖아? 그런데 Docker로 리눅스 컨테이너를 돌리고 있잖아? 이게 어떻게 가능하지?"
+
+답은 **WSL2(Windows Subsystem for Linux 2)**입니다. WSL2는 단순한 에뮬레이터가 아닙니다. 마이크로소프트가 **진짜 리눅스 커널**을 윈도우 안에서 돌리는 구조를 만든 것입니다.
+
+```mermaid
+flowchart TB
+ subgraph 우리_PC["우리 개발 PC (Windows)"]
+ direction TB
+ WIN[Windows NT 커널
바탕화면, 마우스, VS Code 등 실행]
+
+ subgraph HYPERV["Hyper-V 가상화 층"]
+ direction TB
+ LINUX[진짜 Linux 커널
WSL2가 제공하는 경량 가상머신]
+ end
+
+ subgraph DOCKER["Docker Desktop"]
+ direction TB
+ ENGINE[Docker Engine 데몬
Linux 커널 위에서 실행됨]
+ ENGINE --> C1[auth-server 컨테이너]
+ ENGINE --> C2[postgres 컨테이너]
+ ENGINE --> C3[vault 컨테이너]
+ end
+ end
+
+ WIN -->|"Hyper-V를 통해
리눅스 커널 호스팅"| LINUX
+ LINUX -->|"커널 기능 제공
(cgroups, namespaces)"| ENGINE
+```
+
+이 그림을 단계별로 설명하면 이렇습니다.
+
+1. 우리 윈도우 PC의 `Windows NT 커널`이 먼저 부팅됩니다. 바탕화면, VS Code 등이 여기서 돕니다.
+2. Windows에는 `Hyper-V`라는 가상화 기능이 내장되어 있습니다. 이것이 아주 가벼운 가상머신을 하나 만듭니다.
+3. 그 가상머신 안에 **진짜 리눅스 커널**이 올라갑니다. 에뮬레이션이 아니라 실제 리눅스 커널 바이너리입니다.
+4. Docker Desktop은 이 리눅스 커널 위에서 Docker Engine을 실행합니다.
+5. 우리가 `docker run`으로 띄우는 Postgres, Vault, auth-server 컨테이너들은 모두 이 리눅스 커널의 기능(cgroups, namespaces)을 사용합니다.
+
+그래서 우리 윈도우 PC에서 리눅스 컨테이너가 아무 문제 없이 돌아가는 것입니다. 컨테이너 입장에서는 자기가 리눅스 위에서 돌고 있다고 "느끼는" 것이고, 실제로도 그렇습니다.
+
+#### 리눅스 계열의 차이: Ubuntu, CentOS, Arch는 뭐가 다른가
+
+"리눅스"라고 하면 하나의 운영체제처럼 들리지만, 실제로는 **수십 가지 배포판(Distribution)**이 있습니다. 커널(심장)은 같지만, 그 위에 올라가는 패키지 관리 방식, 기본 설치 범위, 업데이트 정책이 다릅니다.
+
+비유하자면, 같은 엔진을 쓰는 자동차라도 세단, SUV, 트럭이 다르듯이, 같은 리눅스 커널 위에 어떤 옷을 입혔느냐가 배포판의 차이입니다.
+
+| 계열 | 대표 배포판 | 패키지 관리 도구 | 특징 | 서버 적합도 |
+|---|---|---|---|---|
+| Debian 계열 | **Ubuntu**, Debian | `apt` (deb 패키지) | 초보자 친화적, 커뮤니티 크고 자료 많음, 2년마다 LTS(장기 지원) 버전 출시 | ⭐⭐⭐⭐⭐ 가장 많이 사용 |
+| RHEL 계열 | CentOS, Rocky Linux, AlmaLinux | `yum` / `dnf` (rpm 패키지) | 기업 환경에서 검증된 안정성, Red Hat의 상업 지원 가능, 보안 인증(FIPS 등) | ⭐⭐⭐⭐⭐ 엔터프라이즈 표준 |
+| Arch 계열 | **Arch Linux**, Manjaro | `pacman` | 아무것도 기본 설치하지 않음, 사용자가 직접 모든 것을 구성, 롤링 릴리스(항상 최신) | ⭐⭐ 학습용에 가까움 |
+| Alpine | Alpine Linux | `apk` | 극도로 가벼움(5MB급 베이스 이미지), 보안 중심 설계 | ⭐⭐⭐⭐ Docker 이미지 베이스로 인기 |
+
+이 프로젝트의 K3s 노드는 **Ubuntu** 위에서 돌고 있습니다. Ubuntu를 선택한 이유는 K3s 공식 문서의 1순위 지원 대상이고, 자료가 가장 많고, `apt`로 패키지 설치가 간편하기 때문입니다.
+
+PPT에서 언급한 **Arch Linux**는 "아무것도 안 해주니까 직접 다 하면서 원리를 배우기 좋다"는 학습 목적으로 시도한 것입니다. 실제 서버 운영에는 Ubuntu나 RHEL 계열을 쓰는 것이 일반적입니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- 리눅스의 "모든 것이 파일" 원칙이 왜 컨테이너 기술(Docker, K8s)과 궁합이 좋은지, `/proc`와 `/sys` 파일시스템의 역할을 조사해보세요.
+- Alpine Linux가 Docker 이미지 베이스로 인기 있는 이유는 무엇일까요? 반대로, Alpine을 쓰면 생기는 단점(glibc vs musl 차이)은 무엇일까요?
+- 우리 프로젝트의 `hashicorp/vault:1.18` 이미지는 어떤 리눅스 배포판을 베이스로 사용하고 있을까요? `docker inspect`로 확인해보세요.
+- WSL2 없이 윈도우에서 리눅스 컨테이너를 돌릴 수 있는 다른 방법이 있을까요? (힌트: VirtualBox, VMware, Multipass)
+
+### 23-2. 부팅과 커널의 비밀: Dual Boot와 GRUB 부트로더
+
+#### 컴퓨터 전원을 누르면 실제로 무슨 일이 일어나는가
+
+우리는 매일 전원 버튼을 누르지만, 전원 버튼을 누른 순간부터 로그인 화면이 뜰 때까지 컴퓨터 내부에서는 **5단계의 정밀한 체인**이 순서대로 실행됩니다.
+
+이 체인 중 하나라도 실패하면 화면에 아무것도 안 뜹니다. 또는 에러 메시지만 나오고 멈춥니다.
+
+```mermaid
+flowchart TD
+ A["1단계: 전원 ON
전기가 메인보드에 공급됨"] --> B["2단계: BIOS / UEFI 실행
메인보드 칩에 박혀있는 초소형 프로그램
하드웨어 점검(POST) 수행"]
+ B --> C["3단계: 부트로더 실행
BIOS가 하드디스크의 약속된 위치에서
부트로더 프로그램을 찾아 실행"]
+ C --> D{"4단계: OS 선택
부트로더가 설치된 OS 목록을 보여줌
(Dual Boot인 경우)"}
+ D -->|"Ubuntu 선택"| E["5a단계: Linux 커널 적재
vmlinuz(커널 바이너리)를
RAM에 통째로 올림"]
+ D -->|"Windows 선택"| F["5b단계: Windows Boot Manager
Windows 커널을 RAM에 올림"]
+ E --> G["6단계: init / systemd 실행
커널이 PID 1번 프로세스를 생성
이것이 모든 프로세스의 조상"]
+ G --> H["7단계: 로그인 화면
또는 텍스트 프롬프트"]
+```
+
+각 단계를 하나씩 풀어보겠습니다.
+
+**1~2단계: BIOS / UEFI**
+
+전원을 누르면 가장 먼저 실행되는 것은 윈도우도 리눅스도 아닙니다. **메인보드 칩에 박혀 있는 아주 작은 프로그램**이 먼저 뜹니다. 이것이 BIOS(오래된 방식) 또는 UEFI(최신 방식)입니다.
+
+이 프로그램이 하는 일은 "하드웨어가 정상인지 확인"하는 것입니다. RAM이 있는지, 하드디스크가 연결되어 있는지, 그래픽카드가 있는지 등을 빠르게 체크합니다(이것을 POST, Power-On Self-Test라고 부릅니다).
+
+**3단계: 부트로더**
+
+하드웨어 점검이 끝나면 BIOS/UEFI는 "이제 운영체제를 찾아야 한다"고 판단합니다. 하지만 BIOS 자체는 운영체제를 이해하지 못합니다. 그래서 하드디스크의 **약속된 위치(EFI System Partition)**에서 **부트로더**라는 중간 프로그램을 찾아서 실행합니다.
+
+부트로더는 쉽게 말하면 **"어떤 운영체제를 켤지 고르는 메뉴판 프로그램"**입니다.
+
+**4단계: OS 선택 (Dual Boot)**
+
+만약 하드디스크에 Ubuntu와 Windows가 둘 다 설치되어 있다면, 부트로더가 두 선택지를 보여줍니다. 사용자가 Ubuntu를 선택하면 리눅스 커널을 RAM에 올리고, Windows를 선택하면 Windows Boot Manager에게 제어를 넘깁니다.
+
+**이것이 듀얼 부팅이 가능한 원리입니다.** 하드디스크에 두 OS가 나란히 깔려 있고, 부트로더가 "어느 쪽 커널을 메모리에 올릴지" 선택하는 것입니다.
+
+**5~6단계: 커널 → PID 1**
+
+리눅스 커널(`vmlinuz`)이 RAM에 올라오면, 커널은 시스템의 모든 하드웨어를 초기화하고, 그 다음 **딱 하나의 프로세스**를 생성합니다. 이것이 `systemd` (또는 구형 시스템에서는 `init`)이고, **PID 번호 1번**을 받습니다.
+
+PID 1번은 **모든 프로세스의 조상**입니다. 이후에 뜨는 SSH 서버, 네트워크 데몬, Docker Engine, K3s kubelet 등은 전부 이 PID 1번의 자손입니다.
+
+Part I의 5-2에서 "왜 `exec`를 써서 PID 1을 Java 프로세스로 교체하는가"를 설명했습니다. 이제 부팅 과정을 알았으니 그 의미가 더 선명해집니다. 컨테이너 안에서도 PID 1은 특별하고, 운영체제에서의 PID 1이 곧 컨테이너의 PID 1과 같은 개념입니다.
+
+#### GRUB 부트로더와 다른 부트로더의 차이
+
+현재 이 프로젝트의 개발 환경은 **GRUB(GRand Unified Bootloader)**을 사용하고 있습니다. GRUB은 리눅스에서 가장 널리 쓰이는 부트로더입니다. 하지만 세상에는 다른 부트로더도 있습니다.
+
+| 부트로더 | 특징 | 커스터마이징 | Dual Boot 지원 | 적합한 상황 |
+|---|---|---|---|---|
+| **GRUB** | 가장 유명, 거의 모든 리눅스 배포판의 기본 부트로더 | 매우 유연(테마, 스크립트, 커널 파라미터 조정 가능) | ⭐⭐⭐⭐⭐ `os-prober`로 다른 OS 감지 | 일반 서버/데스크탑 대부분 |
+| **systemd-boot** | systemd 프로젝트의 일부, 설정이 단순 | 설정 파일이 간결하지만 유연성은 낮음 | ⭐⭐⭐ 수동 설정 필요 | UEFI 전용, 단일 OS 서버 |
+| **rEFInd** | GUI가 예쁘고, EFI 엔트리를 직접 스캔 | 아이콘/테마 커스터마이징 편리 | ⭐⭐⭐⭐⭐ EFI 파티션 직접 탐색 | 맥 + 리눅스 듀얼부팅 |
+| **Syslinux/ISOLINUX** | 극도로 가벼움, 설치 미디어(USB/CD)용 | 제한적 | ⭐⭐ 단일 OS 전용 | 부팅 USB, 임베디드 |
+
+#### GRUB은 어떻게 Windows를 "자동으로" 감지하는가
+
+위 표에서 GRUB이 "다른 OS를 감지한다"고 적었는데, **이것은 마법이 아니라 구체적인 메커니즘**이 있습니다. 이것을 이해해야 "왜 가끔 GRUB이 감지를 못 하는 상황이 생기는지"까지 알 수 있습니다.
+
+Ubuntu에서 `sudo update-grub` 명령을 실행하면 내부적으로 이런 일이 벌어집니다.
+
+```mermaid
+flowchart TD
+ A["사용자가 sudo update-grub 실행"] --> B["grub-mkconfig 프로그램 시작
GRUB 메뉴 설정 파일을 자동 생성하는 도구"]
+ B --> C["1단계: /boot 디렉터리 스캔
리눅스 커널 파일(vmlinuz)을 찾음"]
+ B --> D["2단계: os-prober 실행
다른 OS가 있는지 디스크를 뒤짐"]
+ D --> E["os-prober가 하는 일:
① 모든 디스크 파티션을 하나씩 마운트
② 각 파티션에서 OS 흔적을 찾음"]
+ E --> F{"Windows 흔적 발견?
예: /EFI/Microsoft/Boot/bootmgfw.efi
또는 NTFS 파티션의 bootmgr"}
+ F -->|"흔적 발견"| G["GRUB 메뉴에 'Windows Boot Manager' 항목 추가"]
+ F -->|"흔적 없음"| H["GRUB 메뉴에 리눅스만 표시"]
+ G --> I["결과: /boot/grub/grub.cfg 파일 생성
이 파일이 부팅 시 메뉴로 표시됨"]
+ H --> I
+```
+
+핵심은 **`os-prober`**라는 프로그램입니다. 이 프로그램이 하는 일을 쉽게 설명하면 이렇습니다.
+
+1. 컴퓨터에 연결된 **모든 디스크의 모든 파티션**을 하나씩 열어봅니다.
+2. 각 파티션을 임시로 마운트해놓고, 그 안에 **운영체제의 흔적**이 있는지 찾습니다.
+ - Windows의 흔적: EFI 파티션 안의 `/EFI/Microsoft/Boot/bootmgfw.efi` 파일, 또는 NTFS 파티션의 `bootmgr` 파일
+ - 다른 리눅스의 흔적: `/boot/vmlinuz` 커널 파일
+ - macOS의 흔적: HFS+ 파티션의 특정 구조
+3. 흔적을 찾으면 "이 파티션에 이런 OS가 있다"고 보고합니다.
+4. `grub-mkconfig`가 이 보고를 받아서 GRUB 메뉴 설정 파일(`/boot/grub/grub.cfg`)에 해당 OS 항목을 추가합니다.
+
+그래서 GRUB의 "자동 감지"란 사실 **디스크의 모든 파티션을 뒤져서 알려진 OS의 파일 패턴을 찾는 것**입니다. 이것을 알면 아래 상황이 왜 생기는지도 이해됩니다.
+
+- **Windows를 나중에 깔면 GRUB이 사라지는 이유**: Windows 설치 프로그램이 EFI System Partition의 기본 부트 엔트리를 자기 것(Windows Boot Manager)으로 **덮어씁니다**. GRUB 파일이 지워지는 것은 아니지만, UEFI가 "기본으로 실행할 부트로더"를 Windows 것으로 바꿔버리니까 GRUB 메뉴가 안 뜨는 것입니다. 이 경우 Ubuntu 설치 USB로 부팅한 뒤 `sudo grub-install`로 GRUB을 기본 부트 엔트리로 다시 등록하면 복구됩니다.
+- **`os-prober`가 비활성화된 최신 Ubuntu에서 Dual Boot 메뉴가 안 뜨는 이유**: Ubuntu 21.10부터 보안상의 이유로 `os-prober`가 기본 비활성화되었습니다. `/etc/default/grub`에 `GRUB_DISABLE_OS_PROBER=false`를 추가하고 `sudo update-grub`을 다시 실행해야 Windows가 메뉴에 나타납니다.
+
+반면 **rEFInd**는 `os-prober` 같은 별도 프로그램 없이, **EFI System Partition 안의 `.efi` 파일들을 직접 스캔**해서 부팅 가능한 OS 목록을 만듭니다. EFI 표준 자체가 "이 디렉터리 구조에 부팅 파일을 놓아라"는 규칙을 정해놨기 때문에, 그 규칙대로 있는 파일들만 찾으면 되는 것입니다.
+
+**systemd-boot**는 이런 자동 탐색을 아예 안 합니다. 관리자가 설정 파일에 "Windows는 이 파티션에 있고, 이 loader를 써라"고 직접 적어줘야 합니다. 그래서 Dual Boot 지원이 약한 것입니다.
+
+GRUB이 압도적으로 많이 쓰이는 이유는 이런 **범용성** 때문입니다. 디스크를 스캔해서 OS를 알아서 찾아주고, 커널 부팅 파라미터를 세밀하게 조정할 수 있고, 복구 모드 진입도 GRUB 메뉴에서 직접 가능합니다.
+
+서버 운영에서 커널 파라미터 튜닝은 특히 중요합니다. 예를 들어 K8s 노드에서는 `net.bridge.bridge-nf-call-iptables=1` 같은 커널 파라미터를 켜야 CNI 네트워크가 정상 동작합니다. GRUB은 부팅 시 이 파라미터를 커널에 전달하는 역할도 합니다(`/etc/default/grub`의 `GRUB_CMDLINE_LINUX` 항목).
+
+#### 이 부팅 지식이 K8s 운영에서 왜 중요한가
+
+"부팅 과정까지 알아야 하나?" 싶을 수 있습니다. 하지만 서버를 운영하다 보면 이런 상황이 생깁니다.
+
+- K8s 워커 노드가 갑자기 응답을 멈춤 → **커널 패닉**인지, **systemd 데몬 장애**인지, **kubelet 프로세스 죽음**인지 구분해야 합니다.
+- CNI 플러그인이 동작하지 않음 → 원인이 **커널 파라미터**(net.bridge 설정)인지, CNI 바이너리(Flannel/Calico)인지 진단해야 합니다.
+- 보안 업데이트 후 재부팅했는데 안 켜짐 → **GRUB 설정이 깨졌는지**, 새 커널에 문제가 있는지 확인해야 합니다.
+
+이 모든 상황에서 "전원 → BIOS → GRUB → 커널 → systemd → kubelet → containerd"라는 체인을 머릿속에 그릴 수 있어야 어느 층에서 문제가 생겼는지 빠르게 좁혀갈 수 있습니다.
+
+```mermaid
+flowchart LR
+ A[전원] --> B[BIOS/UEFI] --> C[GRUB] --> D[Linux 커널] --> E[systemd
PID 1] --> F[kubelet
K8s 워커 에이전트] --> G[containerd
컨테이너 런타임] --> H[Pod 안의
Java/Vault/Postgres]
+```
+
+위 체인에서 왼쪽으로 갈수록 "사무실 건물의 전기"에 가깝고, 오른쪽으로 갈수록 "사무실 안의 사원"에 가깝습니다. 장애 진단은 항상 "어느 층에서 끊겼는가"를 찾는 것입니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- BIOS와 UEFI의 차이는 무엇인가요? 왜 최신 컴퓨터는 UEFI를 쓰는 걸까요? (힌트: 2TB 이상 디스크 지원, 보안 부팅)
+- Dual Boot 환경에서 Ubuntu를 먼저 설치하고 Windows를 나중에 깔면 GRUB이 깨졌다는 말을 자주 듣습니다. 왜 그런 걸까요? (힌트: Windows가 EFI Partition의 부트로더를 자기 것으로 덮어쓰기)
+- `systemd`가 PID 1인 이유와, 컨테이너 안에서 PID 1이 `sh`(셸)이면 왜 시그널 처리에 문제가 생기는지 조사해보세요. (Part I 5-2의 `exec` 사용 이유와 연결됩니다)
+- 커널 파라미터 `net.bridge.bridge-nf-call-iptables=1`이 왜 K8s CNI에 필수인지 공식 문서에서 찾아보세요.
+- K8s 노드가 재부팅될 때, 그 노드 위에 있던 Pod들은 어떻게 되나요? 다른 노드로 옮겨지나요, 아니면 그냥 사라지나요?
+
+## 24. Spring Security, OAuth2, Keycloak 심층 해부
+
+### 24-1. Spring Security 무용론? — 클린 아키텍처와의 조화
+
+#### 일반적인 Spring Security 로그인은 어떻게 동작하는가
+
+Spring Security를 처음 배우면 이런 흐름을 따릅니다.
+
+```mermaid
+sequenceDiagram
+ participant User as 사용자 브라우저
+ participant Filter as Spring Security Filter Chain
(웹 앞단, 인프라 계층)
+ participant DAO as DaoAuthenticationProvider
+ participant UDS as UserDetailsService
+ participant DB as 데이터베이스
+
+ User->>Filter: 로그인 요청 (이메일 + 비밀번호)
+ Filter->>DAO: 인증 시도
+ DAO->>UDS: "이 이메일의 사용자 정보 가져와"
+ UDS->>DB: SELECT * FROM users WHERE email = ?
+ DB-->>UDS: 사용자 정보 (해시된 비밀번호 포함)
+ UDS-->>DAO: UserDetails 객체 반환
+ DAO->>DAO: 비밀번호 비교 (BCrypt)
+ DAO-->>Filter: 인증 성공 → Authentication 객체 생성
+ Filter-->>User: 로그인 성공, 세션 또는 토큰 발급
+```
+
+이 흐름에서 중요한 것은 **Security Filter 안에서 DB 조회가 일어난다는 점**입니다. `UserDetailsService`가 DB에서 사용자를 찾고, `DaoAuthenticationProvider`가 비밀번호를 검증합니다. 이 모든 것이 웹 필터 체인, 즉 **인프라 계층** 안에서 발생합니다.
+
+작은 프로젝트에서는 이게 간편합니다. Spring이 다 해주니까요. 하지만 이 구조에는 **아키텍처적 문제**가 있습니다.
+
+#### 왜 우리 프로젝트는 이 방식을 안 쓰는가 — 클린 아키텍처 위반
+
+클린 아키텍처(Clean Architecture)의 핵심 규칙은 하나입니다.
+
+> **의존성은 무조건 바깥에서 안쪽으로만 향해야 한다.**
+
+이것을 원으로 그리면 이렇습니다.
+
+```mermaid
+flowchart TB
+ subgraph 가장_바깥["가장 바깥: 인프라/프레젠테이션"]
+ direction TB
+ A["Spring Security Filter
Spring MVC Controller
JPA Repository
DB 드라이버"]
+ end
+
+ subgraph 중간["중간: Application / UseCase"]
+ direction TB
+ B["로그인 UseCase
회원가입 UseCase
토큰 발급 UseCase"]
+ end
+
+ subgraph 가장_안쪽["가장 안쪽: Domain"]
+ direction TB
+ C["User 엔티티
비즈니스 규칙
순수 Java 코드"]
+ end
+
+ A -->|"의존 가능 ✅"| B
+ B -->|"의존 가능 ✅"| C
+ C -.-x|"의존 불가 ❌
Domain은 Security를
몰라야 함"| A
+```
+
+**바깥 원(Security Filter, Controller, DB)**은 안쪽 원(UseCase, Domain)을 호출할 수 있습니다. 하지만 안쪽 원은 바깥 원의 존재를 **전혀 몰라야** 합니다.
+
+그런데 기본 Spring Security 흐름에서는 **Filter(바깥 원) 안에서 DB 조회(비즈니스 로직)가 직접 실행**됩니다. 이것은 바깥 원이 안쪽 원을 관통해서 직접 DB를 건드리는 것이라, 클린 아키텍처 원칙에 **위배**됩니다.
+
+Spring 공식 문서도 이 점을 인정합니다. 공식 Web Integration 문서에서는 "웹 계층은 여러 계층 중 하나일 뿐이며, 서비스 계층에 정의된 서비스 객체에 비즈니스 관련 사용 사례를 처리하도록 위임하라"고 권고합니다.
+
+#### 우리 프로젝트는 어떻게 했는가
+
+이 프로젝트의 `auth-server`에서는 Security의 역할을 **문지기로 축소**했습니다.
+
+```mermaid
+flowchart LR
+ subgraph Security_역할["Spring Security가 하는 일 (문지기)"]
+ direction TB
+ S1["CORS 설정
다른 출처의 요청 차단/허용"]
+ S2["JWT 서명 검증
토큰이 위조되었는지 확인"]
+ S3["RBAC 권한 체크
일반 유저인지, 관리자인지
API 접근 허용/차단"]
+ end
+
+ subgraph Service_역할["비즈니스 로직이 하는 일 (UseCase)"]
+ direction TB
+ U1["로그인 검증
DB에서 사용자 조회"]
+ U2["회원 가입 처리
DB에 사용자 저장"]
+ U3["토큰 발급
Vault Transit으로 서명"]
+ end
+
+ Security_역할 -->|"Argument Resolver로
토큰 정보만 넘겨줌"| Service_역할
+```
+
+구체적으로:
+
+| 역할 | 일반적인 Spring Security | 이 프로젝트 |
+|---|---|---|
+| 비밀번호 검증 | Filter 안의 `DaoAuthenticationProvider`가 직접 수행 | UseCase(Service 계층)에서 수행 |
+| DB 조회 | `UserDetailsService`가 Filter 안에서 DB 접근 | Controller가 Argument Resolver로 토큰 정보만 파싱한 뒤, **Service 계층에서** DB 접근 |
+| 비밀번호 암호화 | Security의 `BCryptPasswordEncoder` 직접 사용 | 도메인이 "비밀번호 검증해줘"라고 **포트(인터페이스)**를 통해 요청 → 바깥의 Security가 BCrypt로 처리 |
+| OAuth2 로그인 | Security의 OAuth2LoginFilter가 session에 유저 정보를 밀어넣음 | 토큰 교환까지만 Security가 처리 → 이후 Argument Resolver → Service 계층에서 DB 조회 및 가입 처리 |
+
+이렇게 하면 **도메인 코드는 Spring Security의 존재를 전혀 모릅니다.** 내일 갑자기 Security를 걷어내고 다른 보안 프레임워크로 바꿔도, 도메인과 UseCase 코드는 수정할 필요가 없습니다.
+
+#### ArchUnit: 이 규칙을 어떻게 강제하는가
+
+사람이 코드를 짜다 보면 실수로 규칙을 어길 수 있습니다. "급하니까 Domain에서 직접 Security 클래스를 import하자"라고 하는 순간 아키텍처가 무너집니다.
+
+이 프로젝트에서는 **ArchUnit**이라는 라이브러리로 이 규칙을 **테스트 코드로 강제**합니다.
+
+ArchUnit은 JUnit 테스트처럼 동작합니다. 개발자가 코드를 커밋하고 CI가 돌 때, ArchUnit 테스트가 함께 실행됩니다. 만약 `domain` 패키지의 클래스가 `infrastructure` 패키지의 클래스를 import하고 있으면, **테스트가 실패**합니다.
+
+즉, ArchUnit은 런타임에 동작하는 것이 아니라 **테스트 시점(CI 빌드 시점)**에 동작합니다. 실제 서비스가 돌고 있을 때 성능에 영향을 주는 것이 아니라, 코드를 올릴 때 "이 코드는 아키텍처 규칙을 어겼으므로 빌드 실패"라고 알려주는 **정적 분석 파수꾼**입니다.
+
+이 프로젝트의 ConfigMap인 `apps/auth-server/overlays/dev/configmap.yaml`을 보면 Security가 남긴 흔적을 확인할 수 있습니다.
+
+```yaml
+# Vault Transit으로 JWT를 다룬다는 설정 — Security는 이 주소를 알고 서명을 요청할 뿐
+APP_SECURITY_JWT_VAULT_ENABLED: "true"
+APP_SECURITY_JWT_VAULT_ADDRESS: http://vault.vault.svc.cluster.local:8200
+APP_SECURITY_JWT_VAULT_TRANSIT_KEY_NAME: project-auth-jwt
+
+# Keycloak과의 OIDC 연동 — Security는 토큰 교환까지만 담당
+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_JWT_VAULT_ENABLED: "true"`는 "JWT 서명을 로컬에서 하지 않고 Vault Transit에 위임하겠다"는 뜻입니다. Security가 서명의 **실행자**가 아니라 서명을 **중계하는 문지기** 역할만 한다는 것을 설정값으로도 확인할 수 있습니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- ArchUnit이 검사하는 시점은 언제인가? 런타임인가, CI 빌드 시점인가? (답: 테스트 시점, 즉 JUnit이 돌 때)
+- Keycloak이 완전히 다운되면 기존에 발급된 JWT 토큰은 여전히 유효한가? (힌트: JWT는 자기 완결적 — 서명 검증에 외부 호출이 필요 없다면 유효하다)
+- `BcryptPasswordEncoder`를 도메인 코드에서 직접 쓰지 않고 포트(인터페이스)로 뺀 이유는 무엇인가? 포트를 안 쓰고 직접 BCrypt를 호출하면 어떤 문제가 생기는가?
+- 이 프로젝트에서 `APP_SECURITY_JWT_GENERATE_KEY_PAIR_ON_STARTUP: "false"`로 설정한 이유는 무엇인가? (힌트: 키 생성도 Vault Transit에 맡겼기 때문)
+- MVC 패턴과 클린 아키텍처의 가장 큰 차이는 무엇인가? MVC에서 Controller가 직접 DB를 호출하는 것이 왜 큰 프로젝트에서는 문제가 되는가?
+
+### 24-2. 현업의 OAuth2: Keycloak과 OIDC 내부 동작
+
+#### OAuth2의 핵심 아이디어: "비밀번호를 우리가 받지 않는다"
+
+사용자가 구글 로그인을 누르는 상황을 생각해보세요. 우리 서비스가 사용자의 구글 비밀번호를 직접 받아서 구글에 대신 로그인해주는 것은 **매우 위험**합니다. 사용자 입장에서도 "내 구글 비밀번호를 니네 서비스에 왜 줘야 하지?" 싶습니다.
+
+OAuth2는 이 문제를 해결합니다. **비밀번호를 우리한테 주는 게 아니라, 구글 로그인 페이지에서 직접 로그인하고, 구글이 "이 사람은 진짜야"라는 증거(토큰)만 우리한테 건네주는 구조**입니다.
+
+이 흐름을 **인가 코드 부여 방식(Authorization Code Grant)**이라고 부릅니다.
+
+```mermaid
+sequenceDiagram
+ participant User as 사용자 브라우저
+ participant Spring as auth-server
(Spring Security)
+ participant Google as 구글 로그인 서버
+
+ User->>Spring: "구글로 로그인할래요"
+ Spring->>User: 구글 로그인 페이지로 리다이렉트
(우리 서버가 비밀번호를 받지 않음!)
+ User->>Google: 구글 로그인 페이지에서 직접 로그인
+ Google->>User: 로그인 성공! 인가 코드를 들고
우리 서버의 콜백 URL로 돌아가거라
+ User->>Spring: 인가 코드(임시 교환권)를 들고 콜백 URL로 돌아옴
+
+ Note over Spring,Google: 여기서부터는 서버 대 서버 통신 (사용자 브라우저를 거치지 않음)
+ Spring->>Google: "이 인가 코드, 진짜 너네가 준 거 맞지?
Access Token으로 바꿔줘"
+ Google-->>Spring: Access Token + ID Token 반환
+ Spring->>Spring: ID Token에서 사용자 이메일, 이름 등 추출
+ Spring-->>User: 우리 서비스의 JWT 토큰 발급
+```
+
+핵심 포인트는 **인가 코드(Authorization Code)**입니다. 이것은 "일회용 교환권"같은 것입니다.
+
+1. 구글이 사용자에게 인가 코드를 줍니다.
+2. 사용자가 그 코드를 우리 서버에 전달합니다.
+3. 우리 서버가 구글에 "이 코드 진짜야? 토큰으로 바꿔줘"라고 서버 대 서버 통신을 합니다.
+
+왜 처음부터 토큰을 안 주고 코드를 먼저 줄까요? 코드는 **사용자의 브라우저를 경유**합니다. 브라우저는 해킹당할 수 있습니다. 그래서 코드는 일회용이고 짧은 시간만 유효합니다. 실제 토큰 교환은 **서버 대 서버 통신**으로 이루어지니 브라우저가 탈취당해도 토큰은 안전합니다.
+
+#### Keycloak이 없으면 어떤 끔찍한 일이 생기는가
+
+위 흐름을 구글, 카카오, 네이버, GitHub 각각에 대해 구현한다고 생각해보세요.
+
+```mermaid
+flowchart TB
+ subgraph 문제["Keycloak 없이 직접 구현한 경우"]
+ direction TB
+ APP[auth-server]
+ APP --> G[구글 API
응답 형식: A]
+ APP --> K[카카오 API
응답 형식: B]
+ APP --> N[네이버 API
응답 형식: C]
+ APP --> GH[GitHub API
응답 형식: D]
+ end
+```
+
+각 소셜 로그인 제공자마다:
+- 콜백 URL 형식이 다릅니다.
+- 사용자 정보 응답 형식(JSON 구조)이 다릅니다. 구글은 `email` 필드, 카카오는 `kakao_account.email` 필드입니다.
+- 토큰 갱신 방식이 다릅니다.
+
+제공자가 4개면 4가지 파싱 코드를 짜야 합니다. 10개가 되면 10가지입니다. 코드가 폭발합니다.
+
+#### Keycloak은 이 문제를 어떻게 해결하는가
+
+**Keycloak**은 **사설 통합 인증 센터(Identity Provider Broker)**입니다. 쉽게 말하면 "외부 로그인 제공자와의 복잡한 대화를 대신 해주고, 우리한테는 항상 같은 형식으로 결과를 알려주는 중간 통역사"입니다.
+
+```mermaid
+flowchart TB
+ subgraph 해결["Keycloak을 둔 경우"]
+ direction TB
+ APP2[auth-server
Keycloak 하나만 상대하면 됨]
+ KC[Keycloak
통합 인증 센터]
+ APP2 -->|"항상 OIDC 표준 형식
하나의 콜백, 하나의 응답 규격"| KC
+ KC --> G2[구글]
+ KC --> K2[카카오]
+ KC --> N2[네이버]
+ KC --> GH2[GitHub]
+ end
+```
+
+이 구조에서 `auth-server`의 Spring Security가 하는 일은 극적으로 줄어듭니다.
+
+1. 사용자가 "구글로 로그인" 클릭 → Security가 사용자를 **Keycloak**으로 리다이렉트 (구글로 직접이 아님!)
+2. Keycloak이 구글과 알아서 통신 → 구글의 응답을 **OIDC 표준 형식(JSON)**으로 변환
+3. Keycloak이 auth-server에 콜백 → Security가 OIDC 표준 토큰을 수신
+4. 여기까지가 Security의 역할 끝 → 이후 Argument Resolver가 토큰에서 사용자 정보 추출 → Service 계층에서 DB 조회
+
+**OIDC(OpenID Connect)**란 OAuth2 위에 "사용자 신원 확인" 규격을 얹은 표준입니다. OAuth2만으로는 "이 사람이 어떤 리소스에 접근 가능하다"만 알 수 있지만, OIDC를 쓰면 "이 사람의 이메일은 뭐고 이름은 뭐다"까지 표준화된 형식(ID Token)으로 받을 수 있습니다.
+
+#### 이 구조가 실제 코드에 어떻게 반영되어 있는가
+
+이 저장소의 실제 파일들을 보면 위 아키텍처가 선언으로 드러납니다.
+
+**1. Keycloak이 auth-server의 인증 상대방이라는 것을 알려주는 설정**
+
+`apps/auth-server/overlays/dev/configmap.yaml`:
+```yaml
+# auth-server가 Keycloak을 통해 OIDC 인증을 받겠다는 선언
+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
+
+# 구글/GitHub 로그인도 Keycloak을 경유한다는 뜻
+# registration ID가 "keycloak-google", "keycloak-github"다
+APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google
+APP_SECURITY_OAUTH2_GITHUB_REGISTRATION_ID: keycloak-github
+```
+
+`APP_SECURITY_OAUTH2_GOOGLE_REGISTRATION_ID: keycloak-google`이 중요합니다. 이름이 **"keycloak-google"**인 것은 **구글 로그인이지만 Keycloak을 거쳐서 간다**는 뜻입니다. auth-server는 구글 API를 직접 호출하지 않고, Keycloak에게 "구글 쪽으로 브로커링 해줘"라고 힌트(`idp_hint: google`)만 보냅니다.
+
+**2. Keycloak의 client 설정을 코드로 맞추는 Job**
+
+`infra/platform/base/keycloak-client-sync-job.yaml`을 보면, 이 Job이 Keycloak Admin API를 호출해서:
+- `project-auth-server` 클라이언트의 `secret`을 업데이트하고
+- `baseUrl`을 맞추고
+- `redirectUris`를 코드 기반으로 등록합니다
+
+```yaml
+# keycloak-client-sync-job.yaml 중 핵심 부분
+/opt/keycloak/bin/kcadm.sh update "clients/${CLIENT_UUID}" \
+ -r project-auth \
+ -s "secret=$KEYCLOAK_CLIENT_SECRET" \
+ -s "baseUrl=$AUTH_SERVER_BASE_URL" \
+ -s 'redirectUris=[".../login/oauth2/code/keycloak-google",".../login/oauth2/code/keycloak-github"]'
+```
+
+사람이 Keycloak 관리 콘솔에서 마우스로 클릭하는 것이 아니라, **K8s Job이 코드로 자동 설정**합니다. 이것이 GitOps 관점에서 중요합니다. 설정이 코드에 있으니 변경 추적이 되고, 재현 가능합니다.
+
+**3. OAuth2 client secret은 Vault에서 온다**
+
+Keycloak과 auth-server가 통신하려면 **client secret**(일종의 비밀 비밀번호)이 필요합니다. 이 값은 `deployment.vault-patch.yaml`에서 Vault를 통해 주입됩니다.
+
+```yaml
+# deployment.vault-patch.yaml의 일부
+{{ with secret "kv/data/dev/platform/keycloak/client-auth-server" }}
+export APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET={{ printf "%q" .Data.data.APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET }}
+{{ end }}
+```
+
+즉, client secret은 Git에 없고, Vault에만 있고, Pod가 뜰 때 파일로 주입됩니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- OAuth2의 "인가 코드"를 사용자 브라우저가 중간에 가로챌 수 있는가? 가로채도 왜 안전한가? (힌트: 코드를 토큰으로 교환할 때 client secret이 필요하고, 이것은 서버만 알고 있다)
+- OIDC의 ID Token과 OAuth2의 Access Token은 어떤 차이가 있는가? 둘 다 JWT인가?
+- Keycloak이 완전히 다운되면 어떤 일이 생기는가? 이미 로그인한 사용자는 괜찮은가? 새로 로그인하려는 사용자는?
+- `redirectUris`가 잘못 설정되면 어떤 보안 문제가 생기는가? (힌트: Open Redirect 공격)
+- `keycloak-client-sync-job.yaml`에서 `until ... do sleep 5; done` 루프가 있는 이유는 무엇인가? (힌트: Keycloak이 아직 기동 중일 수 있다)
+
+### 24-3. DB 버전 관리(Flyway) 분리 배포 작전
+
+#### 왜 DB 스키마 변경은 특별하게 다뤄야 하는가
+
+일반적인 코드 변경(버그 수정, 기능 추가)은 서버를 새로 배포하면 반영됩니다. 이전 코드가 새 코드로 바뀌는 것뿐이니, 잘못되면 이전 버전으로 롤백하면 됩니다.
+
+하지만 **DB 스키마 변경**은 다릅니다. 새 컬럼을 추가하거나, 테이블 이름을 바꾸거나, 인덱스를 거는 것은 **데이터 구조 자체를 바꾸는 것**입니다. 한번 바꾸면 되돌리기가 훨씬 어렵습니다. 새 컬럼에 데이터가 이미 쌓였다면 단순 롤백으로는 원상복구가 안 됩니다.
+
+**Flyway**는 DB 스키마 변경을 **버전 관리**하는 도구입니다. 코드 변경을 Git으로 관리하듯이, DB 변경을 `V1__create_user.sql`, `V2__add_email_column.sql` 같은 번호 붙은 마이그레이션 파일로 관리합니다.
+
+#### 기존 방식의 문제: Spring Boot 기동 시 Flyway 자동 실행
+
+처음에는 Flyway를 Spring Boot 앱 안에 넣었습니다. 서버가 기동될 때 Flyway가 자동으로 실행되어 DB 스키마를 최신으로 맞추는 방식입니다.
+
+작은 서비스에서는 이것도 괜찮습니다. 하지만 **트래픽이 몰려서 서버를 10대로 늘려야 하는 상황**을 생각해보세요.
+
+```mermaid
+flowchart TB
+ subgraph 문제_상황["서버 10대가 동시에 기동되는 상황"]
+ direction TB
+ S1[auth-server Pod 1] -->|"Flyway: ALTER TABLE users..."| DB[(PostgreSQL)]
+ S2[auth-server Pod 2] -->|"Flyway: ALTER TABLE users..."| DB
+ S3[auth-server Pod 3] -->|"Flyway: ALTER TABLE users..."| DB
+ S4["... Pod 4~10도 동시에"] -->|"Flyway: ALTER TABLE users..."| DB
+ end
+
+ DB -->|"💥 Lock 경합!
누가 먼저야?
DDL Lock 충돌!"| DEAD[배포 데드락
일부 Pod는 마이그레이션 성공
일부 Pod는 Lock 대기 중 타임아웃]
+```
+
+Flyway는 내부적으로 DB Lock을 사용해서 중복 실행을 방지하려고 합니다. 하지만 10대의 서버가 **동시에** 일어나면서 모두 "나 먼저 마이그레이션 할게!"라고 달려들면, Lock 경합이 발생합니다. 일부는 성공하고 일부는 타임아웃으로 실패합니다. 실패한 Pod는 기동에 실패합니다.
+
+#### 이 프로젝트의 해결책: 완전 분리된 K8s Job
+
+이 프로젝트에서는 Flyway를 **앱 서버에서 완전히 떼어내서 별도의 K8s Job으로 분리**했습니다.
+
+```mermaid
+flowchart TD
+ subgraph 해결_구조["이 프로젝트의 배포 흐름"]
+ direction TB
+
+ subgraph PreSync["1단계: PreSync (앱 배포 전)"]
+ JOB[auth-db-migration Job
Flyway 실행
딱 1개만 실행됨] -->|"스키마 변경 완료"| DB2[(PostgreSQL)]
+ end
+
+ subgraph MainSync["2단계: Main Sync (스키마 준비 완료 후)"]
+ S1b[auth-server Pod 1
Flyway 안 함] --> DB2
+ S2b[auth-server Pod 2
Flyway 안 함] --> DB2
+ S3b[auth-server Pod 3
Flyway 안 함] --> DB2
+ end
+
+ PreSync -->|"Job 성공해야
다음 단계 진행"| MainSync
+ end
+```
+
+이 구조의 핵심은 Argo CD의 **PreSync Hook**입니다.
+
+`apps/auth-server/base/db-migration-job.yaml`을 보면:
+
+```yaml
+annotations:
+ argocd.argoproj.io/hook: PreSync # 일반 리소스보다 먼저 실행
+ argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded # 성공하면 정리
+ argocd.argoproj.io/sync-wave: "-1" # 가장 먼저
+```
+
+그리고:
+
+```yaml
+spec:
+ backoffLimit: 1 # 실패하면 1번만 재시도
+ template:
+ spec:
+ restartPolicy: Never # 완료 후 재시작하지 않음
+```
+
+이 어노테이션과 설정을 합치면 이런 동작이 됩니다.
+
+1. Argo CD가 sync를 시작하면 **PreSync 리소스를 먼저** 실행합니다.
+2. migration Job이 **딱 1개** 뜹니다. (Deployment가 아니라 Job이니까 복제본 없음)
+3. Flyway가 DB 스키마를 변경합니다.
+4. Job이 성공하면(exit code 0) Argo CD가 다음 단계(Deployment 등)를 진행합니다.
+5. Job이 실패하면 **배포 전체가 멈춥니다.** 스키마가 반쪽만 적용된 상태로 앱이 뜨는 것을 방지합니다.
+
+auth-server의 ConfigMap에서 `APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP: "false"`인 이유도 이것 때문입니다. 앱 서버 자체는 Flyway를 실행하지 않습니다. 마이그레이션은 오직 Job만 합니다.
+
+#### 분리에 따른 의존성 문제와 해결
+
+PPT에서 언급했듯이, 처음에 Flyway를 분리하려 했을 때 문제가 있었습니다. Flyway가 Spring Boot 앱 안에 있다 보니, 관련 설정 Bean들이 앱 기동에 필요했습니다. 단순히 Flyway 실행만 떼어내는 것으로는 의존성이 끊기지 않았습니다.
+
+최종 해결책은 **Flyway를 아예 별도의 라이브러리 모듈로 분리**하고, Docker 이미지 빌드 시 `migration.jar`라는 독립적인 JAR 파일을 만든 것입니다.
+
+`db-migration-job.yaml`에서 이것이 드러납니다:
+
+```yaml
+containers:
+ - name: auth-db-migration
+ image: ghcr.io/donghyeonka/project-auth-server # 같은 이미지지만
+ command:
+ - java
+ - -jar
+ - /app/migration.jar # migration.jar를 따로 실행
+ env:
+ - name: SPRING_MAIN_WEB_APPLICATION_TYPE
+ value: none # 웹 서버를 띄우지 않음
+ - name: APP_PERSISTENCE_MIGRATION_RUN_ON_STARTUP
+ value: "true" # 이 Job에서만 마이그레이션 실행
+```
+
+`SPRING_MAIN_WEB_APPLICATION_TYPE: none`은 "Spring의 웹 서버(Tomcat 등)를 켜지 마라"는 뜻입니다. migration에는 HTTP 서버가 필요 없으니까요. 이렇게 하면 순수하게 Flyway만 돌리고 종료됩니다.
+
+#### 동적 DB 계정: migration Job은 왜 고정 비밀번호를 안 쓰는가
+
+dev overlay의 `db-migration-job.vault-patch.yaml`을 보면, migration Job은 고정 비밀번호가 아니라 **Vault가 그 순간에 만들어주는 임시 DB 계정**을 사용합니다.
+
+```yaml
+vault.hashicorp.com/agent-inject-secret-migration-env: database/creds/auth-db-migration-dev
+```
+
+경로가 `kv/...`(고정 값 저장소)가 아니라 `database/creds/...`(동적 발급 엔진)인 것이 핵심입니다.
+
+이 경로로 요청하면 Vault가 **그 순간에** PostgreSQL에 접속해서 임시 사용자를 만들고, 짧은 TTL(수명)이 지나면 자동으로 삭제합니다.
+
+왜 이렇게 할까요?
+
+- migration은 **고권한 작업**입니다. 테이블 생성, 컬럼 변경 같은 DDL을 수행합니다.
+- 이런 강력한 권한을 가진 계정이 **영구적으로 존재**하면, 유출 시 피해가 큽니다.
+- 동적 계정은 Job 실행 후 자동 만료되니, 유출되어봐야 이미 삭제된 계정입니다.
+
+그리고 `agent-pre-populate-only: "true"` 어노테이션의 의미도 여기서 명확해집니다. Deployment에 붙는 Vault Agent는 sidecar로 계속 살아있으면서 secret을 갱신할 수 있지만, Job은 **짧게 실행되고 끝나는 일회성 작업**이니 사이드카가 계속 떠 있을 필요가 없습니다. 한 번 secret 파일을 만들어두고 바로 사라지는 것입니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- Flyway 마이그레이션 Job이 중간에 실패해서 테이블은 만들었는데 인덱스는 못 만든 상태라면, 다음 Job 실행 시 어떻게 되는가? Flyway는 이것을 어떻게 감지하는가? (힌트: `flyway_schema_history` 테이블)
+- DDL(테이블 구조 변경)과 DML(데이터 변경) 마이그레이션의 위험도 차이는 무엇인가? 왜 DDL 마이그레이션이 더 위험한가?
+- `backoffLimit: 1`의 의미는 무엇인가? 왜 migration Job은 횟수를 제한하는가?
+- 만약 Flyway가 성공했는데 앱 배포가 실패해서 롤백한다면, DB 스키마는 이전으로 돌아가는가? (답: 아니다. DB 스키마 롤백은 별도 마이그레이션이 필요하다)
+- `database/creds/auth-db-migration-dev`와 `kv/data/dev/platform/postgres/auth-server`의 차이는 무엇인가? 왜 runtime 앱은 KV를 쓰고 migration Job은 database engine을 쓰는가?
+
+## 25. Kubernetes(K3s) 심층 해부와 네트워크 구조
+
+### 25-1. K8s vs K3s: 왜 가볍고 왜 선택했는가
+
+#### Kubernetes의 기본 구조: 두 종류의 노드
+
+Kubernetes를 쉽게 비유하면, **회사의 본사(Control Plane)**와 **공장(Worker Node)**의 관계입니다.
+
+본사는 "무엇을 어디에 얼마나 만들지" 결정하고, 공장은 실제로 물건(컨테이너)을 생산합니다. 본사가 망하면 새 지시가 안 내려가지만, 이미 돌고 있는 공장은 당장은 계속 돌아갑니다.
+
+```mermaid
+flowchart TB
+ subgraph CP["Control Plane (본사)"]
+ direction TB
+ API["API Server
모든 요청의 창구
kubectl 명령이 여기로 들어옴"]
+ ETCD["etcd / SQLite
클러스터의 모든 상태를
저장하는 데이터베이스"]
+ SCHED["Scheduler
새 Pod를 어느 노드에
배치할지 결정"]
+ CM["Controller Manager
선언된 상태와 현재 상태의
차이를 감지하고 조정"]
+
+ API <--> ETCD
+ SCHED --> API
+ CM --> API
+ end
+
+ subgraph W1["Worker Node 1 (공장)"]
+ direction TB
+ KL1["kubelet
API Server의 지시를 받아
컨테이너를 생성/삭제"]
+ KP1["kube-proxy
네트워크 규칙을 관리
(Service → Pod 라우팅)"]
+ CR1["containerd
실제 컨테이너를 실행하는
런타임 엔진"]
+
+ KL1 --> CR1
+ end
+
+ subgraph W2["Worker Node 2 (공장)"]
+ direction TB
+ KL2["kubelet"]
+ KP2["kube-proxy"]
+ CR2["containerd"]
+ KL2 --> CR2
+ end
+
+ API -->|"Watch 스트림으로
이벤트 전달"| KL1
+ API -->|"Watch 스트림으로
이벤트 전달"| KL2
+```
+
+각 컴포넌트가 하는 일을 구체적으로 설명합니다.
+
+| 컴포넌트 | 어디에 있나 | 무슨 일을 하나 | 없으면 어떻게 되나 |
+|---|---|---|---|
+| **API Server** | Control Plane | 모든 통신의 중심. `kubectl` 명령, kubelet 보고, Scheduler 요청이 전부 여기를 거침 | 클러스터 전체가 통신 불가. 신규 배포, 스케일링, 조회 모두 불가능 |
+| **etcd** (K8s) / **SQLite** (K3s) | Control Plane | "이 클러스터에 무엇이 있고, 어떤 상태인지"를 영구 저장. Deployment 몇 개, Pod 몇 개, Service 설정 등 | 상태 정보가 날아감. 클러스터를 처음부터 다시 만들어야 함 |
+| **Scheduler** | Control Plane | 새 Pod가 생겼을 때 "어느 Worker에 배치할지" 결정 (CPU, 메모리, affinity 조건 등 고려) | Pod가 Pending 상태에서 영원히 멈춤 |
+| **Controller Manager** | Control Plane | "선언된 상태(Deployment에 replica: 3)"와 "현재 상태(Pod 2개 살아있음)"의 차이를 발견하고 Pod를 1개 더 만듦 | Pod가 죽어도 자동 복구 안 됨, 스케일링 안 됨 |
+| **kubelet** | 각 Worker | API Server에게 "제 노드에서 이 Pod를 실행하겠습니다"라는 지시를 받고, containerd에게 실제 실행을 시킴 | 해당 노드에서 컨테이너 생성, 삭제, 모니터링 불가 |
+| **kube-proxy** | 각 Worker | Service의 IP로 들어온 트래픽을 실제 Pod의 IP로 전달하는 네트워크 규칙을 관리 | Service를 통한 통신 불가 (Pod IP 직접 지정하면 가능) |
+| **containerd** | 각 Worker | 실제 컨테이너 이미지를 다운로드하고, 프로세스를 격리해서 실행 | 컨테이너 실행 자체가 불가 |
+
+#### kubelet은 API Server에게 어떻게 지시를 받는가 — Watch 메커니즘
+
+여기서 중요한 의문이 생깁니다. kubelet은 API Server에게 명령을 어떻게 받을까요?
+
+두 가지 방식이 가능합니다.
+
+1. **Polling(폴링)**: kubelet이 1초마다 API Server에 "나한테 새 일 있어?" 하고 물어보는 방식
+2. **Watch(감시)**: kubelet이 API Server에 "내 노드에 변경 생기면 바로 알려줘"라고 한 번 등록해놓고, 변경이 있을 때만 알림을 받는 방식
+
+Kubernetes는 **Watch 방식**을 씁니다. 왜냐하면 Worker Node가 100대, 1000대가 되면 모두가 1초마다 물어보는 건 API Server에게 엄청난 부하가 됩니다. Watch는 **변경이 있을 때만** 이벤트를 푸시하니까 훨씬 효율적입니다.
+
+```mermaid
+sequenceDiagram
+ participant KL as kubelet (Worker Node)
+ participant API as API Server (Control Plane)
+
+ KL->>API: "내 노드에 관련된 변경사항을
Watch 스트림으로 구독합니다"
+ Note over KL,API: HTTP Long-Poll 연결이 유지됨
+ API-->>KL: (아무 일 없으면 조용)
+
+ Note over API: 사용자가 kubectl apply로
새 Deployment 생성
+ API->>API: Scheduler가 "Worker Node 1에 배치" 결정
+ API-->>KL: "새 Pod를 실행하세요" 이벤트 푸시
+ KL->>KL: containerd에게 컨테이너 생성 요청
+ KL-->>API: "Pod 실행 중(Running)" 상태 보고
+```
+
+이 구조를 **선언적 상태 관리(Declarative State Management)**라고 합니다.
+
+사용자는 "auth-server를 3개 돌려라"라고 **원하는 상태를 선언**합니다. Controller Manager가 현재 상태와 원하는 상태를 지속적으로 비교합니다. Pod가 1개 죽으면 "2개인데 3개여야 하니까 1개 더 만들어야지"라고 판단하고, 이것을 **Reconciliation Loop(조정 루프)**라고 부릅니다. 이 루프는 **끊임없이** 돕니다.
+
+#### K3s는 이 구조를 어떻게 경량화했는가
+
+K3s는 위의 Kubernetes와 동일한 개념이지만, 일부를 **다이어트**했습니다.
+
+```mermaid
+flowchart LR
+ subgraph K8S["K8s (풀 사이즈)"]
+ direction TB
+ E1["etcd
분산 합의 알고리즘 (Raft)
별도 클러스터 3~5대 필요
수백 MB 메모리"]
+ C1["Cloud Controller Manager
AWS, GCP 연동 코드"]
+ S1["Storage Driver
다양한 CSI 드라이버 포함"]
+ end
+
+ subgraph K3S["K3s (경량)"]
+ direction TB
+ E2["SQLite
단일 파일 DB
단일 바이너리 내장
수 MB 메모리"]
+ C2["없음
클라우드 연동 코드 제거"]
+ S2["Local Path Provisioner
기본 탑재, 간단한 로컬 볼륨"]
+ end
+
+ K8S -->|"이것이 100MB 바이너리 하나로
압축된 것이 K3s"| K3S
+```
+
+| 차이점 | K8s | K3s |
+|---|---|---|
+| 상태 저장소 | **etcd** — 분산 합의 알고리즘(Raft)을 자체 구현한 별도 프로세스. HA를 위해 최소 3대 클러스터 필요 | **SQLite** — 파일 하나(`/var/lib/rancher/k3s/server/db/state.db`)로 상태를 저장. 프로세스 추가 없음 |
+| 바이너리 크기 | 여러 바이너리 합계 수백 MB | **단일 바이너리 약 100MB** (API Server, Scheduler, Controller Manager, kubelet, kube-proxy 전부 포함) |
+| 네트워크 플러그인(CNI) | 별도 설치 필요 | **Flannel 기본 탑재** (설치 없이 바로 Pod 간 통신 가능) |
+| 인그레스 컨트롤러 | 별도 설치 (NGINX, Traefik 등) | **Traefik 기본 탑재** |
+| 자원 요구량 | Control Plane 최소 2GB RAM | **최소 512MB RAM** |
+
+PPT에서 "K3s가 etcd 대신 SQLite를 쓰기 때문에 가볍다"고 설명했습니다. 좀 더 깊이 들어가면, etcd는 **여러 노드 간의 데이터 일관성을 보장**하기 위해 Raft 합의 알고리즘을 돌립니다. 3대의 etcd 노드가 "이 데이터를 저장할게"라고 합의하는 과정 자체가 CPU와 메모리를 많이 씁니다.
+
+K3s의 SQLite는 이런 분산 합의가 **없습니다**. 파일 하나에 기록할 뿐입니다. 대신 트레이드오프가 있습니다. **하나의 Control Plane이 죽으면 상태 저장소가 함께 날아갑니다.** 이것이 K3s가 "실험/개발 환경, IoT 엣지 디바이스"에 적합하고, "대규모 운영 인프라"에는 풀 K8s를 쓰는 이유입니다.
+
+(다만 K3s도 HA를 원하면 SQLite 대신 외부 DB(MySQL, PostgreSQL 등)를 상태 저장소로 쓸 수 있습니다.)
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- etcd의 Raft 합의 알고리즘은 왜 노드가 최소 3대 필요한가? 2대면 안 되는 이유는? (힌트: 과반수 투표)
+- K3s에서 SQLite를 쓰면 HA 구성이 불가능한가? 가능하다면 어떤 방식인가?
+- Reconciliation Loop는 얼마나 자주 도는가? 1초마다? 이벤트가 있을 때만?
+- 우리 프로젝트의 `vault-deployment.yaml`에 `strategy: Recreate`로 설정한 이유는 무엇인가? RollingUpdate와 무엇이 다른가? (힌트: 볼륨 동시 마운트 문제)
+- API Server가 다운되면 이미 돌고 있는 Pod들은 어떻게 되는가? 바로 죽는가?
+
+### 25-2. 복잡한 K8s 네트워크의 마법 (CNI와 DNS)
+
+#### "Service"란 정확히 무엇인가 — 추상적 설명을 넘어서
+
+Part I에서 Service를 "Pod 앞에 놓는 고정된 네트워크 진입점"이라고 설명했습니다. 하지만 이것만으로는 **Service가 별도의 프로세스인지, 가상의 설정인지, 물리적으로 어디에 존재하는지** 감이 안 옵니다.
+
+답부터 말하면: **Service는 독립적인 프로세스나 컨테이너가 아닙니다.** Service는 **kube-proxy가 각 노드의 iptables(또는 IPVS)에 기록해놓은 네트워크 규칙(라우팅 테이블)**입니다.
+
+이것을 실제 동작으로 풀어보겠습니다.
+
+```mermaid
+sequenceDiagram
+ participant APP as auth-server Pod
(다른 Pod에서 vault를 호출)
+ participant IPTABLES as iptables 규칙
(kube-proxy가 관리)
+ participant POD as vault Pod
(실제 컨테이너)
+
+ Note over APP: configmap에 적힌 주소:
vault.vault.svc.cluster.local:8200
+ APP->>APP: DNS 조회: vault.vault.svc.cluster.local
→ CoreDNS가 10.43.x.x (Service의 ClusterIP) 반환
+ APP->>IPTABLES: 10.43.x.x:8200으로 패킷 전송
+ Note over IPTABLES: kube-proxy가 미리 심어놓은 규칙:
"10.43.x.x:8200 → 실제 Pod IP 10.42.y.y:8200"
+ IPTABLES->>POD: 실제 Pod의 IP(10.42.y.y:8200)로 전달
+ POD-->>APP: 응답 반환
+```
+
+이 그림을 단계별로 설명하면 이렇습니다.
+
+**1단계: Service 생성 시** — `vault-service.yaml`을 `kubectl apply`하면 API Server가 이것을 etcd(K3s는 SQLite)에 기록합니다. 이 순간 Kubernetes가 Service에 **ClusterIP**(예: 10.43.x.x)라는 가상 IP를 할당합니다. 이 IP는 **어떤 노드에도 실제로 바인딩되어 있지 않은 가상 주소**입니다.
+
+**2단계: kube-proxy가 규칙 설정** — 각 노드의 kube-proxy가 Watch 스트림으로 "새 Service가 생겼다"는 이벤트를 받습니다. kube-proxy는 자기 노드의 **iptables에 규칙을 추가**합니다. "10.43.x.x:8200으로 가는 패킷은 → 실제 Pod IP 10.42.y.y:8200으로 보내라."
+
+**3단계: Pod가 Service를 호출할 때** — auth-server Pod가 `vault.vault.svc.cluster.local:8200`으로 요청을 보내면, DNS가 이것을 ClusterIP(10.43.x.x)로 해석합니다. 패킷이 노드의 네트워크 스택을 통과할 때, **iptables 규칙에 의해** 실제 Pod IP로 변환됩니다.
+
+그래서 "Service가 Pod 앞에 놓여있다"는 비유적 표현이고, 실제로는 **각 노드의 iptables 규칙이 트래픽을 중계하는 것**입니다. Service라는 별도 프로세스가 떠서 트래픽을 통과시키는 것이 아닙니다.
+
+이 프로젝트의 `infra/vault/base/vault-service.yaml`을 보면:
+
+```yaml
+apiVersion: v1
+kind: Service
+metadata:
+ name: vault # 이 이름이 DNS 이름의 일부가 됨
+spec:
+ selector:
+ app: vault # label이 "app: vault"인 Pod로 트래픽을 연결
+ ports:
+ - name: http
+ port: 8200 # Service가 받는 포트
+ targetPort: 8200 # Pod가 실제로 리스닝하는 포트
+ type: ClusterIP # 클러스터 내부에서만 접근 가능
+```
+
+`selector: app: vault`가 핵심입니다. kube-proxy는 `app: vault` 라벨이 달린 **모든 Pod의 IP**를 수집해서 iptables 규칙에 등록합니다. Pod가 죽으면 규칙에서 제거하고, 새 Pod가 뜨면 규칙에 추가합니다. 이것이 "고정된 진입점"이라고 불리는 이유입니다. **Pod는 죽고 태어나면 IP가 바뀌지만, Service의 ClusterIP와 DNS 이름은 바뀌지 않습니다.**
+
+#### CNI: Pod에 IP를 어떻게 부여하는가
+
+각 Pod에는 고유한 IP 주소가 있습니다. 하지만 Pod는 가상의 컨테이너입니다. 물리 서버처럼 NIC(네트워크 카드)가 있는 것이 아닙니다. 그러면 IP는 어떻게 생기는 걸까요?
+
+이것을 담당하는 것이 **CNI(Container Network Interface)** 플러그인입니다.
+
+K3s는 기본으로 **Flannel**이라는 CNI를 탑재하고 있습니다. Flannel은 **VXLAN 오버레이 네트워크**를 만듭니다.
+
+쉽게 비유하면, 물리적으로 다른 건물(노드)에 있는 사무실(Pod)들을 마치 같은 내부 네트워크에 있는 것처럼 **가상 터널**로 연결하는 것입니다.
+
+```mermaid
+flowchart TB
+ subgraph Node1["Worker Node 1 (물리 IP: 192.168.1.10)"]
+ direction TB
+ P1["auth-server Pod
Pod IP: 10.42.0.5"]
+ P2["migration Job Pod
Pod IP: 10.42.0.6"]
+ F1["Flannel
VXLAN 터널 엔드포인트"]
+ end
+
+ subgraph Node2["Worker Node 2 (물리 IP: 192.168.1.11)"]
+ direction TB
+ P3["vault Pod
Pod IP: 10.42.1.3"]
+ P4["postgres Pod
Pod IP: 10.42.1.4"]
+ F2["Flannel
VXLAN 터널 엔드포인트"]
+ end
+
+ F1 <-->|"VXLAN 터널
Pod 패킷을 캡슐화해서
물리 네트워크 위로 전달"| F2
+ P1 -.-|"10.42.0.5 → 10.42.1.3
다른 노드지만
직접 통신 가능"| P3
+```
+
+auth-server Pod(10.42.0.5)가 vault Pod(10.42.1.3)로 패킷을 보내면:
+1. 패킷이 Flannel의 VXLAN 인터페이스에 도착합니다.
+2. Flannel이 이 패킷을 **UDP로 캡슐화**(원래 패킷을 외부 패킷 안에 넣음)합니다.
+3. 물리 네트워크(192.168.1.10 → 192.168.1.11)를 통해 상대 노드로 전달합니다.
+4. 상대 노드의 Flannel이 캡슐을 벗기고 vault Pod에게 전달합니다.
+
+이것이 **다른 물리 서버에 있는 Pod들이 마치 같은 네트워크에 있는 것처럼** 통신할 수 있는 원리입니다.
+
+#### CoreDNS: vault.vault.svc.cluster.local은 누가 해석하는가
+
+이 프로젝트에서는 `vault.vault.svc.cluster.local`이라는 DNS 이름이 여기저기 등장합니다.
+
+이 이름의 구조를 분해하면:
+
+| 부분 | 의미 |
+|---|---|
+| `vault` | Service의 이름 (`metadata.name: vault`) |
+| `vault` | Service가 속한 Namespace |
+| `svc` | "이것은 Service의 DNS다"라는 고정 접미사 |
+| `cluster.local` | 클러스터의 기본 도메인 |
+
+이 이름을 IP 주소로 변환하는 것은 **CoreDNS**라는 Pod입니다. CoreDNS는 K3s(그리고 K8s)가 기본으로 띄우는 **클러스터 내부 DNS 서버**입니다.
+
+```mermaid
+flowchart LR
+ A["auth-server Pod가
vault.vault.svc.cluster.local
을 호출"] --> B["Pod의 /etc/resolv.conf에
CoreDNS IP가 적혀있음"]
+ B --> C["CoreDNS Pod가
K8s API로부터
Service 목록을 Watch"]
+ C --> D["vault Service의
ClusterIP: 10.43.x.x
를 응답"]
+ D --> E["iptables가
10.43.x.x를
실제 Pod IP로 변환"]
+```
+
+모든 Pod 안에는 `/etc/resolv.conf` 파일이 자동으로 생성되고, 여기에 CoreDNS의 IP가 적혀 있습니다. 그래서 Pod 안에서 `vault.vault.svc.cluster.local`을 호출하면 자동으로 CoreDNS에게 물어보게 됩니다.
+
+`vault.hcl` 설정 파일에서 이 DNS 의존성이 직접 드러납니다:
+
+```hcl
+api_addr = "http://vault.vault.svc.cluster.local:8200"
+cluster_addr = "http://vault.vault.svc.cluster.local:8201"
+```
+
+만약 CoreDNS가 죽으면? DNS 이름 해석이 안 되니, 이 주소로의 통신이 전부 실패합니다. 이미 해석된 IP가 캐시에 있으면 잠시 동안은 되겠지만, 캐시 TTL이 만료되면 끝입니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- `type: ClusterIP`와 `type: NodePort`, `type: LoadBalancer`의 차이는 무엇인가? 이 프로젝트에서 왜 `ClusterIP`만 쓰는가?
+- Flannel(VXLAN)과 Calico(BGP/eBPF)의 핵심 차이는 무엇인가? 왜 K3s는 Flannel을 기본으로 택했는가?
+- CoreDNS의 캐시 TTL이 30초인데, Pod가 20초 만에 죽고 새 Pod가 새 IP로 뜨면 DNS 캐시 때문에 오래된 IP로 가지 않는가?
+- `vault-service.yaml`에서 `port: 8200`과 `targetPort: 8200`이 같은데, 이 둘을 다르게 설정하면 어떤 일이 생기는가? 언제 다르게 쓰는가?
+- NetworkPolicy를 설정하면 iptables 규칙이 바뀌는가, 아니면 CNI 수준에서 별도 방화벽이 생기는가? (힌트: CNI마다 다르다)
+
+### 25-3. 깡통 서버(Stateless)와 볼륨 클레임(PVC)
+
+#### Stateless: 컨테이너가 죽으면 내부 데이터는 전부 사라진다
+
+K8s에서 컨테이너는 **깡통**입니다. 안에 뭘 저장하든, 컨테이너가 삭제되면 전부 사라집니다.
+
+이것이 왜 **장점**인지 직관적이지 않을 수 있습니다. "데이터가 사라지면 안 되지 않나?"
+
+장점은 **교체가 자유롭다**는 것입니다. auth-server Pod가 죽으면 K8s가 새 Pod를 만들어서 교체합니다. 이전 Pod의 "더러운" 상태(메모리 누수, 잘못된 임시 파일 등)가 깨끗이 청소되고, 새 Pod는 완전히 깨끗한 상태에서 시작합니다. 서버를 "수리"하는 것이 아니라 "교체"하는 것입니다. 이것을 **Cattle, not Pets(가축이지 애완동물이 아니다)** 패턴이라고 부릅니다.
+
+하지만 **반드시 데이터를 유지해야 하는 워크로드**가 있습니다:
+- **PostgreSQL**: DB 데이터가 사라지면 끝장입니다.
+- **Vault**: Raft 저장소에 seal/unseal 상태와 secret이 저장되어 있습니다.
+
+이런 워크로드를 위해 **PVC(PersistentVolumeClaim)**가 존재합니다.
+
+#### PVC → PV → StorageClass: 볼륨이 생기는 메커니즘
+
+PVC를 이해하려면 세 가지 개념 사이의 관계를 알아야 합니다.
+
+```mermaid
+flowchart LR
+ subgraph 사용자_요청["개발자가 선언하는 것"]
+ PVC["PVC
(PersistentVolumeClaim)
'5GB 볼륨 하나 주세요'"]
+ end
+
+ subgraph 중간_매개["K8s가 처리하는 것"]
+ SC["StorageClass
'볼륨을 어떤 방식으로
만들지 정의한 템플릿'"]
+ end
+
+ subgraph 실제_저장소["실제로 생성되는 것"]
+ PV["PV
(PersistentVolume)
'실제 5GB 디스크 공간'"]
+ DISK["노드의 로컬 디스크
또는 NFS/클라우드 EBS"]
+ end
+
+ PVC -->|"① '5GB 주세요'
StorageClass 참조"| SC
+ SC -->|"② Provisioner가
실제 볼륨 생성"| PV
+ PV -->|"③ 바인딩 완료"| PVC
+ PV -->|"실제 데이터 저장"| DISK
+```
+
+비유하자면:
+- **PVC**는 "5GB짜리 USB 하나 주세요"라는 **요청서**
+- **StorageClass**는 "USB는 삼성 제품으로, SSD로 만들겠다"는 **제조 사양서**
+- **PV**는 실제로 만들어진 **USB 그 자체**
+
+이 프로젝트의 `infra/vault/base/vault-pvc.yaml`을 보면:
+
+```yaml
+apiVersion: v1
+kind: PersistentVolumeClaim
+metadata:
+ name: vault-data # 이 이름으로 Deployment에서 참조
+spec:
+ accessModes:
+ - ReadWriteOnce # 한 번에 하나의 노드만 읽기/쓰기 가능
+ resources:
+ requests:
+ storage: 5Gi # 5GB 요청
+```
+
+`ReadWriteOnce`는 **이 볼륨을 동시에 두 노드에서 마운트할 수 없다**는 뜻입니다. 하나의 노드에서만 읽고 쓸 수 있습니다.
+
+이것이 `vault-deployment.yaml`에서 `strategy: Recreate`을 쓰는 이유와 직결됩니다.
+
+```yaml
+# vault-deployment.yaml
+spec:
+ strategy:
+ type: Recreate # 기존 Pod를 먼저 죽이고 새 Pod를 띄움
+```
+
+만약 `RollingUpdate`(기본값)를 쓰면, 새 Pod가 먼저 뜨고 나서 기존 Pod를 죽입니다. 그런데 `ReadWriteOnce` 볼륨은 동시에 두 Pod가 마운트할 수 없으니, 새 Pod는 볼륨을 마운트하지 못하고 **Pending 상태에서 영원히 멈춥니다.** 기존 Pod는 "새 Pod가 Ready가 될 때까지 죽지 마라"고 기다리니, 양쪽 다 데드락에 빠집니다.
+
+`Recreate`은 이 문제를 막습니다. 기존 Pod를 **먼저 완전히 종료**시키고, 볼륨 마운트가 해제된 다음에야 새 Pod를 띄웁니다. 대신 업데이트 동안 **잠깐 서비스가 중단**됩니다. Vault는 이 짧은 다운타임을 감수할 수 있으니 이 전략을 택한 것입니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- `ReadWriteOnce`와 `ReadWriteMany`, `ReadOnlyMany`의 차이는 무엇인가? PostgreSQL은 왜 `ReadWriteOnce`를 쓰는가?
+- PVC를 실수로 `kubectl delete pvc`로 삭제하면 데이터가 바로 날아가는가? (힌트: `reclaimPolicy`에 따라 다르다 — `Retain` vs `Delete`)
+- K3s의 기본 StorageClass인 `local-path-provisioner`는 어떻게 동작하는가? 노드의 어느 디렉터리에 데이터가 저장되는가?
+- StatefulSet과 Deployment의 차이는 무엇인가? PostgreSQL은 왜 StatefulSet을 쓰는가? (힌트: Pod 이름 안정성, 볼륨 1:1 대응)
+- `strategy: Recreate` 동안 Vault가 잠깐 죽는데, 이 시간 동안 auth-server의 JWT 서명은 어떻게 되는가?
+
+## 26. HashiCorp Vault Secret 주입 딥다이브
+
+### 26-1. Vault 아키텍처: Seal/Unseal과 Storage Backend
+
+#### Vault는 왜 "봉인(Sealed)" 상태로 시작하는가
+
+Vault를 처음 접하면 혼란스러운 개념이 **Sealed/Unsealed** 상태입니다.
+
+보통의 서비스는 프로세스가 뜨면 바로 사용 가능합니다. 하지만 Vault는 **프로세스가 떠도 "봉인" 상태**이면 아무 요청도 처리하지 못합니다. 모든 읽기/쓰기가 거부됩니다.
+
+왜 이렇게 설계했을까요?
+
+Vault가 저장하는 것은 DB 비밀번호, API 키, 암호화 키 등 **최고 기밀 정보**입니다. 이 정보들은 디스크에 **암호화되어** 저장됩니다. Vault가 기동될 때, 이 암호화된 데이터를 복호화하려면 **마스터 키**가 필요합니다.
+
+Sealed 상태란 **이 마스터 키가 메모리에 없는 상태**입니다. 디스크에 암호화된 데이터가 있지만, 열쇠가 없어서 읽을 수 없습니다.
+
+```mermaid
+flowchart LR
+ subgraph SEALED["Sealed 상태 (기동 직후)"]
+ direction TB
+ S1["디스크: 암호화된 secret들
🔒 잠겨있음"]
+ S2["메모리: 마스터 키 없음
❌ 복호화 불가"]
+ S3["모든 API 요청 → 503 거부"]
+ end
+
+ subgraph UNSEAL["Unseal 과정"]
+ direction TB
+ U1["마스터 키를
메모리에 올림"]
+ end
+
+ subgraph UNSEALED["Unsealed 상태"]
+ direction TB
+ US1["디스크: 여전히 암호화 상태"]
+ US2["메모리: 마스터 키 보유
✅ 요청 시 복호화 가능"]
+ US3["모든 API 요청 → 정상 처리"]
+ end
+
+ SEALED -->|"Unseal 과정
(키 제공)"| UNSEAL
+ UNSEAL --> UNSEALED
+```
+
+중요한 것은 **디스크의 데이터는 Unsealed 후에도 암호화 상태**라는 점입니다. 마스터 키는 오직 메모리에만 존재합니다. Vault 프로세스가 재시작되면 메모리가 초기화되니, 다시 Sealed 상태로 돌아갑니다.
+
+#### 이 프로젝트의 Auto-Unseal: Transit 방식
+
+사람이 매번 수동으로 unseal 하는 것은 실무에서 불가능합니다. Vault가 Pod 재시작될 때마다 24시간 대기하고 있을 수 없으니까요.
+
+이 프로젝트에서는 **Transit Auto-Unseal**을 사용합니다. 또 다른 Vault(Provider Vault, 이 프로젝트에서는 vault-transit)가 마스터 키의 암호화/복호화를 대행하는 패턴입니다.
+
+```mermaid
+sequenceDiagram
+ participant WV as Workload Vault
(우리가 쓰는 Vault)
+ participant PV as Provider Vault
(vault-transit)
+
+ Note over WV: Pod 기동됨 → Sealed 상태
+ WV->>WV: 디스크에서 암호화된
마스터 키를 읽음
+ WV->>PV: "이 암호화된 마스터 키를
Transit 엔진으로 복호화해줘"
+ Note over PV: Transit 키 "workload-vault-dev-unseal"로
복호화 수행
+ PV-->>WV: 복호화된 마스터 키 반환
+ WV->>WV: 마스터 키를 메모리에 올림
→ Unsealed 상태 전환!
+ Note over WV: 이제 모든 secret 읽기/쓰기 가능
+```
+
+이 구조가 `vault.hcl`에 선언되어 있습니다:
+
+```hcl
+seal "transit" {
+ address = "http://vault-transit.vault-transit.svc.cluster.local:8200"
+ disable_renewal = "false"
+ key_name = "workload-vault-dev-unseal"
+ mount_path = "transit/"
+ tls_skip_verify = "true"
+}
+```
+
+- `address`: Provider Vault의 주소입니다. K8s DNS 이름으로 접근합니다.
+- `key_name: "workload-vault-dev-unseal"`: Provider Vault의 Transit 엔진에 있는 암호화 키 이름입니다. 이 키가 Workload Vault의 마스터 키를 암호화/복호화합니다.
+
+여기서 의문이 생깁니다. Workload Vault가 Provider Vault에게 요청을 보내려면 **인증 토큰**이 필요합니다. 이 토큰은 어디서 올까요?
+
+`vault-deployment.yaml`에서 확인할 수 있습니다:
+
+```yaml
+env:
+ - name: VAULT_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: vault-transit-seal # K8s Secret에 저장된
+ key: VAULT_TRANSIT_SEAL_TOKEN # Provider Vault의 제한된 토큰
+```
+
+이 토큰은 **Provider Vault의 Transit 엔진 사용 권한만** 가진 제한적 토큰입니다. 탈취되어도 할 수 있는 건 "unseal 키 복호화"뿐이며, Vault 안의 secret을 직접 읽는 것은 불가능합니다. 이것이 PPT에서 "토큰의 권한을 최소한으로"라고 언급한 부분입니다.
+
+#### Raft Storage: Vault의 내부 저장소
+
+`vault.hcl`의 다른 섹션을 보면:
+
+```hcl
+storage "raft" {
+ path = "/vault/data"
+ node_id = "vault-dev-0"
+}
+```
+
+**Raft**는 분산 합의 알고리즘입니다 (25장의 etcd에서 나온 그것과 같은 알고리즘). Vault는 별도의 외부 DB를 쓰지 않고, **자체적으로 Raft를 돌려서 데이터를 저장**합니다.
+
+현재 단일 노드(`node_id: vault-dev-0`)로 운영하고 있으니 합의 과정은 사실상 "혼자 결정"이지만, 노드를 추가하면 HA 구성이 가능합니다.
+
+`/vault/data` 경로는 PVC(`vault-data`)에 마운트되어 있어서, Pod가 재시작되어도 데이터가 유지됩니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- Provider Vault(vault-transit)도 재시작되면 누가 unseal하는가? 무한 재귀 아닌가? (힌트: Provider Vault는 수동 unseal 또는 별도의 auto-unseal 메커니즘 사용)
+- Raft 단일 노드에서 장애가 나면 복구 방법은? (힌트: PVC에 저장된 Raft 스냅샷)
+- `vault-transit-seal` K8s Secret이 바로 K8s Secret에 저장된다는 게 딜레마라고 PPT에서 언급했는데, 현업에서는 이걸 어떻게 해결하는가? (힌트: 클라우드의 KMS나 HSM)
+- `tls_skip_verify: "true"`는 왜 설정했는가? 운영 환경에서도 이렇게 하면 안 되는 이유는?
+- Vault의 Sealed 상태에서 readinessProbe가 fail하면 K8s는 어떻게 반응하는가?
+
+### 26-2. Vault Agent Injector의 Secret 주입 전체 과정
+
+#### Pod에 Secret이 들어가는 메커니즘: Mutating Webhook
+
+`deployment.vault-patch.yaml`에 `vault.hashicorp.com/agent-inject: "true"` 어노테이션을 달면 마법처럼 secret이 Pod에 들어갑니다. 하지만 이것은 마법이 아닙니다. **K8s의 Mutating Admission Webhook** 메커니즘이 동작하는 것입니다.
+
+이것이 무엇인지 단계별로 풀어보겠습니다.
+
+```mermaid
+sequenceDiagram
+ participant USER as kubectl apply
(또는 Argo CD)
+ participant API as K8s API Server
+ participant INJECTOR as Vault Agent Injector
(Webhook 서버)
+ participant KUBELET as kubelet
+ participant AGENT as Vault Agent
(사이드카 컨테이너)
+ participant VAULT as Vault Server
+
+ USER->>API: "auth-server Pod를 만들어줘"
+ API->>API: 어노테이션 확인:
vault.hashicorp.com/agent-inject: "true"
+ API->>INJECTOR: "이 Pod 정의를 보내는데,
수정할 게 있으면 수정해줘"
+
+ Note over INJECTOR: Pod 정의를 분석
vault 관련 어노테이션 발견
+ INJECTOR->>INJECTOR: Pod 정의에 사이드카 컨테이너
(Vault Agent) 추가
+ INJECTOR->>INJECTOR: 공유 볼륨
(/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 요청
"kv/data/dev/platform/postgres/auth-server"
+ VAULT-->>AGENT: secret 데이터 반환
+ AGENT->>AGENT: Go 템플릿으로 렌더링
→ /vault/secrets/runtime-env 파일 생성
+
+ Note over KUBELET: auth-server 컨테이너가
/vault/secrets/runtime-env 파일을
읽어서 환경변수로 로드
+```
+
+핵심 메커니즘:
+
+**1. Mutating Webhook** — K8s API Server는 Pod가 생성되기 전에 등록된 Webhook 서버에게 "이 Pod 정의를 수정할 기회를 줄게"라고 보냅니다. Vault Agent Injector가 바로 이 Webhook 서버입니다. Injector는 vault 어노테이션이 있는 Pod 정의에 **사이드카 컨테이너(Vault Agent)**와 **공유 볼륨(/vault/secrets)**을 자동으로 추가합니다.
+
+**2. K8s Auth Handshake** — Vault Agent는 Pod 안에 자동으로 마운트된 **K8s ServiceAccount의 JWT 토큰**을 들고 Vault에 인증합니다. Vault는 이 JWT가 진짜인지 **K8s API Server에 직접 물어서** 확인합니다. 진짜라면 해당 ServiceAccount에 매핑된 policy에 따라 Vault 토큰을 발급합니다.
+
+`terraform/vault/reconcile/main.tf`에서 이 매핑이 선언되어 있습니다:
+
+```hcl
+resource "vault_kubernetes_auth_backend_role" "auth_server" {
+ bound_service_account_names = ["auth-server"] # 이 SA만
+ bound_service_account_namespaces = ["auth-dev"] # 이 네임스페이스에서만
+ role_name = "auth-server-dev"
+ token_policies = ["auth-server-dev"] # 이 policy의 권한만 부여
+}
+```
+
+이것은 "auth-dev 네임스페이스의 auth-server ServiceAccount를 가진 Pod만 `auth-server-dev` 정책으로 Vault를 사용할 수 있다"는 뜻입니다. 다른 네임스페이스의 Pod가 같은 이름의 ServiceAccount를 가져도 **접근 불가**입니다.
+
+**3. Template Rendering** — `deployment.vault-patch.yaml`의 이 부분:
+
+```yaml
+vault.hashicorp.com/agent-inject-template-runtime-env: |
+ {{ with secret "kv/data/dev/platform/postgres/auth-server" }}
+ export APP_DATASOURCE_USERNAME={{ printf "%q" .Data.data.APP_DATASOURCE_USERNAME }}
+ export APP_DATASOURCE_PASSWORD={{ printf "%q" .Data.data.APP_DATASOURCE_PASSWORD }}
+ {{ end }}
+```
+
+이것은 **Go 템플릿** 문법입니다. Vault Agent가 secret 데이터를 받아서, 이 템플릿에 맞춰 `/vault/secrets/runtime-env` 파일을 생성합니다. `printf "%q"`는 값을 따옴표로 감싸서 셸에서 안전하게 쓸 수 있도록 이스케이프하는 함수입니다.
+
+결과 파일은 이런 형태가 됩니다:
+```bash
+export APP_DATASOURCE_USERNAME="auth_user"
+export APP_DATASOURCE_PASSWORD="s3cret!p@ss"
+```
+
+auth-server 컨테이너의 시작 명령이 `. /vault/secrets/runtime-env`로 이 파일을 source하면, 환경변수로 로드됩니다.
+
+#### Sidecar vs Pre-Populate-Only: Deployment용과 Job용의 차이
+
+| | Deployment (auth-server) | Job (db-migration) |
+|---|---|---|
+| 어노테이션 | `agent-inject: "true"` (기본값) | `agent-pre-populate-only: "true"` |
+| Vault Agent 수명 | **sidecar로 계속 살아있음** | **init container로 한 번 실행 후 종료** |
+| Secret 갱신 | 주기적으로 Vault에 재요청하여 파일 갱신 가능 | 불가 (한 번 생성하고 끝) |
+| 적합한 워크로드 | 오래 실행되는 서비스. Secret rotation 시 파일이 자동 갱신됨 | 짧게 실행되고 끝나는 작업. sidecar가 계속 떠 있으면 Job이 "완료"로 전환 안 됨 |
+
+Job에서 `agent-pre-populate-only: "true"`를 안 쓰면 어떻게 될까요? Vault Agent sidecar가 계속 살아있으니까, 메인 컨테이너(migration)가 끝나도 **Pod가 Completed 상태로 전환되지 않습니다.** Job은 Pod가 완료되어야 성공으로 간주하는데, sidecar가 죽지 않으니 Job이 영원히 Running 상태에 머물러서 배포가 멈춥니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- `vault.hashicorp.com/agent-inject: "true"` 어노테이션이 필요한 것 외에, 네임스페이스에 어떤 라벨이 있어야 Injector가 동작하는가? (힌트: `vault-injection: enabled`)
+- Vault Agent가 secret rotation을 감지하면 파일은 갱신되지만, 앱 프로세스는 파일을 다시 읽지 않으면 반영이 안 된다. 이것을 어떻게 해결하는가?
+- `printf "%q"`를 안 쓰고 그냥 `.Data.data.PASSWORD`를 출력하면 어떤 문제가 생기는가? (힌트: 비밀번호에 특수문자가 있으면 셸 해석 문제)
+- `automountServiceAccountToken: true`가 왜 Vault Agent가 있는 Pod에서 필수인가?
+- 이 프로젝트에서 `vault.hashicorp.com/agent-inject-token: "true"` 어노테이션은 왜 auth-server에만 있고 migration Job에는 없는가?
+
+### 26-3. Transit Engine: JWT 서명과 Auto-Unseal의 이중 역할
+
+#### Transit Engine이란: "키를 꺼내주지 않고 대신 연산해주는 서비스"
+
+일반적인 암호화에서는 키를 가져와서 내 코드에서 직접 암호화/복호화를 합니다. 하지만 이 방식은 키가 애플리케이션 메모리에 올라오니, 메모리 덤프 등으로 키가 유출될 위험이 있습니다.
+
+Vault의 **Transit Engine**은 다릅니다. **키가 Vault 밖으로 절대 나가지 않습니다.** 대신 "이 데이터를 암호화해줘", "이 데이터에 서명해줘"라고 **API로 요청**하면, Vault가 내부에서 키를 써서 결과만 돌려줍니다.
+
+```mermaid
+flowchart LR
+ subgraph 위험한_방식["일반적인 방식 (키 유출 위험)"]
+ A1["앱이 키를 다운로드"] --> A2["앱 메모리에 키 올림"] --> A3["앱이 직접 서명"]
+ end
+
+ subgraph 안전한_방식["Transit Engine 방식"]
+ B1["앱이 Vault API 호출
'이 데이터에 서명해줘'"] --> B2["Vault가 내부에서
키로 서명 수행"] --> B3["서명 결과만 반환
키는 Vault 밖으로 안 나감"]
+ end
+```
+
+#### 이 프로젝트에서 Transit의 두 가지 역할
+
+Transit Engine은 이 프로젝트에서 **두 가지 완전히 다른 목적**으로 사용됩니다.
+
+**역할 1: auth-server의 JWT 서명**
+
+auth-server가 사용자에게 JWT 토큰을 발급할 때, RSA 개인키로 서명해야 합니다. 이 키를 auth-server의 메모리에 올리는 대신, **Vault Transit에게 서명을 위임**합니다.
+
+`runbooks/vault/dev/policies/auth-server-dev.hcl`:
+```hcl
+path "transit/keys/project-auth-jwt" {
+ capabilities = ["read"] # 공개키 읽기 (JWT 검증용)
+}
+
+path "transit/sign/project-auth-jwt" {
+ capabilities = ["update"] # 서명 요청 (JWT 발급용)
+}
+```
+
+auth-server는 `transit/sign/project-auth-jwt`로 "이 JWT 페이로드에 서명해줘"라고 요청합니다. Vault가 `project-auth-jwt`라는 RSA 키로 서명한 결과를 돌려줍니다. auth-server는 개인키를 **한 번도 본 적이 없습니다.**
+
+**역할 2: Workload Vault의 Auto-Unseal**
+
+26-1에서 설명한 것처럼, Provider Vault(vault-transit)의 Transit Engine이 Workload Vault의 마스터 키를 암호화/복호화합니다. Transit 키 이름은 `workload-vault-dev-unseal`입니다.
+
+두 역할을 그림으로 보면:
+
+```mermaid
+flowchart TB
+ subgraph TRANSIT["Provider Vault의 Transit Engine"]
+ direction TB
+ K1["키: project-auth-jwt
(RSA 키, JWT 서명용)"]
+ K2["키: workload-vault-dev-unseal
(AES 키, Unseal용)"]
+ end
+
+ AUTH["auth-server Pod"] -->|"'이 JWT에 서명해줘'
transit/sign/project-auth-jwt"| K1
+ VAULT["Workload Vault"] -->|"'이 마스터키 복호화해줘'
transit/decrypt/workload-vault-dev-unseal"| K2
+```
+
+같은 Transit Engine이지만, **다른 키를 사용해서 완전히 다른 목적**으로 쓰이고 있습니다. `terraform/vault-transit/reconcile/main.tf`(또는 `dev/main.tf`)에서 이 키들이 Terraform으로 선언되어 있습니다.
+
+#### Transit 키 Rotation: 기존 JWT는 검증 실패하는가?
+
+Transit 키를 rotation(교체)하면 새 버전의 키가 생깁니다. Vault Transit은 **키 버전 관리**를 합니다.
+
+- **서명**: 항상 **최신 버전**의 키로 서명합니다.
+- **검증**: 서명에 포함된 키 버전 정보를 보고, **해당 버전**의 키로 검증합니다.
+
+즉, 키를 rotation해도 **이전 버전의 키가 삭제되지 않으면** 기존 JWT 검증은 계속 성공합니다. 이전 버전을 명시적으로 "min_decryption_version"으로 제한하기 전까지는 안전합니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- Transit Engine으로 JWT 서명을 하면 auth-server가 Vault에 의존하게 됩니다. Vault가 다운되면 JWT 발급이 불가한데, 이 단일 장애점(SPOF)을 어떻게 완화할 수 있는가?
+- `transit/keys/project-auth-jwt`에 `read` 권한을 준 이유는? read로 무엇을 가져오는가? (힌트: 공개키를 가져와서 JWT 검증에 사용)
+- Transit 키의 `min_encryption_version`과 `min_decryption_version`을 올리면 되돌릴 수 없다. 왜 위험한가?
+- auth-server가 Vault API를 호출하는 것과 직접 메모리에서 키로 서명하는 것의 **성능 차이**는 어느 정도인가? 이 트레이드오프는 어떤 상황에서 정당화되는가?
+- Provider Vault와 Workload Vault를 왜 분리했는가? 하나의 Vault에서 모든 것을 하면 안 되는 이유는?
+
+## 27. Terraform IaC와 Bash 자동화 심층 해부
+
+### 27-1. Terraform의 State 관리와 Plan/Apply 사이클
+
+#### Terraform은 대체 무엇을 하는 도구인가
+
+Kubernetes에서 `kubectl apply -f deployment.yaml`을 하면, K8s API Server가 "이 Deployment를 만들어라"라는 지시를 받고 처리합니다. Terraform도 **비슷한 역할**을 하지만, 대상이 K8s가 아닙니다.
+
+Terraform은 **인프라를 코드로 선언하고, 그 코드의 상태를 추적하면서, 변경이 필요한 부분만 자동으로 적용**하는 도구입니다.
+
+이 프로젝트에서 Terraform이 관리하는 것들은:
+- Vault의 **policy** (누가 어떤 secret을 읽을 수 있는지)
+- Vault의 **K8s auth backend role** (어떤 ServiceAccount가 어떤 policy를 받는지)
+- Vault의 **KV secret** (Provider Vault에서 Workload Vault로 secret 복사)
+- Vault의 **database secret engine** (동적 DB 자격 증명 설정)
+
+이것들을 사람이 하나씩 `vault` CLI로 수동 설정할 수도 있습니다. 하지만 그러면 **"지금 어디까지 설정했지?"를 기억하는 것이 불가능**합니다. 10개의 policy, 5개의 role, 8개의 secret을 수동으로 관리하면 누락이 생기고, 재현이 안 됩니다.
+
+#### State 파일: "내가 무엇을 만들었는지" 기억하는 메모장
+
+Terraform의 핵심은 **State 파일**입니다. 이것은 "Terraform이 지금까지 무엇을 만들었는지"를 기록한 JSON 파일입니다.
+
+```mermaid
+flowchart TD
+ subgraph 선언["개발자가 작성한 것 (main.tf)"]
+ TF["'auth-server-dev policy를 만들어라'
'auth-server K8s auth role을 만들어라'
'postgres secret을 복사해라'"]
+ end
+
+ subgraph state["State 파일 (.tfstate)"]
+ ST["'auth-server-dev policy: 만들었음 ✅'
'auth-server K8s auth role: 만들었음 ✅'
'postgres secret: 만들었음 ✅'"]
+ end
+
+ subgraph 실제["실제 인프라 (Vault)"]
+ REAL["auth-server-dev policy 존재
auth-server K8s auth role 존재
postgres secret 존재"]
+ end
+
+ TF -->|"terraform plan
선언 vs State 비교"| ST
+ ST -->|"terraform apply
차이만 실제에 적용"| REAL
+ REAL -->|"적용 결과를
State에 기록"| ST
+```
+
+이 흐름을 구체적으로 설명하면:
+
+**1. `terraform plan`** — "내가 원하는 상태(main.tf)"와 "지금까지 만든 것(State 파일)"을 비교합니다. 차이가 있으면 "이것을 추가하겠다", "이것을 수정하겠다", "이것을 삭제하겠다"는 계획을 보여줍니다. **아직 아무것도 실행하지 않습니다.** 계획만 보여주는 단계입니다.
+
+**2. `terraform apply`** — plan에서 나온 차이를 **실제로 적용**합니다. Vault API를 호출해서 policy를 만들고, role을 설정하고, secret을 복사합니다.
+
+**3. State 업데이트** — apply가 끝나면 "이것을 만들었다"는 기록을 State 파일에 저장합니다.
+
+다음에 같은 코드로 `terraform apply`를 다시 실행하면, State 파일을 보고 "이미 다 만들어져 있네, 할 일 없음"이라고 판단합니다. **멱등성(idempotency)** — 같은 코드를 여러 번 실행해도 결과가 동일합니다.
+
+#### State가 손상되거나 사라지면?
+
+State 파일이 손상되면 Terraform은 **자기가 무엇을 만들었는지 모르는 상태**가 됩니다. 이 상태에서 `terraform apply`를 하면, 이미 존재하는 리소스를 또 만들려고 시도해서 에러가 발생합니다.
+
+이런 상황을 복구하는 것이 `terraform import`입니다. "이 리소스는 이미 실제로 존재하는데, State에 기록이 없으니 기록해줘"라는 명령입니다.
+
+이 프로젝트의 `scripts/ci/reconcile-vault-dev.sh`에 이 패턴이 함수로 정의되어 있습니다:
+
+```bash
+ensure_transit_state_resource() {
+ local address="$1"
+ local import_id="$2"
+
+ # State에 이 리소스가 있는지 확인
+ if ! terraform ... state show "$address" >/dev/null 2>&1; then
+ # 없으면 import로 기존 리소스를 State에 등록
+ log "Importing missing vault-transit state for ${address}"
+ terraform ... import "$address" "$import_id"
+ fi
+}
+```
+
+이 함수가 하는 일을 풀어보면:
+1. `terraform state show "$address"` — State 파일에서 이 리소스가 기록되어 있는지 확인합니다.
+2. 기록이 없으면(`if !`) — `terraform import`로 실제 Vault에 존재하는 리소스를 State에 등록합니다.
+3. 기록이 있으면 — 아무것도 안 합니다.
+
+이것은 **CI 파이프라인이 State 손실에 강해지도록** 만드는 방어적 패턴입니다. CI 환경은 매번 깨끗한 러너에서 실행될 수 있으니, State가 없을 수도 있습니다. 그래도 에러 없이 이어서 할 수 있도록 보장합니다.
+
+#### Provider 이중 설정: 왜 Vault Provider가 두 개인가
+
+`terraform/vault/reconcile/main.tf`의 상단을 보면:
+
+```hcl
+provider "vault" {
+ address = var.workload_vault_addr # Workload Vault (우리가 쓰는 것)
+ token = var.workload_vault_token
+}
+
+provider "vault" {
+ alias = "transit"
+ address = var.transit_vault_addr # Provider Vault (Transit용)
+ token = var.transit_vault_token
+}
+```
+
+Terraform의 **하나의 provider 블록은 하나의 서버**에 연결됩니다. 이 프로젝트에는 Vault가 **두 대**(Workload + Transit)이니, provider도 두 개가 필요합니다.
+
+`alias = "transit"`이 붙은 provider는 Transit Vault에 연결됩니다. 코드에서 `provider = vault.transit`를 지정하면 Transit Vault에 요청을 보내고, 지정하지 않으면 기본 provider(Workload Vault)에 요청을 보냅니다.
+
+```hcl
+# Provider Vault에서 secret 읽기 (transit alias 사용)
+data "vault_kv_secret_v2" "provider_postgres_superuser" {
+ provider = vault.transit # ← Transit Vault에서 읽겠다
+ mount = var.seed_kv_mount_path
+ name = "dev/workload/platform/postgres/superuser"
+}
+
+# Workload Vault에 secret 쓰기 (기본 provider)
+resource "vault_kv_secret_v2" "platform_postgres_superuser" {
+ # provider 지정 없음 → Workload Vault에 쓴다
+ mount = var.kv_mount_path
+ name = "dev/platform/postgres/superuser"
+ data_json = jsonencode({
+ POSTGRES_SUPERUSER_PASSWORD = data.vault_kv_secret_v2.provider_postgres_superuser.data["POSTGRES_SUPERUSER_PASSWORD"]
+ })
+}
+```
+
+이 두 블록을 합치면: **Transit Vault에서 secret을 읽어서 → Workload Vault에 복사**하는 것입니다. 이것이 Terraform이 하는 "Secret 브릿징" 역할입니다.
+
+#### Data Source vs Resource: 읽기 전용과 쓰기의 차이
+
+| 키워드 | 의미 | 이 프로젝트 예시 |
+|---|---|---|
+| `data` | **읽기 전용**. 이미 존재하는 것을 참조만 함. Terraform이 관리하지 않음 | `data "vault_kv_secret_v2"` — Transit Vault의 secret을 **읽기만** |
+| `resource` | **생성/수정/삭제**. Terraform이 생명주기를 관리함. State에 기록됨 | `resource "vault_kv_secret_v2"` — Workload Vault에 secret을 **생성** |
+| `resource` | | `resource "vault_policy"` — Vault policy를 **생성/업데이트** |
+| `resource` | | `resource "vault_kubernetes_auth_backend_role"` — K8s Auth role **생성** |
+
+`data`로 읽은 값을 `resource`에서 사용하는 것이 **참조 패턴**입니다. Transit Vault에서 `data`로 비밀번호를 읽고, Workload Vault에 `resource`로 복사하는 것이 이 패턴의 전형입니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- `terraform destroy`를 CI에서 실수로 실행하면 어떻게 되는가? 모든 policy, role, secret이 삭제되면 서비스에 어떤 영향이 있는가?
+- State 파일(`.tfstate`)에는 민감한 정보(secret 값 등)가 포함되는가? 그렇다면 이 파일을 어떻게 보호해야 하는가?
+- `terraform plan`에서 "변경 없음"이 나왔는데 `terraform apply`를 하면 실제로 변경되는 경우가 있는가? (힌트: provider의 API가 plan 시점과 apply 시점 사이에 바뀌면)
+- 이 프로젝트에서 `backend "local"`을 쓰고 있는데, 팀으로 협업할 때는 왜 remote backend(S3, GCS 등)를 써야 하는가?
+- `skip_child_token = true`가 provider 설정에 있는 이유는 무엇인가? 이것을 안 쓰면 어떤 문제가 생기는가?
+
+### 27-2. Bash 방어적 프로그래밍: trap, set, wait
+
+#### `set -euo pipefail` — 셸 스크립트의 안전벨트 3종 세트
+
+이 프로젝트의 모든 Bash 스크립트는 첫 줄 근처에 이것이 있습니다:
+
+```bash
+set -euo pipefail
+```
+
+이것은 **세 가지 안전장치**를 동시에 거는 것입니다. 각각이 무엇인지, **없으면 어떤 끔찍한 일이 생기는지** 구체적으로 보겠습니다.
+
+**`-e` (errexit): 에러 발생 시 즉시 종료**
+
+```bash
+# -e 없이 실행하면:
+kubectl apply -f wrong-file.yaml # ← 에러 발생! 파일이 없음
+echo "배포 성공!" # ← 이 줄이 실행됨!!! 에러가 무시됨
+vault write secret/data ... # ← 잘못된 상태에서 계속 진행
+
+# -e 있으면:
+kubectl apply -f wrong-file.yaml # ← 에러 발생!
+# 스크립트 즉시 종료. 아래 줄은 실행 안 됨
+```
+
+`-e` 없이 스크립트를 짜면, 중간에 에러가 나도 멈추지 않고 **다음 줄로 넘어갑니다.** CI에서 이것은 치명적입니다. 앞 단계가 실패했는데 뒷 단계가 계속 실행되면, 반쪽짜리 인프라가 구성됩니다.
+
+**`-u` (nounset): 정의되지 않은 변수 사용 시 에러**
+
+```bash
+# -u 없이:
+echo "Vault 주소: ${VAULT_ADR}" # ← 오타! VAULT_ADDR가 맞는데
+# → "Vault 주소: " 빈 문자열 출력, 에러 없이 넘어감
+vault login -address="" # ← 빈 주소로 로그인 시도...
+
+# -u 있으면:
+echo "Vault 주소: ${VAULT_ADR}" # ← 에러! "VAULT_ADR: unbound variable"
+# 스크립트 즉시 종료. 변수 오타를 바로 잡을 수 있음
+```
+
+변수 이름 오타는 누구나 합니다. `-u`가 없으면 오타된 변수가 빈 문자열로 조용히 치환되어, 디버깅하기 극도로 어려운 버그가 됩니다.
+
+**`-o pipefail`: 파이프라인에서 중간 명령 에러 전파**
+
+```bash
+# pipefail 없이:
+vault read secret/data | jq '.data'
+# 만약 vault read가 실패해도, jq가 성공(빈 입력에 에러 없이 종료)하면
+# 전체 파이프라인은 "성공"으로 간주됨!
+
+# pipefail 있으면:
+vault read secret/data | jq '.data'
+# vault read가 실패하면 → 파이프라인 전체가 실패로 간주
+```
+
+`A | B`에서 `-o pipefail` 없이는 **B의 종료 코드만** 확인합니다. A가 실패해도 B가 성공이면 전체가 성공입니다. `pipefail`을 켜면 A, B 중 **하나라도** 실패하면 전체가 실패합니다.
+
+#### `trap ... EXIT` — 뒷정리 보장 메커니즘
+
+`scripts/ci/reconcile-vault-dev.sh`의 `start_port_forward` 함수를 보면:
+
+```bash
+start_port_forward() {
+ local namespace="$1"
+ local service="$2"
+ local local_port="$3"
+ local remote_port="$4"
+ local log_file="$5"
+
+ # port-forward를 백그라운드에서 실행
+ kubectl -n "$namespace" port-forward "svc/${service}" \
+ "${local_port}:${remote_port}" >"$log_file" 2>&1 &
+
+ local pf_pid=$! # 방금 백그라운드로 보낸 프로세스의 PID를 저장
+
+ # EXIT 트랩: 스크립트가 어떤 이유로든 종료될 때 이 프로세스를 죽임
+ trap 'kill "$pf_pid" >/dev/null 2>&1 || true' EXIT
+
+ printf '%s\n' "$pf_pid"
+}
+```
+
+이 코드를 한 줄씩 해부합니다.
+
+**`... &` (백그라운드 실행)** — `kubectl port-forward`는 끝나지 않는 프로세스입니다. 포트 포워딩을 유지하려고 계속 대기합니다. 이것을 `&`로 백그라운드로 보내야 스크립트의 다음 줄이 실행됩니다. `&` 없이 실행하면 스크립트가 여기서 영원히 멈춥니다.
+
+**`$!` (마지막 백그라운드 PID)** — 방금 `&`로 보낸 프로세스의 PID(프로세스 번호)를 캡처합니다. 나중에 이 프로세스를 죽이려면 PID를 알아야 합니다.
+
+**`trap 'kill "$pf_pid" ...' EXIT`** — 이것이 핵심입니다. `trap`은 "특정 신호를 받으면 이 명령을 실행하라"는 뜻입니다. `EXIT`는 "스크립트가 종료될 때"입니다.
+
+```mermaid
+flowchart TD
+ A["스크립트 시작"] --> B["port-forward 프로세스 시작
PID 저장"]
+ B --> C["trap 등록:
'종료 시 PID를 kill해라'"]
+ C --> D["Terraform apply 등
메인 작업 실행"]
+ D --> E{"결과는?"}
+ E -->|"성공"| F["스크립트 정상 종료"]
+ E -->|"에러 발생 (-e로 즉시 종료)"| G["스크립트 에러 종료"]
+ F --> H["trap 발동:
port-forward 프로세스 kill"]
+ G --> H
+ H --> I["깔끔하게 종료됨
좀비 프로세스 없음"]
+```
+
+trap이 없으면 어떻게 될까요?
+
+스크립트가 에러로 중단되면 `kill` 명령이 실행되지 않고, **port-forward 프로세스가 좀비처럼 살아남습니다.** CI 러너에서 이런 좀비 프로세스가 쌓이면 포트 충돌이 발생하고, 다음 CI 실행이 실패합니다.
+
+`|| true`는 "kill이 실패해도(이미 프로세스가 죽어있어도) 에러로 취급하지 마라"는 뜻입니다. `-e`가 켜져 있으니, `kill` 실패가 스크립트 종료를 유발하는 것을 방지합니다.
+
+#### `require_cmd` / `require_env` — 사전 조건 검증 패턴
+
+```bash
+require_cmd() {
+ local cmd="$1"
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ echo "$cmd is required" >&2
+ exit 1
+ fi
+}
+
+require_env() {
+ local name="$1"
+ if [[ -z "${!name:-}" ]]; then
+ echo "$name must be set" >&2
+ exit 1
+ fi
+}
+```
+
+이 함수들은 스크립트의 **맨 처음**에 호출됩니다:
+
+```bash
+reconcile_transit() {
+ require_cmd kubectl
+ require_cmd vault
+ require_cmd terraform
+ require_cmd curl
+ require_cmd jq
+ require_env TF_STATE_DIR
+ require_env TRANSIT_VAULT_ADDR
+ # ... 여기서 필수 도구와 환경변수가 있는지 먼저 확인
+```
+
+이 패턴의 목적: **10분 동안 실행한 뒤에 "jq가 없습니다"로 실패하는 것을 방지**합니다. 필수 조건을 스크립트 시작 시점에 전부 확인하고, 하나라도 빠지면 즉시 종료합니다. 디버깅 시간을 극적으로 줄여줍니다.
+
+`${!name:-}`의 의미: `${!name}`는 **변수 간접 참조**입니다. `name` 변수에 "VAULT_ADDR"가 들어있으면, `${!name}`는 `$VAULT_ADDR`의 값을 가져옵니다. `:-`는 "변수가 없으면 빈 문자열을 반환"하는 기본값 구문으로, `-u` 옵션에 의한 에러를 방지합니다.
+
+#### `kubectl wait` vs `sleep` — 조건 기반 대기의 중요성
+
+```bash
+# 이 프로젝트의 방식 (올바른 방법)
+kubectl -n vault-transit wait --for=condition=available \
+ deployment/vault-transit --timeout=300s
+
+# 나쁜 방법
+sleep 60 # 60초면 되겠지...?
+```
+
+| | `kubectl wait` | `sleep` |
+|---|---|---|
+| 대기 방식 | **조건을 지속 확인**. 조건 만족 즉시 통과 | 고정 시간만큼 무조건 대기 |
+| 리소스가 30초에 준비되면 | 30초에 즉시 다음 단계 진행 | 60초 다 기다림 (30초 낭비) |
+| 리소스가 90초에 준비되면 | 90초에 즉시 다음 단계 진행 | 60초에 성공으로 간주... **실제로는 아직 안 됨!** |
+| timeout 처리 | `--timeout=300s` 초과 시 에러 코드 반환 → `-e`로 스크립트 종료 | timeout 개념 자체가 없음 |
+
+`sleep`은 **"이 정도면 되겠지"라는 추측**에 기반합니다. CI 환경의 부하 상태에 따라 리소스 준비 시간이 달라지니, 어떤 때는 되고 어떤 때는 안 되는 **불안정한(flaky) 파이프라인**이 됩니다.
+
+`kubectl wait`는 **실제 상태를 확인**합니다. Deployment가 Available 조건을 만족하는 순간 통과하고, timeout 안에 안 되면 명확하게 실패합니다.
+
+#### 이 주제에서 스스로 던져봐야 할 질문들
+
+- `trap`이 여러 번 호출되면 어떤 것이 실행되는가? 마지막에 등록된 것만? 전부? (힌트: 같은 신호에 대한 trap은 덮어쓰기됨)
+- `-e`가 켜져 있을 때 `if ! command ...` 구문에서 command가 실패하면 스크립트가 종료되는가? (힌트: `if`문 안에서는 `-e`가 일시 중단됨)
+- `2>&1`의 의미는 무엇인가? `>&2`와는 무엇이 다른가? (힌트: 표준 에러 리다이렉션 방향의 차이)
+- `terraform apply -auto-approve`는 plan 확인 없이 바로 적용한다. CI에서는 왜 이것을 쓰는가? 사람이 직접 실행할 때는 왜 위험한가?
+- `reconcile-vault-dev.sh`에서 `transit_tf_token="$(transit_login)"`을 왜 두 번 호출하는가? 한 번이면 안 되는가? (힌트: policy 업데이트 후 새 토큰이 필요)
diff --git a/docs/archive/README-legacy.md b/docs/archive/README-legacy.md
new file mode 100644
index 0000000..31681d1
--- /dev/null
+++ b/docs/archive/README-legacy.md
@@ -0,0 +1,1601 @@
+# Project-Auth-GitOps
+GitOps repo에서는 앱/인프라별 공통(base)과 환경별 차이(overlay)를 관리하고, 실제 운영 선언만 둡니다.
+
+## 현재 최신 Dev 아키텍처
+
+최상단 아키텍처는 **항상 최신 dev 기준**만 유지합니다.
+아키텍처가 변경되면 이 섹션은 최신 상태로 갱신하고, 변경 이유와 전후 비교는 아래 cycle에 누적 기록합니다.
+
+```mermaid
+flowchart TD
+ subgraph APP_REPOS[Application Repos]
+ R1[Project-Auth-Server CI]
+ R2[Project-Api-Server CI]
+ end
+
+ subgraph REPO[Project-Auth-GitOps]
+ subgraph AUTOMATION[CD automation]
+ U1[.github/workflows/update-image-tag.yaml]
+ U2[.github/workflows/vault-dev-reconcile.yaml]
+ end
+
+ subgraph ARGO[argocd/dev]
+ P1[projects/apps-project.yaml]
+ P2[projects/infra-project.yaml]
+ A1[applications/apps/auth-server.yaml]
+ A2[applications/apps/api-server.yaml]
+ A3[applications/infra/vault-transit.yaml]
+ A4[applications/infra/vault.yaml]
+ A5[applications/infra/platform.yaml]
+ A6[applications/infra/vault-agent-injector.yaml]
+ A7[applications/infra/sealed-secrets.yaml]
+ end
+
+ subgraph MANIFESTS[manifests]
+ subgraph APPS[apps]
+ M1[auth-server/overlays/dev]
+ M2[api-server/overlays/dev]
+ end
+ subgraph INFRA[infra]
+ M3[vault-transit/overlays/dev]
+ M4[vault/overlays/dev]
+ M5[platform/overlays/dev]
+ end
+ end
+ end
+
+ subgraph CI[Self-hosted Runner]
+ C1[reconcile transit provider policy + workflow role]
+ C2[read workload bootstrap AppRole from provider Vault]
+ C3[reconcile workload Vault policy + runtime KV + db roles]
+ C4[apply Argo CD dev apps]
+ end
+
+ subgraph OPERATOR[Operator Workstation]
+ O1[runbooks/vault-transit/dev bootstrap]
+ O2[runbooks/vault/dev bootstrap]
+ end
+
+ subgraph CLUSTER[Dev Cluster]
+ N1[namespace: auth-dev]
+ N2[namespace: api-dev]
+ N3[namespace: platform]
+ N4[namespace: vault]
+ N5[namespace: vault-transit]
+ N6[namespace: kube-system]
+ W1[auth-server]
+ W2[api-server]
+ W3[postgres]
+ W4[keycloak]
+ W5["vault-transit
Transit unseal provider"]
+ W6["vault
KV + Kubernetes auth + Transit JWT"]
+ W7[vault-agent-injector]
+ W8[sealed-secrets-controller]
+ W9[traefik ingress]
+ end
+
+ R1 -->|new auth image tag| U1
+ R2 -->|new api image tag| U1
+ U1 --> M1
+ U1 --> M2
+ U2 --> C1
+ C1 --> C2
+ C2 --> C3
+ C3 --> C4
+
+ P1 --> A1
+ P1 --> A2
+ P2 --> A3
+ P2 --> A4
+ P2 --> A5
+ P2 --> A6
+ P2 --> A7
+
+ A1 --> M1
+ A2 --> M2
+ A3 --> M3
+ A4 --> M4
+ A5 --> M5
+ A6 --> N4
+
+ M1 --> N1
+ M2 --> N2
+ M3 --> N5
+ M4 --> N4
+ M5 --> N3
+ A7 --> N6
+ O1 --> W5
+ O2 --> W6
+ C1 --> W5
+ C2 --> W5
+ C3 --> W6
+ C4 --> A1
+ C4 --> A2
+
+ N1 --> W1
+ N2 --> W2
+ N3 --> W3
+ N3 --> W4
+ N4 --> W6
+ N4 --> W7
+ N6 --> W8
+ N6 --> W9
+ N5 --> W5
+
+ O2 -. browser/API access via local hosts mapping .-> W9
+ W9 --> W1
+ W9 --> W2
+ W9 --> W4
+ W2 -. JWT issuer .-> W1
+ W1 -. datasource .-> W3
+ W1 -. oauth2 provider .-> W4
+ W6 -. transit auto-unseal .-> W5
+ W1 -. kubernetes auth + transit .-> W6
+ W3 -. injector secret render .-> W6
+ W4 -. injector secret render .-> W6
+```
+
+## 이 저장소의 역할
+
+이 저장소는 **CI가 아니라 CD 중심 GitOps repo**입니다.
+
+- Kubernetes manifest 관리
+- Argo CD `Application` / `AppProject` 관리
+- 이미지 태그 업데이트 반영
+- 환경별 overlay 관리
+- 실제 배포 반영
+
+## 현재 CD 반영 흐름
+
+1. 앱 repo(`Project-Auth-Server`, `Project-Api-Server`)에서 `feature -> main/develop` 병합 후 CI를 실행합니다.
+2. CI가 테스트 통과 뒤 이미지를 build/push하고 새 이미지 태그를 만듭니다.
+3. 앱 repo CI는 이미지 push 뒤 `repository_dispatch`로 이 저장소의 `.github/workflows/update-image-tag.yaml`을 호출해 dev overlay 태그를 갱신합니다.
+4. 최초 1회 bootstrap 또는 복구가 필요할 때는 운영자가 runbook으로 privileged token을 사용해 transit/workload Vault bootstrap을 수행합니다.
+5. 평상시에는 self-hosted runner의 `.github/workflows/vault-dev-reconcile.yaml` 이 **bootstrap readiness 확인 후** transit/workload Vault reconcile 과 Argo CD dev 정의 적용을 자동 수행합니다.
+6. Argo CD가 GitOps repo와 Application 변경을 감지합니다.
+7. `vault-transit` provider가 workload Vault의 transit auto-unseal을 지원합니다.
+8. Argo CD가 cluster에 실제 배포를 반영합니다.
+
+즉, 앱 repo는 **CI 책임**, GitOps repo는 **CD 책임**을 갖고, 이미지 태그는 앱 repo가 자기 repo manifest를 수정하는 대신 **GitOps repo를 갱신하는 방식**으로 반영합니다.
+
+## Bootstrap vs Reconcile
+
+- manual bootstrap runbook
+ - 목적: privileged token으로 transit/workload Vault를 최초 1회 bootstrap 하거나 provider bootstrap path를 복구
+ - 실행 방식: 운영자 로컬/관리자 터미널 수동 실행
+- `vault-dev-reconcile`
+ - 목적: 이미 bootstrap이 끝난 Vault를 workflow AppRole 기준으로 안전하게 reconcile
+ - 전제: `kv/dev/workload/bootstrap` 과 provider seed path가 이미 준비돼 있어야 함
+
+즉 routine CI는 bootstrap을 “대신 수행”하지 않고, bootstrap이 끝났는지 확인한 뒤 그 상태를 유지/동기화하는 역할만 맡습니다.
+
+## 현재 Secret Lifecycle
+
+- `ghcr-regcred`처럼 **image pull secret**이 필요한 항목만 `SealedSecret`을 유지합니다.
+- `auth-server`, `postgres`, `keycloak`의 **runtime secret**은 더 이상 Git이나 workflow secret에 넣지 않고 workload Vault KV(`kv/dev/...`)에 저장합니다.
+- workload KV의 seed 값과 workload Vault bootstrap token은 `vault-transit` provider Vault KV가 source of truth 역할을 합니다.
+- `vault-transit` provider와 workload Vault는 각각 클러스터 밖 runbook으로 1회 init/bootstrap 합니다.
+- 이후 dev 자동화는 self-hosted runner의 CI secret store에 저장한 **transit provider workflow AppRole 정보**만 사용하고, 실제 workload secret 값은 provider Vault에서 읽습니다.
+- root token은 bootstrap 직후 revoke하는 것을 기본값으로 두고, Kubernetes 안에는 저장하지 않습니다.
+- 애플리케이션과 플랫폼 워크로드는 Vault Agent Injector와 Kubernetes auth로 인증하고 secret file을 렌더링받습니다.
+- workload Vault는 `vault-transit` provider가 발급한 최소 권한 transit token으로 auto-unseal 합니다.
+- `auth-server`는 Injector가 공유한 Vault token file을 사용해 workload Vault Transit을 계속 호출합니다.
+
+## Auto-unseal 상태
+
+- 현재 dev 환경은 **Vault 2개 구조의 Transit auto-unseal** 을 전제로 합니다.
+- `vault-transit` provider가 `workload-vault-dev-unseal` transit key를 제공하고, workload Vault는 `vault-transit-seal` secret의 최소 권한 token으로 auto-unseal 합니다.
+- CI secret store는 provider Vault workflow AppRole 같은 최소 자동화 정보만 보관합니다.
+- 이 구조는 단일 Vault보다 운영 난이도는 높지만, root token 직접 사용 최소화와 trust boundary 분리에 더 유리합니다.
+
+## 현재 Dev 접근 경로
+
+- `auth-server`, `api-server`, `keycloak` 은 여전히 `ClusterIP` 로 유지하고, dev에서는 `traefik` `Ingress` 를 north-south 진입점으로 둡니다.
+- public host는 `auth-public.auth-dev.svc.cluster.local`, `api-public.api-dev.svc.cluster.local`, `keycloak-public.platform.svc.cluster.local` 로 분리하고, namespace 내부 DNS에서는 `ExternalName -> traefik` 경로로 같은 호스트를 해석합니다.
+- 외부 접근이 필요할 때는 운영자 노트북의 `hosts` 파일을 현재 Traefik `LoadBalancer` IP 또는 dev node IP로 매핑해 callback/redirect 와 브라우저 접근을 엽니다.
+- 예시: ` auth-public.auth-dev.svc.cluster.local api-public.api-dev.svc.cluster.local keycloak-public.platform.svc.cluster.local`
+- dev public ingress는 HTTP만 사용합니다.
+- `auth-server` 와 `api-server` 는 namespace 내부에서도 같은 public host를 HTTP로 호출합니다.
+- east-west는 `auth-dev`, `api-dev`, `platform`, `vault` namespace에 `default deny + allowlist` `NetworkPolicy` 를 적용해 필요한 흐름만 열어둡니다.
+
+## README 작성 원칙
+
+이 저장소의 README에는 `ops` 관련 내용만 기록합니다.
+
+문서 작성은 1회성 정리가 아니라 **변경 이력 누적 방식**으로 관리합니다.
+즉, 기존에 작성한 구조/문제점/개선 내용을 지우고 새로 덮어쓰지 않고, **항상 기존 내용 아래에 이어서 추가**합니다.
+다만 README 최상단의 `현재 최신 Dev 아키텍처` 섹션은 예외적으로 **항상 최신 상태로 갱신**합니다.
+
+이 README에는 아래 사이클을 반복해서 계속 누적 작성합니다.
+
+1. 처음 구조 `mermaid`
+2. 해당 구조의 문제점
+3. 변경된 후 구조 `mermaid`
+4. 이전 구조 대비 변경된 점
+5. 변경으로 해결된 내용
+
+## 기록 규칙
+
+- 이전 사이클은 삭제하거나 수정해서 덮어쓰지 않습니다.
+- README 최상단의 `현재 최신 Dev 아키텍처`는 최신 상태만 유지하고, 예전 아키텍처는 cycle로 추적합니다.
+- 새로운 `ops` 변경이 생기면 README의 가장 아래에 새 사이클을 추가합니다.
+- 아키텍처 변경이 생기면 먼저 최상단 `현재 최신 Dev 아키텍처`를 갱신하고, 같은 변경을 새 cycle에 기록합니다.
+- 각 사이클은 당시의 구조, 문제, 개선 결과가 모두 보이도록 독립적으로 작성합니다.
+- 구조 설명은 가능하면 `mermaid` 다이어그램으로 남깁니다.
+- 변경된 점은 반드시 **이전 구조와 비교**해서 작성합니다.
+- 해결 내용은 어떤 문제가 어떻게 해소되었는지 명확하게 작성합니다.
+- 런타임 장애나 수동 운영 이슈를 해결했으면, README 하단 cycle에 **재현 명령, 핵심 관찰값, 판단 근거, 수정 내용, 검증 명령** 을 함께 남깁니다.
+- 트러블슈팅 명령은 가능하면 실제로 사용한 형태 그대로 남기고, 왜 그 명령을 쳤는지 한 줄로 설명합니다.
+- secret, token, kubeconfig 본문처럼 민감한 값은 절대 그대로 기록하지 않고, 값의 존재 여부나 길이만 요약합니다.
+
+## 작성 템플릿
+
+아래 형식을 반복해서 README 하단에 계속 추가합니다.
+
+````md
+## Cycle N
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ A[example]
+```
+
+### 2. 문제점
+- 문제 1
+- 문제 2
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ A[changed-example]
+```
+
+### 4. 이전 구조 대비 변경점
+- 변경점 1
+- 변경점 2
+
+### 5. 해결된 내용
+- 해결 1
+- 해결 2
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령
+- 핵심 관찰값
+- 판단 근거
+- 수정 또는 조치
+- 검증 명령
+````
+
+## 트러블슈팅 메모 작성 예시
+
+- 재현/확인 명령: `kubectl -n argocd describe application vault-transit-dev`
+- 핵심 관찰값: `authentication required: Repository not found`
+- 판단 근거: Argo CD app spec 자체는 존재하지만 repo-server가 source repo를 읽지 못해 manifest generation 전에 실패한다고 봤습니다.
+- 수정 또는 조치: `argocd` namespace에 `repo-creds` secret을 선언형으로 적용했습니다.
+- 검증 명령: `kubectl -n argocd annotate application vault-transit-dev argocd.argoproj.io/refresh=hard --overwrite`
+
+## Cycle 1
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph AUTH[Project-Auth-Server]
+ A1[argocd/*.yaml]
+ A2[k8s/dev]
+ A3[k8s/platform-dev]
+ end
+
+ subgraph API[Project-Api-Server]
+ B1[argocd/*.yaml]
+ B2[k8s/dev]
+ end
+
+ subgraph GITOPS_BEFORE[Project-Auth-GitOps]
+ C1[README only]
+ end
+```
+
+### 2. 문제점
+- 운영 선언이 `Project-Auth-Server`와 `Project-Api-Server`에 분산되어 있어서 GitOps 저장소가 실제 단일 운영 기준점이 아니었습니다.
+- `auth-server`는 `k8s/dev`와 `k8s/platform-dev`가 분리돼 있었지만, 현재 GitOps 저장소 기준의 공통 `base`와 환경별 `overlay` 구조가 없었습니다.
+- Argo CD `Application`의 source repo가 각 서비스 repo를 가리키고 있어, 운영 경로를 한 저장소에서 일관되게 추적하기 어려웠습니다.
+- 두 서비스 모두 `prod`를 수용할 고정 overlay 진입점이 없어 이후 환경 확장 시 구조가 다시 흔들릴 수 있었습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph GITOPS_AFTER[Project-Auth-GitOps]
+ subgraph APPS[apps]
+ subgraph AUTH_APP[auth-server]
+ D1[base]
+ D2[overlays/dev]
+ D3[overlays/dev/platform]
+ D4[overlays/prod]
+ end
+
+ subgraph API_APP[api-server]
+ E1[base]
+ E2[overlays/dev]
+ E3[overlays/prod]
+ end
+ end
+
+ subgraph ARGO[argocd]
+ F1[projects/*.yaml]
+ F2[applications/*.yaml]
+ end
+ end
+```
+
+### 4. 이전 구조 대비 변경점
+- `auth-server`와 `api-server`의 Kubernetes 운영 매니페스트를 현재 GitOps repo의 `apps/` 아래로 이관했습니다.
+- 앱 공통 리소스는 `base`로 분리하고, namespace/configmap/sealed secret/image tag 같은 환경 값은 `overlays/dev`로 분리했습니다.
+- `auth-server`의 `platform-dev` 리소스는 `apps/auth-server/overlays/dev/platform`으로 옮겨 기존 dev platform 운영 구성을 유지했습니다.
+- Argo CD `AppProject`와 `Application`도 현재 GitOps repo 기준으로 재배치하고, `repoURL`과 `path`를 새 구조에 맞게 변경했습니다.
+- 원본 repo에 `prod` 운영 매니페스트는 없었기 때문에, 이번 변경에서는 `overlays/prod`에 namespace와 kustomization 골격만 먼저 추가했습니다.
+
+### 5. 해결된 내용
+- 이제 `Project-Auth-GitOps`가 `auth-server`, `api-server`, `platform-dev`의 운영 선언을 모으는 단일 저장소 역할을 하게 되었습니다.
+- 서비스마다 서로 다른 운영 경로를 읽지 않아도 되어, 변경 리뷰와 Argo CD 추적 기준이 단순해졌습니다.
+- 이후 환경이 늘어나더라도 `apps//base`와 `apps//overlays/` 패턴으로 같은 방식의 확장이 가능해졌습니다.
+- `auth-dev` 프로젝트에 `SealedSecret` 허용 리소스를 추가해, 이관된 sealed secret 리소스가 Argo CD 정책과 맞지 않던 문제도 함께 정리했습니다.
+
+## Cycle 2
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph ARGOCD_BEFORE[argocd]
+ A1[applications/auth-server-dev.yaml]
+ A2[applications/api-server-dev.yaml]
+ A3[applications/platform-dev.yaml]
+ A4[applications/sealed-secrets-dev.yaml]
+ B1[projects/auth-dev-project.yaml]
+ B2[projects/api-dev-project.yaml]
+ B3[projects/infra-dev-project.yaml]
+ end
+```
+
+### 2. 문제점
+- `applications`와 `projects`가 파일 단위로 평평하게 놓여 있어서 `dev/prod` 경계와 `apps/infra` 경계가 디렉터리 구조에 드러나지 않았습니다.
+- 앱용 프로젝트가 `auth-dev`, `api-dev`로 분산돼 있어, 같은 성격의 애플리케이션을 한 번에 파악하기 어려웠습니다.
+- `prod`용 Argo CD 진입점이 구조상 준비돼 있지 않아 환경 확장 시 다시 디렉터리 재정리가 필요했습니다.
+- 파일 수가 늘어날수록 어떤 선언이 서비스용인지 인프라용인지 찾는 비용이 계속 커질 구조였습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph ARGOCD_AFTER[argocd]
+ subgraph APPS[applications]
+ subgraph DEV_APPS[dev]
+ C1[apps/auth-server.yaml]
+ C2[apps/api-server.yaml]
+ C3[infra/platform.yaml]
+ C4[infra/sealed-secrets.yaml]
+ end
+ subgraph PROD_APPS[prod]
+ C5[apps/]
+ C6[infra/]
+ end
+ end
+
+ subgraph PROJECTS[projects]
+ subgraph DEV_PROJECTS[dev]
+ D1[apps-project.yaml]
+ D2[infra-project.yaml]
+ end
+ subgraph PROD_PROJECTS[prod]
+ D3[apps-project.yaml]
+ D4[infra-project.yaml]
+ end
+ end
+ end
+```
+
+### 4. 이전 구조 대비 변경점
+- Argo CD 선언을 `argocd/applications//`와 `argocd/projects/` 구조로 재배치했습니다.
+- `auth-server`와 `api-server`는 `dev/apps` 아래로, `platform`과 `sealed-secrets`는 `dev/infra` 아래로 나눠 목적별 경계를 디렉터리에서 바로 보이게 했습니다.
+- 기존 `auth-dev`와 `api-dev` AppProject는 `apps-dev` 하나로 통합하고, `platform`과 `sealed-secrets`는 `infra-dev` 프로젝트로 정리했습니다.
+- `prod`는 아직 실제 Application이 없지만, `applications/prod`와 `projects/prod` 골격을 미리 만들어 이후 추가 위치를 고정했습니다.
+- `argocd/README.md`를 추가해 이 구조 규칙을 디렉터리 안에서도 바로 확인할 수 있게 했습니다.
+
+### 5. 해결된 내용
+- 이제 Argo CD 선언만 보더라도 환경별 구분과 성격별 구분이 동시에 드러나서 탐색 비용이 줄었습니다.
+- 서비스 애플리케이션과 공용 인프라가 각자 어떤 AppProject를 쓰는지 일관되게 정리되어 관리 포인트가 단순해졌습니다.
+- `prod` 확장 시 새 파일을 어디에 둬야 하는지 미리 정해져 있어, 다음 변경에서도 구조를 다시 흔들 필요가 없어졌습니다.
+- `argocd` 자체도 README 기반의 누적 관리 대상이 되면서, 구조 변경 이유를 README와 디렉터리 문서에서 함께 추적할 수 있게 됐습니다.
+
+## Cycle 3
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ A[README 소개]
+ B[README 작성 원칙]
+ C[Cycle 1]
+ D[Cycle 2]
+
+ A --> B --> C --> D
+```
+
+### 2. 문제점
+- 현재 운영 중인 dev 아키텍처를 README 최상단에서 바로 볼 수 있는 기준 그림이 없었습니다.
+- 변경 이력은 누적되고 있었지만, 최신 구조를 한 번에 확인하려면 여러 cycle을 직접 읽어야 했습니다.
+- README의 누적 기록 규칙만 있고, `최신 아키텍처는 어디를 기준으로 볼지`에 대한 별도 원칙이 없었습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ A[README 소개]
+ B[현재 최신 Dev 아키텍처]
+ C[README 작성 원칙]
+ D[Cycle 1]
+ E[Cycle 2]
+ F[Cycle 3]
+
+ A --> B --> C --> D --> E --> F
+```
+
+### 4. 이전 구조 대비 변경점
+- README 최상단에 현재 기준의 **최신 dev 아키텍처**를 `mermaid`로 추가했습니다.
+- 최상단 아키텍처는 항상 최신 상태로 갱신하고, 이전 구조 변화는 cycle로 누적 기록한다는 규칙을 명시했습니다.
+- 현재 단계에서는 요청하신 대로 `prod`는 제외하고 `dev` 운영 구조만 아키텍처에 반영했습니다.
+- 아키텍처 그림 안에는 Argo CD project/application, GitOps manifest 경로, dev cluster 주요 namespace와 런타임 의존 관계를 함께 드러내도록 정리했습니다.
+
+### 5. 해결된 내용
+- 이제 README를 열면 가장 먼저 현재 dev 운영 구조를 확인할 수 있어 최신 상태 파악이 훨씬 빨라졌습니다.
+- 최신 구조와 변경 이력을 분리해, 상단은 현재 기준점으로 쓰고 하단 cycle은 히스토리로 쓰는 역할이 명확해졌습니다.
+- 이후 dev 아키텍처가 바뀌더라도 어떤 내용을 갱신하고 어떤 내용을 누적해야 하는지 README 규칙만 보고 바로 따라갈 수 있게 됐습니다.
+
+## Cycle 4
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph APP_REPOS[Application Repos]
+ A1[CI builds image]
+ end
+
+ subgraph GITOPS_BEFORE[Project-Auth-GitOps]
+ B1[apps/*/overlays/*/kustomization.yaml]
+ B2[argocd/dev/*]
+ end
+
+ APP_REPOS -. image tag info .-> B1
+```
+
+### 2. 문제점
+- GitOps repo가 CD 중심 저장소라는 역할은 정리됐지만, 이미지 태그를 **어떤 진입점으로 갱신할지**가 이 저장소 안에 아직 명시돼 있지 않았습니다.
+- 앱 repo가 이미지를 push한 뒤 GitOps repo를 어떻게 업데이트해야 하는지 표준 스크립트나 workflow가 없어, 저장소마다 방식이 달라질 수 있었습니다.
+- README에도 이 저장소가 `CI`가 아니라 `CD`를 담당한다는 운영 원칙과 실제 반영 흐름이 구조적으로 정리돼 있지 않았습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph APP_REPOS[Application Repos]
+ A1[Project-Auth-Server CI]
+ A2[Project-Api-Server CI]
+ end
+
+ subgraph GITOPS_AFTER[Project-Auth-GitOps]
+ B1[.github/workflows/update-image-tag.yaml]
+ B2[scripts/update-image-tag.sh]
+ B3[apps/*/overlays/*/kustomization.yaml]
+ B4[argocd/dev/*]
+ end
+
+ A1 -->|new image tag| B1
+ A2 -->|new image tag| B1
+ B1 --> B2
+ B2 --> B3
+ B4 --> B3
+```
+
+### 4. 이전 구조 대비 변경점
+- GitOps repo에 이미지 태그 갱신용 스크립트 `scripts/update-image-tag.sh`를 추가했습니다.
+- GitOps repo 내부에서 직접 태그 갱신 commit/push를 수행할 수 있도록 `.github/workflows/update-image-tag.yaml` workflow를 추가했습니다.
+- workflow는 `workflow_dispatch`와 `repository_dispatch` 둘 다 받을 수 있게 구성해, 수동 실행과 앱 repo CI 연동 둘 다 가능하도록 했습니다.
+- README 최상단 dev 아키텍처에도 앱 repo CI에서 GitOps repo로 태그가 반영되는 흐름을 함께 반영했습니다.
+- README에 이 저장소의 역할과 현재 CD 반영 흐름을 별도 섹션으로 정리했습니다.
+
+### 5. 해결된 내용
+- 이제 이 저장소 안에 `이미지 태그 업데이트`를 수행하는 공식 진입점이 생겨, CD 반영 방식이 문서와 파일 기준으로 일치하게 되었습니다.
+- 앱 repo는 자기 저장소의 manifest를 다시 수정하지 않고, GitOps repo를 갱신하는 방식으로 역할이 명확히 분리되었습니다.
+- 이후 앱 repo CI는 새 이미지 태그만 전달하면 되고, 실제 배포 반영은 GitOps repo와 Argo CD 흐름 안에서 일어나도록 정리되었습니다.
+
+## Cycle 5
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph APPS_BEFORE[apps]
+ A1[auth-server/base]
+ A2[auth-server/overlays/dev]
+ A3[auth-server/overlays/dev/platform]
+ A4[auth-server/overlays/prod]
+ B1[api-server/base]
+ B2[api-server/overlays/dev]
+ B3[api-server/overlays/prod]
+ end
+```
+
+### 2. 문제점
+- `platform`이 `apps/auth-server` 하위에 있어, 앱 배포와 공용 인프라 배포의 책임 경계가 디렉터리 구조상 섞여 있었습니다.
+- `postgres`, `keycloak`, `vault`는 `auth-server`의 일부라기보다 공용 infra인데도 앱 overlay에 포함돼 있어 탐색과 확장이 불편했습니다.
+- `dev`와 `prod`를 분리할 때도 `platform`이 앱 트리 안에 있으면 인프라 확장 경로가 일관되지 않았습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph APPS_AFTER[apps]
+ A1[auth-server/base]
+ A2[auth-server/overlays/dev]
+ A3[auth-server/overlays/prod]
+ B1[api-server/base]
+ B2[api-server/overlays/dev]
+ B3[api-server/overlays/prod]
+ end
+
+ subgraph INFRA_AFTER[infra]
+ C1[platform/base]
+ C2[platform/overlays/dev]
+ C3[platform/overlays/prod]
+ end
+```
+
+### 4. 이전 구조 대비 변경점
+- `apps/auth-server/overlays/dev/platform`에 있던 리소스를 `infra/platform/base`와 `infra/platform/overlays/dev`로 분리했습니다.
+- `postgres`, `keycloak`, `vault` 워크로드와 공통 생성 파일은 `infra/platform/base`로 옮기고, namespace/configmap/sealed secret은 `infra/platform/overlays/dev`로 분리했습니다.
+- `infra/platform/overlays/prod`도 함께 추가해 `platform-prod` namespace와 prod용 config skeleton을 둘 수 있게 했습니다.
+- Argo CD `platform-dev` Application의 source path를 새 infra 경로로 변경했습니다.
+- README 최상단 최신 dev 아키텍처도 `apps`와 `infra`가 분리된 현재 구조 기준으로 갱신했습니다.
+
+### 5. 해결된 내용
+- 이제 `platform`은 앱 하위 부속이 아니라 독립된 infra 영역으로 보이기 때문에 구조 해석이 훨씬 자연스러워졌습니다.
+- 앱 배포 경로와 인프라 배포 경로가 분리되어, 이후 `infra` 확장이나 세분화로 이어가기가 쉬워졌습니다.
+- `dev`뿐 아니라 `prod`도 같은 `infra/platform/base -> overlays/` 패턴으로 관리할 준비가 갖춰졌습니다.
+
+## Cycle 6
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[GitOps secret lifecycle]
+ A1[auth-server SealedSecret]
+ A2[platform SealedSecret]
+ A3[app/platform pod env from Secret]
+ A4[sealed-secrets-controller]
+ end
+
+ A1 --> A3
+ A2 --> A3
+ A4 --> A1
+ A4 --> A2
+```
+
+### 2. 문제점
+- `SealedSecret`으로 평문을 Git에 직접 넣지는 않았지만, secret source 자체가 여전히 GitOps 저장소 안의 정적 파일이었습니다.
+- secret rotation과 변경 이력이 결국 Git commit 중심이 되어, 운영형 secret lifecycle이라고 보기 어려웠습니다.
+- `auth-server`는 Vault Transit을 사용하면서도 접근 토큰을 정적 Kubernetes Secret으로 주입받고 있어 Kubernetes auth 기반 접근으로 전환되지 못했습니다.
+- `postgres`, `keycloak`도 `platform-secret` 하나에 묶인 채 정적 secret에 의존하고 있어, 역할별 최소 권한 분리가 어려웠습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[GitOps secret lifecycle]
+ B1["vault-bootstrap Secret
manual / out-of-git"]
+ B2[Vault KV kv/dev/platform/postgres/*]
+ B3[Vault KV kv/dev/platform/keycloak/*]
+ B4[Vault Kubernetes auth roles per workload]
+ B5[Vault Agent Injector]
+ B6[auth-server / auth-db-migration / postgres / keycloak / keycloak-client-sync]
+ B7[ghcr-regcred SealedSecret only]
+ end
+
+ B1 --> B2
+ B1 --> B3
+ B1 --> B4
+ B1 --> B5
+ B5 --> B6
+ B6 --> B4
+ B4 --> B2
+ B4 --> B3
+```
+
+### 4. 이전 구조 대비 변경점
+- `auth-server-secret.sealedsecret.yaml`과 `platform-secret.sealedsecret.yaml`을 제거하고, runtime secret source를 Vault KV로 전환했습니다.
+- HashiCorp 공식 Helm chart를 사용하는 `vault-agent-injector` Argo CD Application을 추가했습니다.
+- `auth-server`, `auth-db-migration`, `postgres`, `keycloak`, `keycloak-client-sync`에 Vault Agent Injector annotation을 적용하고 Kubernetes auth role 기반으로 secret을 주입받도록 변경했습니다.
+- runtime KV path를 `platform/postgres/superuser`, `platform/postgres/auth-server`, `platform/postgres/keycloak`, `platform/keycloak/bootstrap-admin`, `platform/keycloak/client-auth-server`처럼 목적/소유권 기준으로 세분화했습니다.
+- `auth-db-migration`과 `keycloak-client-sync`를 전용 ServiceAccount와 Vault role로 분리해 app/runtime 권한을 job과 분리했습니다.
+- base/prod 매니페스트의 Kubernetes Secret 계약도 `postgres-superuser-credentials`, `postgres-auth-server-credentials`, `postgres-keycloak-credentials`, `keycloak-bootstrap-admin`, `keycloak-client-auth-server`처럼 목적별 이름으로 분해했습니다.
+- `vault-bootstrap`은 Git에 넣지 않는 수동 bootstrap secret으로 분리하고, Vault server는 부팅 시 Kubernetes auth/policy/role을 자동 구성하도록 바꿨습니다.
+- 현재 범위는 dev 운영 환경이므로 Vault role/policy 이름도 `*-dev` 기준으로만 구성했습니다.
+- `ghcr-regcred`는 image pull secret 특성상 Injector로 대체할 수 없어서 SealedSecret으로 유지했습니다.
+
+### 5. 해결된 내용
+- 이제 앱/플랫폼 runtime secret의 기준점이 Git의 암호화 파일이 아니라 Vault가 되어, secret lifecycle이 Git commit 중심 구조에서 벗어났습니다.
+- Vault Kubernetes auth와 role 분리를 통해 `auth-server`, `auth-db-migration`, `postgres`, `keycloak`, `keycloak-client-sync`가 각자 필요한 범위만 읽도록 최소 권한 구조를 만들었습니다.
+- 정적 Kubernetes Secret 계약도 blob 두세 개 대신 목적별 credential 단위로 나뉘어, rotation과 접근 제어 범위를 더 좁힐 수 있게 됐습니다.
+- `auth-server`는 Injector가 제공한 Vault token file을 통해 Vault Transit을 계속 사용할 수 있게 되어, 정적 Vault token SealedSecret 없이도 동작할 기반이 생겼습니다.
+- 현재 구조에서 Git에 남는 비밀 항목은 bootstrap과 image pull 같은 예외 케이스로 좁혀졌고, 운영 secret 흐름과 예외 secret 흐름을 구분할 수 있게 됐습니다.
+
+## Cycle 7
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Vault bootstrap in cluster]
+ A1[vault-bootstrap Secret]
+ A2[vault postStart bootstrap script]
+ A3[policies mounted by ConfigMap]
+ A4[Vault dev mode]
+ end
+
+ A1 --> A2
+ A3 --> A2
+ A2 --> A4
+```
+
+### 2. 문제점
+- root token이 Kubernetes Secret 형태로 클러스터 안에 남아 있어, 운영자가 원한 `root token out of cluster` 조건을 만족하지 못했습니다.
+- Vault policy/role bootstrap이 pod lifecycle에 묶여 있어, 초기화 작업이 GitOps 런타임과 섞여 있었습니다.
+- Vault server가 `-dev` 모드로 실행되고 있어, 수동 init/unseal과 root token revoke 기반 운영 절차를 적용할 수 없었습니다.
+- bootstrap 절차와 KV 입력 절차가 overlay 파일 안에 섞여 있어, 실제 운영 runbook과 배포 manifest의 경계가 불분명했습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Vault bootstrap outside cluster]
+ B1["Vault server
raft + PVC"]
+ B2[scripts/vault/dev/bootstrap-runbook.sh]
+ B3[runbooks/vault/dev/policies/*.hcl]
+ B4[runbooks and scripts]
+ B5["root token
operator local only"]
+ B6[platform-admin-dev token]
+ end
+
+ B5 --> B2
+ B3 --> B2
+ B2 --> B1
+ B2 --> B6
+ B6 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- Vault bootstrap용 `vault-bootstrap` Kubernetes Secret과 in-cluster `postStart` bootstrap 흐름을 제거했습니다.
+- Vault server는 `-dev` 모드 대신 config file 기반 단일-node raft 저장소와 PVC를 사용하도록 변경했습니다.
+- Vault policy와 bootstrap 로직을 GitOps overlay 밖의 `runbooks/vault/dev/`로 이동해, 운영자가 클러스터 밖에서 직접 실행하는 구조로 바꿨습니다.
+- `bootstrap-runbook.sh`는 root token으로 1회 bootstrap을 수행한 뒤 `platform-admin-dev` orphan token을 만들고 root token revoke까지 처리하도록 바꿨습니다.
+- KV 값 입력 예시도 overlay에서 제거하고 runbook 디렉터리로 이동시켜, manifest와 운영 절차를 분리했습니다.
+
+### 5. 해결된 내용
+- 이제 root token이 Kubernetes 안에 저장되지 않고, 초기 bootstrap에만 클러스터 밖에서 사용되도록 구조가 정리되었습니다.
+- Vault bootstrap이 pod 기동 과정과 분리되어, GitOps manifest는 런타임 배포에만 집중하고 초기 운영 절차는 runbook으로 분리되었습니다.
+- Vault 운영 흐름이 `init/unseal -> bootstrap -> admin token 발급 -> root revoke` 순서로 명확해져 dev 운영 환경 기준에 더 가까워졌습니다.
+- 이후에는 `platform-admin-dev` 같은 제한된 운영 토큰으로 KV 갱신과 정책 보조 작업을 할 수 있어, root token을 상시 들고 있을 필요가 없어졌습니다.
+
+## Cycle 8
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Dev Vault automation]
+ A1[Operator runbook only]
+ A2[manual bootstrap script]
+ A3[manual seed input]
+ A4[Argo CD apps applied separately]
+ end
+
+ A1 --> A2 --> A3 --> A4
+```
+
+### 2. 문제점
+- runbook만으로는 dev Vault KV 입력과 auth/policy/role reconcile이 계속 운영자 수동 절차에 묶여 있었습니다.
+- GitOps repo 안에 자동 파이프라인이 없어, KV 준비와 Argo CD dev Application 적용 순서를 일관되게 맞추기 어려웠습니다.
+- 현재 단일 Vault 구조에서는 진짜 Transit auto-unseal을 바로 적용할 수 없는데, 문서상으로는 그 경계가 충분히 드러나지 않았습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Dev Vault automation]
+ B1[self-hosted runner]
+ B2[vault-dev-reconcile workflow]
+ B3[ensure-unsealed.sh]
+ B4[reconcile.sh]
+ B5[populate-kv.sh]
+ B6[apply-argocd-dev-*.sh]
+ B7[runbooks only for first bootstrap / recovery]
+ end
+
+ B1 --> B2
+ B2 --> B3
+ B3 --> B4
+ B4 --> B5
+ B5 --> B6
+ B7 -. fallback .-> B2
+```
+
+### 4. 이전 구조 대비 변경점
+- self-hosted runner 전용 workflow `.github/workflows/vault-dev-reconcile.yaml` 을 추가했습니다.
+- workflow는 당시 CI secret store에 저장된 비밀값을 사용해 unseal/reconcile/populate/apply를 자동 수행했습니다.
+- 기존 runbook의 정책과 절차를 재사용할 수 있도록 `scripts/vault/dev/` 아래에 automation용 스크립트를 분리했습니다.
+- README와 runbook에 자동화에 필요한 CI secret 목록과 현재 자동화 범위를 명시했습니다.
+- Transit auto-unseal은 적용했다고 표기하지 않고, 별도 unseal provider Vault/HSM/KMS가 필요한 후속 아키텍처 작업임을 분명히 남겼습니다.
+
+### 5. 해결된 내용
+- 이제 dev 환경에서는 Vault KV 준비와 Argo CD dev 정의 적용이 self-hosted runner workflow로 자동 수행될 수 있게 되었습니다.
+- 운영자는 최초 bootstrap 또는 복구 시에만 runbook을 보고 개입하면 되고, 평소 dev reconcile은 CI secret store 기반 자동화로 넘길 수 있습니다.
+- 현재 구조에서 자동화된 부분과 아직 별도 아키텍처가 필요한 부분(Transit auto-unseal)이 명확히 분리되어, 다음 변경 방향을 혼동하지 않게 됐습니다.
+
+## Cycle 9
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Single Vault dev]
+ A1[workload Vault only]
+ A2[CI secret store based unseal]
+ A3[KV + Transit JWT in same Vault]
+ A4[no dedicated unseal provider]
+ end
+
+ A2 --> A1
+ A3 --> A1
+```
+
+### 2. 문제점
+- 단일 Vault 구조에서는 runtime secret은 Vault로 옮길 수 있어도, Vault 서버 자체의 unseal trust boundary는 여전히 CI secret store에 크게 의존했습니다.
+- `root token 직접 사용 금지`, `bootstrap 최소화`, `trust boundary를 Vault 쪽으로 이동` 같은 현업형 dev 운영 방향을 만족시키려면 별도 unseal provider가 필요했습니다.
+- README와 자동화 흐름도 아직 단일 Vault 기준 설명이 남아 있어, 실제 운영 구조와 설명이 어긋날 위험이 있었습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Two Vault dev]
+ B1[vault-transit provider]
+ B2[workload Vault]
+ B3[Vault Agent Injector]
+ B4[CI runner automation]
+ B5[runbooks for first bootstrap]
+ end
+
+ B1 -->|transit auto-unseal| B2
+ B2 -->|KV + Kubernetes auth + Transit JWT| B3
+ B4 --> B1
+ B4 --> B2
+ B5 --> B1
+ B5 --> B2
+```
+
+### 4. 이전 구조 대비 변경점
+- `infra/vault-transit/overlays/dev` 와 `argocd/applications/dev/infra/vault-transit.yaml` 을 추가해 unseal provider Vault를 별도 infra로 분리했습니다.
+- workload Vault config에 transit seal stanza를 추가하고, `vault-transit-seal` 최소 권한 token으로 auto-unseal 하도록 변경했습니다.
+- self-hosted runner workflow는 provider bootstrap -> workload reconcile -> KV populate -> app apply 순서로 재구성했습니다.
+- `scripts/vault-transit/dev/` 와 `runbooks/vault-transit/dev/` 를 추가해 provider Vault 전용 bootstrap/policy 경로를 분리했습니다.
+- README 최상단 최신 아키텍처, secret lifecycle, auto-unseal 설명을 2-Vault 구조 기준으로 갱신했습니다.
+
+### 5. 해결된 내용
+- 이제 workload Vault의 unseal trust boundary가 단순 CI secret store 의존에서 `vault-transit` provider Vault 기반 구조로 한 단계 올라갔습니다.
+- runtime secret, JWT transit signing, workload Vault 운영, unseal provider 역할이 분리되어 현업형 dev 운영 방향에 더 가까워졌습니다.
+- 단일 Vault보다 운영 난이도는 높아졌지만, root token 직접 사용 최소화와 운영 신뢰 경계 분리 측면에서는 더 나은 dev 구조를 갖추게 됐습니다.
+
+## Cycle 10
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[2-Vault but workflow secrets remain]
+ A1[CI secret store]
+ A2[provider Vault]
+ A3[workload Vault]
+ end
+
+ A1 -->|app secret values + bootstrap tokens| A3
+ A1 -->|provider token| A2
+```
+
+### 2. 문제점
+- Vault를 2개로 나눴어도, workflow에 앱 비밀값과 workload Vault bootstrap token이 남아 있으면 여전히 GitOps CI가 secret source처럼 보일 수 있었습니다.
+- 다른 앱 repo workflow에는 없는 민감값이 이 repo workflow에만 남아 있어, 운영 구조 일관성이 떨어졌습니다.
+- 목표였던 `비밀 배포의 신뢰 경계가 SealedSecret/CI가 아니라 Vault에 있어야 한다`는 방향이 완전히 충족되지 않았습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Provider Vault as source of truth]
+ B1[CI secret store]
+ B2[vault-transit provider Vault]
+ B3[workload Vault]
+ end
+
+ B1 -->|provider unseal/bootstrap only| B2
+ B2 -->|seed values + workload bootstrap token| B3
+```
+
+### 4. 이전 구조 대비 변경점
+- `vault-dev-reconcile` workflow에서 앱 비밀값과 workload Vault bootstrap token을 제거했습니다.
+- `scripts/vault/dev/populate-kv.sh` 와 `scripts/vault/dev/reconcile.sh` 는 provider Vault KV에서 값을 읽어 workload Vault에 반영하도록 변경했습니다.
+- provider Vault에 workload seed 값을 넣는 실행 스크립트 `scripts/vault-transit/dev/populate-workload-seeds.sh` 와 참고용 `runbooks/vault-transit/dev/populate-workload-seeds.example.sh` 를 분리했습니다.
+- README와 runbook에서 CI secret store에는 provider Vault 접근용 최소값만 남고, 실제 workload secret source는 provider Vault라는 점을 명시했습니다.
+
+### 5. 해결된 내용
+- 이제 workflow는 orchestration만 담당하고, 실제 앱/플랫폼 secret 값은 provider Vault에서 workload Vault로 흘러가는 구조가 되었습니다.
+- CI secret store에 남는 민감값 범위가 줄어들어, `Vault 안에 secret source of truth를 두자`는 목표에 더 가까워졌습니다.
+- 다른 앱 repo 기준으로 봐도 이 저장소만 workflow에 앱 비밀값을 직접 쥐고 있던 불균형이 해소되었습니다.
+
+## Cycle 11
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Infra layout]
+ A1[infra/platform = postgres + keycloak + workload vault]
+ A2[infra/vault-transit = transit provider]
+ end
+```
+
+### 2. 문제점
+- `vault-transit` 만 따로 빠져 있고 workload Vault는 `platform` 안에 남아 있어, infra 경계가 namespace/역할 기준으로 일관되지 않았습니다.
+- `platform` 이라는 이름만 보면 postgres/keycloak 묶음으로 이해되는데, 여기에 workload Vault까지 들어 있어 해석이 애매했습니다.
+- 구조가 애매하면 문서와 운영 흐름을 볼 때도 `platform` 과 `vault` 의 책임이 섞여 보이기 쉬웠습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Infra layout]
+ A1[infra/platform = postgres + keycloak]
+ A2[infra/vault = workload vault]
+ A3[infra/vault-transit = transit provider]
+ end
+```
+
+### 4. 이전 구조 대비 변경점
+- workload Vault 리소스를 `infra/platform` 에서 분리해 `infra/vault/base|overlays` 로 이동했습니다.
+- Argo CD infra app도 `vault-transit`, `vault`, `platform` 3개로 역할이 드러나도록 나눴습니다.
+- workload Vault service 주소를 `vault.vault.svc.cluster.local` 기준으로 정리하고, 관련 config/script/document를 모두 새 namespace 기준으로 갱신했습니다.
+- `platform` 은 이제 postgres/keycloak 영역만 담당하고, `vault` 는 workload secret/runtime auth/transit JWT를 담당하도록 구조를 고정했습니다.
+
+### 5. 해결된 내용
+- 이제 infra 디렉터리와 namespace가 역할 기준으로 일치해, 구조 해석과 운영 설명이 훨씬 자연스러워졌습니다.
+- `platform`, `vault`, `vault-transit` 이 각각 무엇을 위한 스택인지 경로만 봐도 바로 드러납니다.
+- 이후 더 깊게 파고들 때도 어떤 변경이 어느 스택의 책임인지 구분하기 쉬워졌습니다.
+
+## Cycle 12
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Internal-only dev app network]
+ A1[auth-server ClusterIP]
+ A2[api-server ClusterIP]
+ A3[keycloak ClusterIP]
+ A4[cluster-local URLs only]
+ end
+
+ A4 --> A1
+ A4 --> A2
+ A4 --> A3
+```
+
+### 2. 문제점
+- 앱 접근 경로가 사실상 cluster 내부 DNS에만 묶여 있어, 브라우저 callback/redirect 를 거는 dev 플로우를 노트북 관점에서 설명하기 어려웠습니다.
+- `auth-server`, `api-server`, `keycloak` 이 모두 포트가 드러난 내부 URL에 결합돼 있어, north-south 진입점을 붙이기 전제도 약했습니다.
+- 리뷰에서 지적한 `연결만 되면 되는 구조` 에서 최소한의 ingress 경계와 외부 접근 경로가 부족했습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Ingress-backed dev app network]
+ B1[traefik ingress]
+ B2[auth-server ClusterIP:80]
+ B3[api-server ClusterIP:80]
+ B4[keycloak ClusterIP:80]
+ B5[local hosts mapping on operator laptop]
+ end
+
+ B5 --> B1
+ B1 --> B2
+ B1 --> B3
+ B1 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- `apps/auth-server/overlays/dev`, `apps/api-server/overlays/dev`, `infra/platform/overlays/dev` 에 `Ingress` 를 추가해 north-south 진입점을 만들었습니다.
+- `auth-server`, `api-server`, `keycloak` 서비스 포트를 각각 `80 -> targetPort` 형태로 정리해 내부/외부에서 같은 호스트 표기를 쓰기 쉽게 맞췄습니다.
+- dev 설정의 issuer/base URL 에서 `:8080/:8081/:8082` 포트 결합을 제거하고, ingress 가능한 host 기준으로 정리했습니다.
+- 노트북 dev 한계를 감안해 별도 split-horizon DNS 대신 현재 서비스 FQDN 을 ingress host로 재사용하고, 운영자 로컬 `hosts` 매핑으로 외부 접근을 여는 절충안을 택했습니다.
+
+### 5. 해결된 내용
+- 이제 dev도 최소한 `ClusterIP 뒤 Ingress` 구조가 되어, callback/redirect 가 필요한 앱 접근 경로를 north-south 관점에서 설명할 수 있게 됐습니다.
+- 앱 설정이 내부 포트에 덜 결합돼, 이후 별도 dev 사설 도메인을 붙일 때도 변경 폭이 줄어듭니다.
+- namespace 간 통신 제한은 이후 사이클에서 더 세분화할 수 있도록 기반만 먼저 깔았습니다.
+
+## Cycle 13
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[HTTP ingress only]
+ A1[auth/api/keycloak ingress]
+ A2[internal service DNS]
+ A3[no namespace traffic policy]
+ end
+
+ A1 --> A2
+ A3 --> A2
+```
+
+### 2. 문제점
+- public host와 internal host가 완전히 분리되지 않아, dev 기준 public 경로를 일관되게 설명하기 어려웠습니다.
+- east-west 제한이 전혀 없으면, namespace를 나눠도 실제 통신 경계가 거의 없는 상태와 다르지 않았습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[HTTP + east-west guardrail]
+ B1[public ExternalName host]
+ B2[traefik web ingress]
+ B3[namespace NetworkPolicy allowlist]
+ end
+
+ B1 --> B2
+ B3 --> B2
+```
+
+### 4. 이전 구조 대비 변경점
+- `auth-public`, `api-public`, `keycloak-public` `ExternalName` 서비스를 추가해 cluster 내부에서도 public host를 `traefik` 경유로 해석할 수 있게 했습니다.
+- `Ingress` 는 `web` entrypoint 기반의 HTTP 경로로 단순화했습니다.
+- `keycloak` 은 public hostname과 proxy header를 인지하도록 패치했습니다.
+- `auth-dev`, `api-dev`, `platform` 에는 `default deny + allowlist` `NetworkPolicy` 를 추가해 ingress, DNS, Vault, Postgres, Traefik 경로만 열어두었습니다.
+
+### 5. 해결된 내용
+- 이제 dev도 north-south 경로를 public host 하나로 일관되게 쓸 수 있습니다.
+- namespace 분리가 단순 디렉터리/리소스 분리만이 아니라 실제 통신 허용 범위로도 반영되기 시작했습니다.
+
+## Cycle 14
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Ingress bootstrap leftovers]
+ A1[plain Secret in Git]
+ A2[vault namespace out of policy scope]
+ A3[local access steps undocumented]
+ end
+```
+
+### 2. 문제점
+- ingress 관련 secret이 일반 Secret 평문으로 repo에 남아 있으면 Git에 올릴 수 있는 상태라고 보기 어려웠습니다.
+- `vault` namespace는 가장 민감한 통신 경계 중 하나인데, 정책 범위에서 빠져 있으면 east-west 제한이 덜 완성된 상태였습니다.
+- 운영자 로컬 접근 절차가 문서화되지 않으면 브라우저/CLI 검증이 사람마다 달라질 수 있었습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Git-safe ingress config]
+ B1[Git-safe ingress secret handling]
+ B2[vault server allowlist policy]
+ end
+
+ B1 --> B2
+```
+
+### 4. 이전 구조 대비 변경점
+- `vault` overlay에 workload Vault ingress/egress 정책과 injector webhook ingress 정책을 추가했습니다.
+
+### 5. 해결된 내용
+- `vault` 도 최소한 서버 트래픽과 webhook ingress 경계가 정책에 반영돼, east-west 제한 범위가 더 자연스러워졌습니다.
+
+## Cycle 15
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Dev bootstrap blockers]
+ A1[Argo CD repo auth missing]
+ A2[self-hosted runner tool mismatch]
+ A3[vault-transit raft config incomplete]
+ end
+
+ A1 --> A3
+ A2 --> A3
+```
+
+### 2. 문제점
+- Argo CD가 `Project-Auth-GitOps` private repo를 읽지 못해 `vault-transit-dev`, `platform-dev`, `auth-server-dev`, `api-server-dev` 가 모두 `ComparisonError` 상태에 머물렀습니다.
+- self-hosted runner는 등록됐지만 `vault`, `terraform` 같은 필수 CLI가 없어 workflow가 `Validate required tools` 단계에서 바로 실패했습니다.
+- `vault-transit` deployment가 생성된 뒤에도 Vault가 `Cluster address must be set when using raft storage` 에러로 죽어 bootstrap을 진행할 수 없었습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Diagnosable bootstrap flow]
+ B1[argocd repo-creds secret]
+ B2[self-hosted runner with required CLIs]
+ B3[vault-transit raft api/cluster addr]
+ B4[repeatable troubleshooting notes]
+ end
+
+ B1 --> B3
+ B2 --> B3
+ B3 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- Argo CD GitHub 인증은 UI 대신 `repo-creds` secret으로 선언형 등록하는 절차를 사용했습니다.
+- runner 이슈는 GitHub Actions 로그만 보지 않고, runner 호스트에서 `command -v ...` 로 실제 설치 여부를 확인하는 방식으로 정리했습니다.
+- `infra/vault-transit/base/files/vault/vault.hcl` 과 `infra/vault/base/files/vault/vault.hcl` 에 `api_addr`, `cluster_addr`, `cluster_address` 를 추가하고, 두 service/deployment에 `8201` cluster 포트를 열었습니다.
+- README에 트러블슈팅 메모 규칙을 추가해, 이후에도 명령과 판단 근거를 함께 누적 기록할 수 있게 했습니다.
+
+### 5. 해결된 내용
+- Argo CD repo 인증 문제는 선언형 secret 적용 후 `vault-transit-dev` 가 `Synced` 로 전환되는 것으로 원인을 분리할 수 있게 됐습니다.
+- self-hosted runner 이슈는 "workflow 코드 문제"와 "runner 환경 문제"를 구분해서 진단하는 기준이 생겼습니다.
+- `vault-transit` CrashLoopBackOff 는 raft 설정 누락이 원인임을 로그로 확인했고, 동일 패턴이 `vault` 에 재발하지 않도록 base config까지 함께 보완했습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: `kubectl -n argocd describe application vault-transit-dev`
+ 핵심 관찰값: `Failed to load target state`, `authentication required: Repository not found`
+- 판단 근거: app/project 객체는 존재하지만 repo-server가 GitHub repo를 읽지 못해 sync 이전 단계에서 실패한다고 판단했습니다.
+- 수정 또는 조치: `/tmp/argocd-github-repo-creds.yaml` 로 `argocd.argoproj.io/secret-type=repo-creds` secret을 적용하고 `argocd.argoproj.io/refresh=hard` 로 강제 refresh 했습니다.
+- 검증 명령: `kubectl -n argocd describe application vault-transit-dev`
+ 검증 결과: `OperationCompleted`, `Sync Status: Synced`, `namespace/vault-transit created`
+- 재현/확인 명령: runner 호스트에서 `command -v kubectl vault jq base64 curl terraform`
+ 핵심 관찰값: `vault`, `terraform` 이 비어 있었고 workflow 로그도 `vault is required on the self-hosted runner` 에서 종료됐습니다.
+- 판단 근거: job이 GitHub-hosted가 아니라 runner 로컬 셸에서 실행되므로, 해당 머신에 CLI가 실제 설치돼 있어야 한다고 판단했습니다.
+- 수정 또는 조치: runner 호스트에 HashiCorp apt repo를 추가하고 `vault`, `terraform` 을 설치했습니다.
+- 검증 명령: `vault version`, `terraform version`
+- 재현/확인 명령: `kubectl -n vault-transit rollout status deploy/vault-transit --timeout=180s`, `kubectl -n vault-transit logs deploy/vault-transit --tail=200`
+ 핵심 관찰값: `CrashLoopBackOff`, `Cluster address must be set when using raft storage`
+- 판단 근거: 이미지 pull/PVC 문제는 아니고 Vault 프로세스가 raft listener 설정 부족 때문에 바로 종료된다고 판단했습니다.
+- 수정 또는 조치: `vault-transit` 와 `vault` base `vault.hcl`, deployment, service에 raft cluster 주소와 `8201` 포트를 추가했습니다.
+- 검증 명령: `kubectl kustomize infra/vault-transit/overlays/dev`, `kubectl kustomize infra/vault/overlays/dev`
+
+## Cycle 16
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Vault transit unstable startup]
+ A1[raft address incomplete]
+ A2[image entrypoint touching read-only config]
+ A3[RollingUpdate on single PVC]
+ end
+
+ A1 --> A2
+ A2 --> A3
+```
+
+### 2. 문제점
+- `vault-transit` application이 sync된 뒤에도 pod가 `CrashLoopBackOff` 에 빠져 bootstrap을 시작할 수 없었습니다.
+- 로그에는 `Cluster address must be set when using raft storage` 와 `Could not chown /vault/config` 가 함께 보여, 설정 누락과 기동 방식 문제가 섞여 있었습니다.
+- 단일 replica와 단일 PVC를 쓰는 `vault`/`vault-transit` 을 `RollingUpdate` 로 굴리면 old/new pod가 겹치면서 rollout 안정성이 떨어졌습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Stable single-node Vault startup]
+ B1[raft api_addr + cluster_addr]
+ B2[listener cluster_address + 8201 port]
+ B3[copy config to /tmp before start]
+ B4[Recreate deployment strategy]
+ end
+
+ B1 --> B2
+ B2 --> B3
+ B3 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- `infra/vault-transit/base/files/vault/vault.hcl` 과 `infra/vault/base/files/vault/vault.hcl` 에 `api_addr`, `cluster_addr`, listener `cluster_address` 를 추가했습니다.
+- 두 service/deployment에 `8201` cluster 포트를 추가했습니다.
+- `vault` 와 `vault-transit` deployment를 `strategy: Recreate` 로 바꿨습니다.
+- 두 deployment 모두 ConfigMap의 `vault.hcl` 을 `/tmp/vault.hcl` 로 복사한 뒤 `vault server -config=/tmp/vault.hcl` 로 실행하도록 바꿨습니다.
+
+### 5. 해결된 내용
+- raft storage 필수 설정 누락으로 인한 즉시 종료 원인을 코드에서 제거했습니다.
+- read-only ConfigMap mount와 이미지 entrypoint 충돌 가능성을 줄여, 기동 경로가 더 단순해졌습니다.
+- 단일 PVC 기반 Vault rollout에서 old/new pod 겹침을 최소화하는 방향으로 배포 전략을 정리했습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: `kubectl -n vault-transit rollout status deploy/vault-transit --timeout=180s`
+ 핵심 관찰값: `deployment "vault-transit" exceeded its progress deadline`
+- 재현/확인 명령: `kubectl -n vault-transit get deploy,pods -o wide`
+ 핵심 관찰값: pod가 `CrashLoopBackOff`
+- 재현/확인 명령: `kubectl -n vault-transit logs deploy/vault-transit --tail=200`
+ 핵심 관찰값: `Cluster address must be set when using raft storage`, `Could not chown /vault/config`
+- 판단 근거: config 값 부족만이 아니라, Vault 이미지 기본 entrypoint와 read-only ConfigMap mount 조합도 불안정 요인이라고 판단했습니다.
+- 수정 또는 조치: raft 주소/포트 보강, `Recreate` 전략 적용, `/tmp` 복사 후 실행 방식으로 deployment를 단순화했습니다.
+- 검증 명령: `kubectl kustomize infra/vault-transit/overlays/dev`, `kubectl kustomize infra/vault/overlays/dev`
+
+## Cycle 17
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Workload Vault auth and secret convergence]
+ A1[vault healthy but auth login 403]
+ A2[vault egress missing]
+ A3[legacy DB state vs new Vault values]
+ A4[Vault Agent template newline breakage]
+ end
+
+ A1 --> A2
+ A2 --> A3
+ A3 --> A4
+```
+
+### 2. 문제점
+- `postgres`, `auth-db-migration`, `keycloak` pod의 Vault Agent가 `auth/kubernetes/login` 에서 `403 permission denied` 를 내며 secret을 못 받았습니다.
+- `vault` namespace default-deny egress 때문에 workload Vault가 Kubernetes API와 PostgreSQL에 나가지 못했습니다.
+- Vault KV 값은 최신으로 바뀌었지만 PostgreSQL PVC는 기존 사용자 비밀번호를 유지하고 있어, 앱이 주입받은 값과 DB 내부 상태가 어긋났습니다.
+- Vault Agent template의 whitespace trim 때문에 `export` 문이 줄바꿈 없이 붙어서 잘못된 env 파일이 렌더링됐습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Restored workload secret path]
+ B1[workload vault kubernetes auth restored]
+ B2[vault -> kubernetes api egress]
+ B3[vault -> postgres egress]
+ B4[KV values aligned with runtime state]
+ B5[template newlines preserved]
+ end
+
+ B1 --> B2
+ B1 --> B3
+ B2 --> B4
+ B3 --> B4
+ B4 --> B5
+```
+
+### 4. 이전 구조 대비 변경점
+- `terraform/vault/dev` 로 workload Vault의 `auth/kubernetes`, role, database, transit 구성을 다시 reconcile 했습니다.
+- `infra/vault/overlays/dev/networkpolicy.yaml` 에 Kubernetes API egress와 PostgreSQL egress를 추가했습니다.
+- provider/workload Vault KV와 실제 PostgreSQL 사용자 상태를 다시 맞추는 절차를 수행했습니다.
+- `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml`, `infra/platform/overlays/dev/postgres.vault-patch.yaml` 에서 Vault template 줄바꿈이 유지되도록 수정했습니다.
+
+### 5. 해결된 내용
+- Workload Vault가 앱 service account JWT를 받아들여 Vault Agent 인증이 진행되기 시작했습니다.
+- `postgres` init container는 Vault Agent 인증을 통과했고, DB credential 발급 단계로 넘어갈 수 있게 됐습니다.
+- Keycloak/Auth가 읽는 injected env 파일이 shell 문법상 유효한 형태로 렌더링되기 시작했습니다.
+- 남은 앱 health 문제를 “Vault auth 실패”가 아니라 “DB credential mismatch / runtime convergence” 단계로 좁힐 수 있게 됐습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: `kubectl -n platform logs postgres-0 -c vault-agent-init --tail=80`, `kubectl -n auth-dev logs -c vault-agent-init --tail=80`
+ 핵심 관찰값: `auth/kubernetes/login` 에서 `403 permission denied`
+- 재현/확인 명령: `vault auth list`, `vault read auth/kubernetes/config`, `vault read auth/kubernetes/role/`
+ 핵심 관찰값: `kubernetes` auth mount가 한때 사라졌고, backend/role을 다시 복구해야 했음
+- 재현/확인 명령: `curl .../tokenreviews` with reviewer token
+ 핵심 관찰값: Kubernetes `TokenReview` 자체는 성공했고, 문제를 Vault backend / network 쪽으로 좁힐 수 있었음
+- 재현/확인 명령: `kubectl -n vault exec deploy/vault -- nslookup postgres.platform.svc.cluster.local`
+ 핵심 관찰값: workload Vault pod에서는 headless service 대표 이름이 `NXDOMAIN` 이었고, `postgres-0.postgres.platform.svc.cluster.local` 은 해석됨
+- 판단 근거: `vault` 가 TokenReview와 DB dynamic credential 발급을 하려면 Kubernetes API / PostgreSQL egress가 모두 필요했고, 둘 중 하나라도 막히면 downstream pod가 전부 `Init` 단계에 머문다고 판단했습니다.
+- 수정 또는 조치:
+ - `infra/vault/overlays/dev/networkpolicy.yaml` 에 Kubernetes API / PostgreSQL egress 추가
+ - workload Vault auth backend 재생성 및 role 재적용
+ - provider/workload Vault KV와 PostgreSQL 실제 사용자 비밀번호 재정렬
+ - Vault template 줄바꿈 수정
+- 검증 명령:
+ - `vault read database/creds/auth-db-migration-dev`
+ - `kubectl -n platform exec postgres-0 -c postgres -- psql ...`
+ - `kubectl -n platform exec -c vault-agent -- cat /vault/secrets/keycloak-env`
+ - `kubectl -n argocd get applications platform-dev auth-server-dev api-server-dev -o wide`
+
+## Cycle 18
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[CI local state drift]
+ A1[terraform local backend]
+ A2[runner cannot see existing state]
+ A3[workflow AppRole lacks bootstrap privileges]
+ end
+
+ A1 --> A2
+ A2 --> A3
+```
+
+### 2. 문제점
+- CI의 `terraform/vault-transit/dev apply` 가 매번 `Plan: 11 to add` 로 시작하며 이미 존재하는 `kv/`, `transit/`, `approle` 을 다시 만들려 했습니다.
+- `vault-transit` workflow AppRole 토큰은 기존 리소스 reconcile 용이지, 최초 bootstrap 수준의 `sys/auth/*` / `auth/token/create` 권한까지 갖지 않아 `403 permission denied` 가 났습니다.
+- 원인은 runner가 `vault-transit-dev.tfstate` 를 못 보고 local backend state 없이 실행되고 있었기 때문이었습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Fail-fast CI reconcile]
+ B1[persistent terraform state path]
+ B2[init -reconfigure with explicit backend path]
+ B3[empty/missing state guard]
+ B4[workflow token only reconciles existing resources]
+ end
+
+ B1 --> B2
+ B2 --> B3
+ B3 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- `.github/workflows/vault-dev-reconcile.yaml` 에 `TF_STATE_DIR` 을 추가해 runner workspace의 `.terraform-state` 를 명시적으로 사용하도록 바꿨습니다.
+- `terraform init` 에 `-reconfigure -backend-config=path=...` 를 넣어 매 실행마다 state 경로를 명시적으로 고정했습니다.
+- `vault-transit` state 파일이 없거나 비어 있으면 bootstrap처럼 create 시도하지 않고, 명확한 에러로 중단하도록 가드를 추가했습니다.
+
+### 5. 해결된 내용
+- CI가 state 없이 기존 리소스를 다시 만들려다가 실패하는 패턴을 조기에 차단할 수 있게 됐습니다.
+- workflow AppRole 토큰이 “기존 리소스 reconcile” 용도라는 점을 workflow 자체에 반영해, bootstrap과 reconcile 경계를 분명히 했습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: workflow 로그에서 `path is already in use at kv/`, `path is already in use at transit/`, `permission denied` 확인
+- 재현/확인 명령: `terraform -chdir=terraform/vault-transit/dev state list`
+ 핵심 관찰값: 로컬에는 state가 있지만 CI 실행 컨텍스트에서는 state를 못 보는 패턴이었음
+- 판단 근거: state가 없으니 Terraform이 기존 mount/auth backend를 신규 생성 대상으로 보고, workflow AppRole 토큰은 bootstrap 권한이 없어 403이 난다고 판단했습니다.
+- 수정 또는 조치: workflow에서 state path를 고정하고, missing/empty state일 때 fail-fast 하도록 변경했습니다.
+- 검증 명령: 다음 CI 실행에서 `Missing vault-transit Terraform state ...` 또는 정상 `state list` 통과 여부 확인
+
+## Cycle 19
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Post-bootstrap runtime drift]
+ A1[vault auth backend drift]
+ A2[vault egress gaps]
+ A3[persisted postgres state]
+ A4[malformed injected env files]
+ A5[CI local state missing]
+ end
+
+ A1 --> A2
+ A2 --> A3
+ A3 --> A4
+ A5 --> A1
+```
+
+### 2. 문제점
+- workload Vault는 살아 있었지만 `auth/kubernetes/login` 이 `403 permission denied` 를 내며 `postgres`, `auth-db-migration`, `keycloak` 의 Vault Agent init이 모두 막혔습니다.
+- `vault` namespace egress가 Kubernetes API와 PostgreSQL까지 열려 있지 않아 TokenReview와 DB dynamic credential 발급이 실패했습니다.
+- PostgreSQL PVC를 유지한 상태에서 Vault KV 값만 바꾸면 DB 내부 사용자 비밀번호와 새 주입값이 어긋나 Keycloak/Auth가 계속 로그인에 실패했습니다.
+- Vault Agent template에서 aggressive trim을 써서 `export` 문이 붙어 렌더링되고, 실제 injected env 파일이 shell 문법상 깨졌습니다.
+- CI는 local backend state를 못 본 채 기존 `vault-transit` 리소스를 다시 만들려 해서 `path is already in use` / `permission denied` 로 실패했습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Recoverable runtime convergence]
+ B1[workload vault auth recreated]
+ B2[vault -> kubernetes api egress]
+ B3[vault -> postgres egress]
+ B4[provider/workload KV re-aligned]
+ B5[postgres/keycloak runtime state re-aligned]
+ B6[template newlines preserved]
+ B7[CI imports or reuses local state]
+ end
+
+ B1 --> B2
+ B1 --> B3
+ B2 --> B4
+ B3 --> B5
+ B4 --> B5
+ B5 --> B6
+ B7 --> B1
+```
+
+### 4. 이전 구조 대비 변경점
+- `terraform/vault/dev` 로 workload Vault의 `kubernetes` auth backend와 관련 role을 다시 복구했습니다.
+- `infra/vault/overlays/dev/networkpolicy.yaml` 에 Kubernetes API egress, PostgreSQL egress를 추가했습니다.
+- provider/workload Vault KV 값을 실제 persisted DB 상태와 비교해 다시 정렬하고, 필요 시 PostgreSQL 사용자 비밀번호도 직접 맞췄습니다.
+- `apps/auth-server/overlays/dev/deployment.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak.vault-patch.yaml`, `infra/platform/overlays/dev/keycloak-client-sync.vault-patch.yaml`, `infra/platform/overlays/dev/postgres.vault-patch.yaml` 의 Vault template 줄바꿈을 보존하도록 수정했습니다.
+- `.github/workflows/vault-dev-reconcile.yaml` 에 local state 경로 고정, empty state guard, import 준비 경로를 추가해 CI가 bootstrap 리소스를 새로 만들려 하지 않도록 정리했습니다.
+
+### 5. 해결된 내용
+- `postgres` Vault Agent init은 최종적으로 인증 성공까지 확인됐고, `postgres-0` 는 `2/2 Running` 으로 회복됐습니다.
+- `keycloak` 은 malformed env / DB auth 문제를 분리해서 볼 수 있게 됐고, bootstrap admin/DB credential 정합성까지 운영 관점에서 정리할 수 있게 됐습니다.
+- `auth-server` 는 DB 연결 성공과 Spring Boot 초기화 단계까지 올라와, Vault transit/JWT 쪽 남은 런타임 오류만 분리해 볼 수 있게 됐습니다.
+- CI는 최소한 state 부재를 모른 채 bootstrap을 다시 시도하는 패턴을 fail-fast 하도록 바뀌었습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: `kubectl -n platform logs postgres-0 -c vault-agent-init --tail=80`, `kubectl -n auth-dev logs -c vault-agent-init --tail=80`
+ 핵심 관찰값: `auth/kubernetes/login` 에서 `403 permission denied`
+- 재현/확인 명령: `vault auth list`, `vault read auth/kubernetes/config`, `vault read auth/kubernetes/role/`
+ 핵심 관찰값: `kubernetes` auth mount가 한때 없어졌고, role/config를 다시 복구해야 했음
+- 재현/확인 명령: `curl .../tokenreviews`
+ 핵심 관찰값: Kubernetes `TokenReview` 는 성공하므로 SA JWT 자체보다 Vault auth/backend/network 문제로 좁혀졌음
+- 재현/확인 명령: `kubectl -n vault exec deploy/vault -- nslookup postgres.platform.svc.cluster.local`
+ 핵심 관찰값: 대표 headless service 이름은 `NXDOMAIN`, `postgres-0.postgres.platform.svc.cluster.local` 은 해석 가능
+- 재현/확인 명령: `kubectl -n platform exec -c vault-agent -- cat /vault/secrets/keycloak-env`
+ 핵심 관찰값: `export KC_DB_PASSWORD=...export KC_BOOTSTRAP_ADMIN_PASSWORD=...` 처럼 줄바꿈이 깨져 있었음
+- 재현/확인 명령: `kubectl -n platform exec postgres-0 -c postgres -- psql ...`, `vault kv get ...`
+ 핵심 관찰값: PostgreSQL 내부 비밀번호와 Vault KV 주입값이 달라 PVC 기반 기존 상태와 새 입력값이 충돌하고 있었음
+- 재현/확인 명령: CI 로그에서 `Plan: 11 to add`, `path is already in use`, `permission denied`
+ 핵심 관찰값: runner가 local backend state를 못 보고 기존 `vault-transit` 리소스를 신규 생성 대상으로 보고 있었음
+
+## Cycle 20
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Partial state drift]
+ A1[self-hosted runner workspace]
+ A2[local backend state exists but is incomplete]
+ A3[terraform sees some resources, misses others]
+ A4[workflow token tries create on missing state entries]
+ end
+
+ A1 --> A2
+ A2 --> A3
+ A3 --> A4
+```
+
+### 2. 문제점
+- CI는 더 이상 완전히 빈 state는 아니었지만, `vault-transit` state에 정책 일부만 남고 mount/auth/token 같은 핵심 리소스가 빠진 **partial state** 상태로 실행되고 있었습니다.
+- 기존 workflow는 `state list` 가 완전히 비어 있을 때만 import 하도록 되어 있어, partial state일 때는 import 분기가 전혀 돌지 않았습니다.
+- 그래서 Terraform은 빠진 리소스만 신규 생성 대상으로 보고 `kv/`, `transit/`, `approle/` 를 다시 만들려 했고, seal token 생성 단계에서는 `403 permission denied` 가 났습니다.
+- 추가로 `vault_approle_auth_backend_role_secret_id` 는 Terraform provider가 import를 지원하지 않아, partial state 복구 시 이 리소스만은 다른 managed resource처럼 state로 되살릴 수 없었습니다.
+- imported `vault_mount.kv` 는 live 상태에서 `type = "kv"` + `options.version = "2"` 로 읽히는데, 선언은 `type = "kv-v2"` 였기 때문에 partial state 복구 후에도 mount replacement가 다시 발생했습니다.
+- imported `vault_token.seal` 은 기존 accessor revoke가 필요한데, workflow 정책에 `auth/token/revoke-accessor` 권한이 빠져 있었습니다.
+- 더 근본적으로는 `vault_mount`, `vault_auth_backend`, `vault_token.seal` 같은 bootstrap 성격의 리소스를 routine CI reconcile에 계속 묶어두면, provider import/state round-trip 차이만으로도 불필요한 replacement가 반복될 수 있었습니다.
+- 같은 패턴은 이후 `terraform/vault/dev` 에도 다시 터질 수 있는 구조였습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Per-resource state reconciliation]
+ B1[terraform init with explicit backend path]
+ B2[state show per managed resource]
+ B3[missing resources imported individually]
+ B4[apply runs only after state convergence]
+ end
+
+ B1 --> B2
+ B2 --> B3
+ B3 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- `.github/workflows/vault-dev-reconcile.yaml` 의 transit 단계 import 로직을 **empty-state 전용 가드**에서 **리소스 단위 ensure/import** 방식으로 변경했습니다.
+- transit import 전에 live `vault-transit-automation-dev` policy를 현재 파일 내용으로 한 번 덮어쓰고, 새 토큰으로 다시 로그인하도록 바꿔 `auth/token/lookup-accessor` 같은 새 권한이 import 전에 즉시 반영되게 했습니다.
+- import를 지원하지 않는 `vault_approle_auth_backend_role_secret_id.workflow` 는 ensure/import 대상에서 제외하고, state에 없으면 `apply` 때 새 secret ID를 발급하도록 정리했습니다.
+- `terraform/vault-transit/dev/main.tf`, `terraform/vault/dev/main.tf` 의 KV mount 선언을 `type = "kv"` + `options = { version = "2" }` 로 바꾸고, mount에는 `prevent_destroy = true` 를 추가했습니다.
+- transit/workload KV mount에는 `ignore_changes = [type, options]` 를 추가해 import 표현 차이로 replacement가 반복되지 않게 했습니다.
+- `vault_token.seal` 은 routine CI에서 매번 rotation/replacement 하지 않도록 `lifecycle { ignore_changes = all }` 로 바꿨고, Kubernetes secret에는 `wait_for_service_account_token = true` 를 명시해 provider 기본값 드리프트를 줄였습니다.
+- `vault-transit` 에 대해 아래 리소스를 매 실행마다 `state show` 로 확인하고, 빠진 경우만 import 하도록 바꿨습니다.
+ - `vault_mount.kv`, `vault_mount.transit`
+ - `vault_auth_backend.approle`
+ - `vault_policy.*`
+ - `vault_approle_auth_backend_role.workflow`
+ - `vault_approle_auth_backend_role_secret_id.workflow`
+ - `vault_token.seal`
+ - `vault_transit_secret_backend_key.workload_unseal`
+ - `kubernetes_secret_v1.vault_transit_seal`
+- 같은 방식으로 `terraform/vault/dev` 에도 workload Vault managed resource별 import 보강을 추가했습니다.
+- `runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl` 에 `auth/token/lookup-accessor`, `auth/token/revoke-accessor` 권한을 추가해 token import/replacement cleanup이 가능하도록 보강했습니다.
+
+### 5. 해결된 내용
+- self-hosted runner가 이전 실패 실행 때문에 **부분적으로만 남은 state** 를 가지고 있어도, 다음 실행에서 빠진 리소스를 개별 import 하며 수렴할 수 있게 됐습니다.
+- CI가 “state가 조금이라도 있으니 안전하다”고 오판하고 bootstrap API를 다시 두드리는 경로를 막았습니다.
+- transit 단계뿐 아니라 workload 단계도 같은 형태의 local backend drift에 대비할 수 있게 됐습니다.
+- AppRole secret-id 리소스는 import 대신 재생성으로 수렴시키되, 기존 secret-id는 즉시 무효화되지 않으므로 현재 CI 로그인에 쓰는 값과 공존할 수 있게 했습니다.
+- KV mount 선언과 live import 결과를 맞춰 mount replacement를 제거했고, mount에는 `prevent_destroy` 를 걸어 CI가 provider/workload KV를 다시 지우지 못하게 했습니다.
+- seal token replacement가 필요한 경우에도 accessor revoke 권한이 있어 cleanup 단계까지 마칠 수 있게 했습니다.
+- bootstrap 성격의 리소스는 CI가 “계속 바꿔야 하는 대상”이 아니라 “존재를 확인하고 drift를 최소화해야 하는 대상”으로 취급하도록 방향을 바꿨습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: CI 로그에서 `Plan: 8 to add, 1 to change`, `path is already in use at kv/`, `path is already in use at approle/`, `permission denied`
+ 핵심 관찰값: 완전 빈 state라면 `Plan: 11 to add` 이어야 하는데, 일부 정책만 state에 남아 있어 **partial state** 였음
+- 재현/확인 명령: `terraform -chdir=terraform/vault-transit/dev state list`
+ 핵심 관찰값: 로컬 정상 state에는 11개 managed resource가 모두 있었음
+- 판단 근거: 기존 workflow는 `state list` 가 비었을 때만 import를 수행하므로, partial state에서는 import가 건너뛰어지고 빠진 리소스를 신규 생성 대상으로 보게 된다고 판단했습니다.
+- 판단 근거: `vault_approle_auth_backend_role_secret_id` 는 provider가 import 미지원이므로, 그 항목까지 import 대상으로 유지하면 partial state 복구가 그 단계에서 항상 멈춘다고 판단했습니다.
+- 판단 근거: `vault_mount.kv` plan에 `type "kv" -> "kv-v2"` replacement가 보인 것은 선언 방식 mismatch 때문이고, `vault_token.seal` 삭제 실패는 `auth/token/revoke-accessor` 권한 부재 때문이라고 판단했습니다.
+- 판단 근거: CI가 bootstrap 리소스를 계속 교체하려 들수록 state/import/provider 표현 차이의 영향을 크게 받으므로, 현업에서는 이런 리소스를 bootstrap 단계와 routine reconcile 단계로 분리하는 편이 안정적이라고 판단했습니다.
+- 수정 또는 조치:
+ - workflow에 `ensure_transit_state_resource`, `ensure_workload_state_resource` 함수를 추가
+ - 필요한 import ID를 accessor/path 기준으로 계산해 빠진 리소스만 import
+ - transit import 전에 `vault policy write vault-transit-automation-dev ...` 후 재로그인
+ - transit automation policy에 `auth/token/lookup-accessor`, `auth/token/revoke-accessor` 추가
+ - import 미지원인 `vault_approle_auth_backend_role_secret_id` 는 ensure 대상에서 제외
+ - transit/workload KV mount 선언을 `kv` + `options.version=2` 로 수정
+ - transit/workload mount에 `prevent_destroy = true` 추가
+ - transit/workload KV mount에 `ignore_changes = [type, options]` 추가
+ - `vault_token.seal` 에 `ignore_changes = all` 추가
+ - `kubernetes_secret_v1.vault_transit_seal` 에 `wait_for_service_account_token = true` 명시
+- 검증 명령: 다음 CI 실행에서 `Importing missing vault-transit state for ...` / `Importing missing workload-vault state for ...` 로그가 먼저 나오고, 그 뒤 `terraform apply` 가 create 대신 reconcile로 수렴하는지 확인
+
+## Cycle 21
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Mixed bootstrap + reconcile]
+ A1[vault-dev-reconcile]
+ A2[partial state import]
+ A3[provider bootstrap path missing]
+ A4[CI tries to continue with routine token]
+ end
+
+ A1 --> A2
+ A2 --> A3
+ A3 --> A4
+```
+
+### 2. 문제점
+- `vault-dev-reconcile` 가 bootstrap과 reconcile 책임을 같이 지다 보니, provider bootstrap path(`kv/dev/workload/bootstrap`) 가 없을 때도 routine workflow 안에서 해결하려는 구조였습니다.
+- 이 구조는 workflow AppRole과 local backend state 특성에 지나치게 민감했고, bootstrap 미완료/복구 상황에서 CI가 불필요하게 복잡해졌습니다.
+- 실제로 transit 단계가 통과된 뒤에도 workload bootstrap credential 부재 때문에 workflow가 중간에 실패했습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Separated bootstrap + reconcile]
+ B1[manual bootstrap runbook]
+ B2[privileged workload bootstrap]
+ B3[vault-dev-reconcile]
+ B4[routine CI reconcile only]
+ end
+
+ B1 --> B2
+ B2 --> B3
+ B3 --> B4
+```
+
+### 4. 이전 구조 대비 변경점
+- `scripts/vault/dev/bootstrap-runbook.sh`, `scripts/vault-transit/dev/bootstrap-runbook.sh` 가 `TF_STATE_DIR` override를 받아 runner/local 어디서든 같은 state 규칙으로 bootstrap 하도록 맞췄습니다.
+- `.github/workflows/vault-dev-reconcile.yaml` 은 transit reconcile 후 provider bootstrap path 존재 여부를 먼저 확인하고, 없으면 manual bootstrap runbook 으로 넘기도록 정리했습니다.
+
+### 5. 해결된 내용
+- routine CI가 bootstrap까지 억지로 끌고 가다 실패하는 구조를 끊고, “privileged bootstrap” 과 “least-privilege reconcile” 을 역할별로 분리했습니다.
+- 운영자는 bootstrap이 필요할 때만 수동 runbook을 실행하고, 평상시 CI는 bootstrap readiness 확인 뒤 안전한 reconcile만 수행하게 됐습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: CI 로그에서 `Workload Vault bootstrap AppRole is missing from provider Vault.`
+ 핵심 관찰값: transit reconcile은 성공했지만 provider bootstrap path가 비어 있어 workload routine reconcile 토큰을 만들 수 없었음
+- 판단 근거: bootstrap credential 부재는 privileged bootstrap으로만 해결해야 하고, workflow AppRole 기반 reconcile 단계에서 해결하려고 하면 책임이 섞여 구조가 계속 복잡해진다고 판단했습니다.
+- 수정 또는 조치:
+ - `scripts/vault/dev/bootstrap-runbook.sh`, `scripts/vault-transit/dev/bootstrap-runbook.sh` 에 `TF_STATE_DIR` 지원 추가
+ - `vault-dev-reconcile.yaml` 에 manual bootstrap runbook 안내 문구 추가
+- 검증 명령:
+ - 운영자 터미널에서 `scripts/vault/dev/bootstrap-runbook.sh` 실행
+ - 이후 `vault-dev-reconcile` 재실행
+ - `kubectl -n argocd get applications -o wide`
+
+## Cycle 22
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[CI still touched bootstrap-shaped resources]
+ A1[full terraform roots in CI]
+ A2[mount/auth/token drift handling]
+ A3[bootstrap path dependency in reconcile]
+ end
+
+ A1 --> A2
+ A2 --> A3
+```
+
+### 2. 문제점
+- `vault-dev-reconcile` 는 bootstrap path가 비어 있으면 명확히 멈추긴 했지만, 내부적으로는 여전히 full Terraform root와 비슷한 책임을 일부 끌고 있었습니다.
+- 현업식 권장 구조로 보려면 routine CI는 import 가능한 reconcile 리소스만 다루고, bootstrap-shaped resource는 아예 다른 루트/다른 절차로 분리되는 편이 더 안정적입니다.
+- bootstrap workflow를 GitHub에 남겨두는 것도 “privileged bootstrap은 로컬 수동” 원칙과 살짝 어긋났습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Manual bootstrap + reconcile-only CI]
+ B1[terraform/vault-transit/dev]
+ B2[terraform/vault/dev]
+ B3[manual runbooks]
+ B4[terraform/vault-transit/reconcile]
+ B5[terraform/vault/reconcile]
+ B6[vault-dev-reconcile workflow]
+ end
+
+ B3 --> B1
+ B3 --> B2
+ B1 --> B4
+ B2 --> B5
+ B4 --> B6
+ B5 --> B6
+```
+
+### 4. 이전 구조 대비 변경점
+- routine CI 전용 root를 추가했습니다.
+ - `terraform/vault-transit/reconcile`
+ - `terraform/vault/reconcile`
+- `vault-dev-reconcile` 는 더 이상 bootstrap 전용 리소스(`mount`, `auth backend`, `secret_id`, `seal token`)를 관리하지 않고, reconcile-safe 리소스만 import/apply 합니다.
+- `vault-dev-bootstrap` workflow는 제거하고, bootstrap은 수동 runbook만 사용하도록 정리했습니다.
+
+### 5. 해결된 내용
+- CI가 bootstrap-shaped 리소스 때문에 계속 state/import/provider 차이에 흔들리던 구조를 끊었습니다.
+- routine CI는 least-privilege AppRole과 import 가능한 reconcile 리소스만 다루는 더 안정적인 경로로 수렴했습니다.
+- privileged bootstrap token은 GitHub secret store에 넣지 않고 운영자 로컬에서만 쓰는 쪽으로 구조를 정리했습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: reconcile workflow 로그에서 bootstrap path 부재 확인
+ 핵심 관찰값: provider bootstrap path가 없으면 routine CI가 더 진행할 수 없음
+- 판단 근거: bootstrap과 reconcile을 코드 레벨의 Terraform root부터 분리해야 CI가 덜 흔들린다고 판단했습니다.
+- 수정 또는 조치:
+ - `terraform/vault-transit/reconcile`, `terraform/vault/reconcile` 추가
+ - `.github/workflows/vault-dev-bootstrap.yaml` 삭제
+ - `vault-dev-reconcile.yaml` 을 reconcile-only 루트 기준으로 수정
+ - runbook/README/AGENT 를 manual bootstrap 구조에 맞게 갱신
+- 검증 명령:
+ - 수동 bootstrap 후 `vault-dev-reconcile` 재실행
+ - CI plan에서 mount/auth backend/seal token 리소스가 더 이상 나오지 않는지 확인
+
+## Cycle 23
+
+### 1. 초기 구조
+```mermaid
+flowchart TD
+ subgraph BEFORE[Wrapper-less CI]
+ A1[vault-dev-reconcile YAML]
+ A2[long inline bash]
+ A3[full Terraform responsibility]
+ A4[GitHub context mixed in bash]
+ end
+
+ A1 --> A2
+ A2 --> A3
+ A2 --> A4
+```
+
+### 2. 문제점
+- `vault-dev-reconcile.yaml` 안에 긴 bash 로직이 직접 들어 있어 테스트/리뷰/수정 포인트가 YAML과 강하게 결합돼 있었습니다.
+- routine CI가 bootstrap용 full Terraform root를 직접 만지면서 state/import 결합도가 높았습니다.
+- `update-image-tag.yaml` 는 GitHub context를 inline bash 안에서 분기 처리하고 있어 리뷰 시 변수 흐름을 파악하기 불편했습니다.
+- `for ... sleep` 폴링이 남아 있어 가시성과 타임아웃 해석이 불편했습니다.
+- 여전히 Vault 접근 경로는 `port-forward` 에 의존하므로, 네트워크 관점의 최종 권장 구조까지는 아직 가지 못했습니다.
+
+### 3. 변경 후 구조
+```mermaid
+flowchart TD
+ subgraph AFTER[Thin workflow wrappers]
+ B1[vault-dev-reconcile.yaml]
+ B2[scripts/ci/reconcile-vault-dev.sh]
+ B3[terraform/vault-transit/reconcile]
+ B4[terraform/vault/reconcile]
+ B5[update-image-tag.yaml]
+ B6[scripts/ci/update-image-tag.sh]
+ end
+
+ B1 --> B2
+ B2 --> B3
+ B2 --> B4
+ B5 --> B6
+```
+
+### 4. 이전 구조 대비 변경점
+- `scripts/ci/reconcile-vault-dev.sh` 를 추가하고, `vault-dev-reconcile.yaml` 은 단계별 wrapper(step)만 남기도록 줄였습니다.
+- routine CI가 쓰는 Terraform 루트를 `terraform/vault-transit/reconcile`, `terraform/vault/reconcile` 로 분리했습니다.
+- `kubectl wait --for=condition=available ...` 로 deployment 대기를 정리했습니다.
+- `scripts/ci/update-image-tag.sh` 를 추가하고, `update-image-tag.yaml` 은 GitHub context를 `env` 로만 전달하도록 바꿨습니다.
+- bootstrap workflow는 제거하고, bootstrap은 수동 runbook 전용으로 정리했습니다.
+- `vault-dev-reconcile.yaml` 은 `VAULT_DEV_RECONCILE_RUNS_ON`, `RECONCILE_USE_PORT_FORWARD`, `TRANSIT_VAULT_ADDR`, `WORKLOAD_VAULT_ADDR` GitHub Variables 로 실행 위치와 네트워크 방식을 바꿀 수 있게 정리했습니다.
+
+### 5. 해결된 내용
+- workflow YAML은 orchestration wrapper 역할에 집중하고, 실제 로직은 저장소 안의 테스트 가능한 스크립트로 이동했습니다.
+- routine CI가 bootstrap 리소스를 직접 다루지 않게 되어 Terraform state/import 결합도가 줄었습니다.
+- `update-image-tag` 의 변수 해석 흐름이 스크립트 기준으로 단순해져 협업/리뷰 가독성이 좋아졌습니다.
+- `kubectl wait` 기반으로 대기 로직이 조금 더 직관적으로 바뀌었습니다.
+- `scripts/ci/reconcile-vault-dev.sh` 는 `RECONCILE_USE_PORT_FORWARD=false` 와 in-cluster service URL을 주면 port-forward 없이도 실행할 수 있게 바꿔, 향후 ARC/Job 전환 시 재사용할 수 있게 했습니다.
+- 따라서 ARC 도입 시에는 workflow YAML을 다시 뜯기보다 GitHub Variables 만 바꿔 in-cluster service 경로와 runner scale set 이름으로 전환할 수 있게 됐습니다.
+
+### 6. 트러블슈팅 메모
+- 재현/확인 명령: `sed -n '1,260p' .github/workflows/vault-dev-reconcile.yaml`
+ 핵심 관찰값: YAML 내부에 긴 inline bash가 남아 있으면 CI 디버깅과 변경 추적이 어려움
+- 판단 근거: GitHub Actions는 wrapper, 실제 로직은 repo의 script/terraform root가 맡는 편이 팀 협업과 유지보수에 유리하다고 판단했습니다.
+- 수정 또는 조치:
+ - `scripts/ci/reconcile-vault-dev.sh` 추가
+ - `scripts/ci/update-image-tag.sh` 추가
+ - routine reconcile용 Terraform root 추가
+ - `vault-dev-reconcile.yaml`, `update-image-tag.yaml` 을 wrapper형으로 축소
+ - `reconcile-vault-dev.sh` 에 in-cluster direct access 모드(`RECONCILE_USE_PORT_FORWARD=false`) 추가
+ - `vault-dev-reconcile.yaml` 에 runner label / Vault 주소 / port-forward 사용 여부를 GitHub Variables 로 주입하는 경로 추가
+- 검증 명령:
+ - `git diff --check`
+ - 다음 CI 실행에서 단계별 step 실패 지점이 UI에 분리되어 보이는지 확인
+ - `update-image-tag` 수동 실행으로 env/입력 해석이 정상인지 확인
diff --git a/docs/archive/README.md b/docs/archive/README.md
new file mode 100644
index 0000000..3021bc2
--- /dev/null
+++ b/docs/archive/README.md
@@ -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/`.
diff --git a/docs/runbooks/dev-bootstrap.md b/docs/runbooks/dev-bootstrap.md
new file mode 100644
index 0000000..962b5af
--- /dev/null
+++ b/docs/runbooks/dev-bootstrap.md
@@ -0,0 +1,209 @@
+# Bootstrap an empty dev-k3s cluster
+
+이 runbook은 폐기 가능한 개발 클러스터만 대상으로 합니다. production에
+사용하지 않습니다.
+
+## 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 iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example \
+ .local/terraform-backend/dev-k3s/vault-core.s3.hcl
+cp iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example \
+ .local/terraform-backend/dev-k3s/vault-database.s3.hcl
+```
+
+실제 bucket, endpoint와 workload identity를 설정합니다. backend credential은
+파일에 넣지 않습니다.
+
+## 2. Argo CD와 root Application
+
+```bash
+make bootstrap KUBE_CONTEXT="$(kubectl config current-context)"
+kubectl -n argocd get application project-gitops-dev-k3s
+```
+
+이 명령이 수행하는 직접 cluster mutation은 Argo CD 설치와 root seed뿐입니다.
+Child Application은 root가 생성합니다.
+
+## 3. Dev Vault 초기화
+
+Vault Pod가 생성될 때까지 기다린 뒤 operator workstation에서 forward합니다.
+이 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
+./hack/vault-init.sh init
+```
+
+`.local/vault/dev-k3s-init.json`을 즉시 encrypted custody로 복사합니다.
+dev-only 1-of-1 unseal key와 initial root token이 있으므로 일반 backup과
+분리합니다.
+
+## 4. Vault core
+
+초기 root token을 shell history에 직접 적지 않습니다.
+
+```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-core \
+ BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl
+
+make terraform-apply \
+ TF_ROOT=vault-core \
+ BACKEND_CONFIG=.local/terraform-backend/dev-k3s/vault-core.s3.hcl \
+ APPROVE_APPLY=dev-k3s/vault-core
+```
+
+plan에서 mount, auth backend, policy, role, JWT Transit key 이외 객체가
+나오면 apply하지 않습니다.
+
+## 5. Runtime secret seed
+
+Secret 값은 Git/Terraform을 통과하지 않습니다. 아래 변수는 terminal
+session에만 유지합니다.
+
+```bash
+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/platform/postgres/superuser @"$secret_file"
+
+jq -n \
+ --arg password "$AUTH_DB_PASSWORD" \
+ '{AUTH_DB_PASSWORD: $password, APP_DATASOURCE_USERNAME: "project_auth", APP_DATASOURCE_PASSWORD: $password}' >"$secret_file"
+vault kv put kv/dev/platform/postgres/auth-server @"$secret_file"
+
+jq -n --arg password "$KEYCLOAK_DB_PASSWORD" \
+ '{KEYCLOAK_DB_PASSWORD: $password}' >"$secret_file"
+vault kv put kv/dev/platform/postgres/keycloak @"$secret_file"
+
+jq -n --arg password "$KEYCLOAK_ADMIN_PASSWORD" \
+ '{KC_BOOTSTRAP_ADMIN_PASSWORD: $password}' >"$secret_file"
+vault kv put kv/dev/platform/keycloak/bootstrap-admin @"$secret_file"
+
+jq -n --arg secret "$KEYCLOAK_CLIENT_SECRET" \
+ '{KEYCLOAK_CLIENT_SECRET: $secret, APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET: $secret}' >"$secret_file"
+vault kv put kv/dev/platform/keycloak/client-auth-server @"$secret_file"
+
+rm -f "$secret_file"
+trap - EXIT
+```
+
+PostgreSQL이 Vault Agent 주입 후 시작하는지 확인합니다.
+
+```bash
+kubectl -n platform rollout status statefulset/postgres --timeout=600s
+```
+
+## 6. Vault database state
+
+초기 root token으로 TTL이 짧은 database 전용 token을 발급합니다.
+
+```bash
+export TF_VAR_vault_token="$(
+ vault token create \
+ -policy=vault-database-automation-dev \
+ -ttl=30m \
+ -format=json |
+ jq -r '.auth.client_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
+```
+
+## 7. Root token 폐기
+
+Kubernetes auth operator login이 동작하는지 먼저 검증합니다.
+
+```bash
+operator_jwt="$(
+ kubectl -n vault create token vault-operator \
+ --audience=vault \
+ --duration=10m
+)"
+operator_token="$(
+ VAULT_TOKEN= vault write \
+ -format=json \
+ auth/kubernetes/login \
+ role=vault-operator-dev \
+ jwt="$operator_jwt" |
+ jq -r '.auth.client_token'
+)"
+VAULT_TOKEN="$operator_token" vault token lookup >/dev/null
+```
+
+검증 후 initial root token을 폐기합니다.
+
+```bash
+./hack/vault-init.sh revoke-root
+unset operator_jwt operator_token
+unset TF_VAR_vault_token TF_VAR_postgres_admin_password
+unset POSTGRES_SUPERUSER_PASSWORD AUTH_DB_PASSWORD KEYCLOAK_DB_PASSWORD
+unset KEYCLOAK_ADMIN_PASSWORD KEYCLOAK_CLIENT_SECRET
+```
+
+encrypted custody로 옮긴 init material의 local working copy는 조직의
+dev recovery 정책에 따라 제거합니다.
+
+## 8. 확인
+
+```bash
+kubectl -n argocd get applications
+kubectl -n vault get pods
+kubectl -n platform get pods
+kubectl -n auth-dev get pods
+kubectl -n api-dev get pods
+```
+
+모든 Application의 sync/health를 확인하고 DB migration 및 Keycloak client
+sync hook 결과를 검토합니다. 실패한 hook을 고치기 위해 child manifest를
+직접 apply하지 말고 Git PR을 사용합니다.
diff --git a/docs/runbooks/sealed-secrets-recovery.md b/docs/runbooks/sealed-secrets-recovery.md
new file mode 100644
index 0000000..0457c62
--- /dev/null
+++ b/docs/runbooks/sealed-secrets-recovery.md
@@ -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.
diff --git a/docs/runbooks/terraform-state-migration.md b/docs/runbooks/terraform-state-migration.md
new file mode 100644
index 0000000..01fdb4e
--- /dev/null
+++ b/docs/runbooks/terraform-state-migration.md
@@ -0,0 +1,88 @@
+# Terraform v2 state migration
+
+새 클러스터에는 이 runbook이 필요하지 않습니다. legacy local state 또는
+이전 `provider-foundation`, `workload-foundation`, `workload-config`,
+`database-config` remote state가 실제로 존재할 때만 수행합니다.
+
+State 이동은 live object 삭제보다 위험할 수 있습니다. maintenance window와
+독립 backup 없이 진행하지 않습니다.
+
+## 목표
+
+| 이전 state | 목표 |
+|---|---|
+| provider Transit Vault state | archive 후 provider Vault 폐기 절차에서 별도 처리 |
+| workload foundation | `vault-core`의 기준 state |
+| workload config | 소유 객체를 `vault-core`로 이동 |
+| database config | `vault-database`로 backend key migration |
+
+동일 클러스터 Transit Vault는 더 이상 desired state가 아닙니다. Terraform
+state에서 먼저 삭제하거나 destroy하지 않습니다. snapshot과 seal dependency
+해제 확인 후 별도 decommission 승인을 받아 처리합니다.
+
+## 1. Inventory와 backup
+
+모든 operator machine, runner, remote backend에서 state 위치를 확인합니다.
+
+```bash
+find . -type f \
+ \( -name 'terraform.tfstate*' -o -name '*.tfplan' \) \
+ -not -path './.git/*'
+```
+
+각 state를 `terraform state pull`로 encrypted offline custody에 저장하고
+checksum을 기록합니다. backup에는 secret data가 포함될 수 있습니다.
+
+## 2. Backend key migration
+
+`workload-foundation` backend에 연결한 상태에서 새 `vault-core` backend
+configuration으로 `terraform init -migrate-state`를 수행합니다.
+`database-config`도 같은 방식으로 `vault-database` key로 이동합니다.
+
+실제 backend 파일과 이전 key는 환경마다 다르므로 명령에 값을 하드코딩하지
+않습니다. migration 전후 `terraform state pull` checksum과 `state list`를
+비교합니다.
+
+## 3. Workload configuration ownership 이동
+
+이전 `workload-config` state의 다음 객체를 `vault-core` state의 선언된
+address로 이동합니다.
+
+- application Vault policies
+- workload Kubernetes auth roles
+
+`terraform state mv -state= -state-out=`를
+사용해 offline copy에서 먼저 연습합니다. target address는 현재
+`module.workload_policies`와 `module.workload_roles`의 `terraform state list`
+결과를 기준으로 합니다. resource 이름을 추측하지 않습니다.
+
+이동 후 두 state 모두 plan합니다.
+
+- `vault-core`: 변경 없음 또는 address-only 이동
+- legacy workload-config: 삭제할 live object 없음
+
+두 plan 중 하나라도 destroy를 제안하면 중단하고 backup state를 복원합니다.
+
+## 4. Database state
+
+기존 database state가 없고 live Vault 객체만 존재할 때만 다음 import ID를
+사용합니다.
+
+```text
+vault_database_secret_backend_connection.platform_postgres database/config/platform-postgres-dev
+vault_database_secret_backend_role.auth_db_migration database/roles/auth-db-migration-dev
+vault_database_secret_backend_role.postgres_operator database/roles/postgres-operator-dev
+```
+
+이미 다른 state에 address가 있으면 import하지 말고 state ownership을 먼저
+이동합니다.
+
+## 5. Cutover 완료 조건
+
+- 두 목표 state가 remote backend와 locking을 사용
+- 동일 Vault path가 두 state list에 나타나지 않음
+- plan에 예상하지 않은 create/delete가 없음
+- legacy state와 backup은 immutable archive
+- repo와 runner에 local state/provider directory가 없음
+
+검증이 끝나기 전 legacy backend를 삭제하지 않습니다.
diff --git a/docs/runbooks/vault-backup-restore.md b/docs/runbooks/vault-backup-restore.md
new file mode 100644
index 0000000..f04062f
--- /dev/null
+++ b/docs/runbooks/vault-backup-restore.md
@@ -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가
+필요합니다.
diff --git a/hack/bootstrap-argocd.sh b/hack/bootstrap-argocd.sh
new file mode 100755
index 0000000..d49e245
--- /dev/null
+++ b/hack/bootstrap-argocd.sh
@@ -0,0 +1,59 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+
+usage() {
+ echo "Usage: $0 --context " >&2
+}
+
+if [[ "${1:-}" != "--context" || -z "${2:-}" || -n "${3:-}" ]]; then
+ usage
+ exit 1
+fi
+
+expected_context="$2"
+
+for cmd in curl kubectl sha256sum; do
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ echo "$cmd is required" >&2
+ exit 1
+ fi
+done
+
+current_context="$(kubectl config current-context)"
+if [[ "$current_context" != "$expected_context" ]]; then
+ echo "Refusing bootstrap: current context is ${current_context}, expected ${expected_context}." >&2
+ exit 1
+fi
+
+source "${REPO_ROOT}/bootstrap/argocd/version.env"
+
+manifest="$(mktemp)"
+cleanup() {
+ rm -f "$manifest"
+}
+trap cleanup EXIT INT TERM
+
+curl -fsSL \
+ "https://raw.githubusercontent.com/argoproj/argo-cd/${ARGOCD_VERSION}/manifests/install.yaml" \
+ -o "$manifest"
+printf '%s %s\n' "$ARGOCD_INSTALL_SHA256" "$manifest" | sha256sum -c -
+
+kubectl --context "$expected_context" create namespace argocd --dry-run=client -o yaml |
+ kubectl --context "$expected_context" apply -f -
+kubectl --context "$expected_context" apply \
+ --server-side \
+ --force-conflicts \
+ -n argocd \
+ -f "$manifest"
+kubectl --context "$expected_context" -n argocd \
+ rollout status deployment/argocd-server --timeout=300s
+kubectl --context "$expected_context" -n argocd \
+ rollout status deployment/argocd-repo-server --timeout=300s
+kubectl --context "$expected_context" apply \
+ -f "${REPO_ROOT}/bootstrap/argocd/root-application.yaml"
+
+echo "Argo CD ${ARGOCD_VERSION} and the dev-k3s root Application are installed."
diff --git a/hack/validate.sh b/hack/validate.sh
new file mode 100755
index 0000000..b6b513e
--- /dev/null
+++ b/hack/validate.sh
@@ -0,0 +1,138 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+
+for cmd in bash git helm jq kubectl rg terraform; do
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ echo "$cmd is required" >&2
+ exit 1
+ fi
+done
+
+cd "$REPO_ROOT"
+
+forbidden_files="$(
+ git ls-files |
+ rg '(^|/)(\\.terraform|\\.terraform-state|\\.local)(/|$)|(^|/)terraform\\.tfstate($|\\.)|\\.tfplan$' ||
+ true
+)"
+if [[ -n "$forbidden_files" ]]; then
+ echo "Generated or state files are tracked:" >&2
+ echo "$forbidden_files" >&2
+ exit 1
+fi
+
+while IFS= read -r script; do
+ bash -n "$script"
+done < <(rg --files -g '*.sh')
+
+while IFS= read -r script; do
+ if [[ ! -x "$script" ]]; then
+ echo "Shell entry point is not executable: ${script}" >&2
+ exit 1
+ fi
+done < <(rg --files hack -g '*.sh')
+
+jq empty platform/auth-system/base/files/keycloak/project-auth-realm.json
+
+overlays=(
+ clusters/dev-k3s
+ clusters/dev-k3s/manifests/api-server
+ clusters/dev-k3s/manifests/auth-server
+ clusters/dev-k3s/manifests/auth-system
+ clusters/dev-k3s/manifests/vault
+)
+for overlay in "${overlays[@]}"; do
+ kubectl kustomize "$overlay" >/dev/null
+done
+
+sealed_secrets_render="$(
+ helm template sealed-secrets sealed-secrets \
+ --repo https://bitnami.github.io/sealed-secrets \
+ --version 2.17.9 \
+ --namespace kube-system \
+ --set fullnameOverride=sealed-secrets-controller \
+ --set image.repository=bitnami/sealed-secrets-controller \
+ --set-string image.tag=0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc
+)"
+if [[ "$sealed_secrets_render" != *'image: docker.io/bitnami/sealed-secrets-controller:0.33.1@sha256:e7fad65c2d2f47e48d9ca17408ed56961bfa6a6dd74ccd4a1a214664156534bc'* ]]; then
+ echo "Sealed Secrets image digest was not rendered by the Helm chart." >&2
+ exit 1
+fi
+
+vault_injector_render="$(
+ helm template vault-agent-injector vault \
+ --repo https://helm.releases.hashicorp.com \
+ --version 0.32.0 \
+ --namespace vault \
+ --set server.enabled=false \
+ --set injector.enabled=true \
+ --set global.externalVaultAddr=http://vault.vault.svc.cluster.local:8200 \
+ --set global.tlsDisable=true \
+ --set injector.image.repository=hashicorp/vault-k8s \
+ --set-string injector.image.tag=1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d \
+ --set injector.agentImage.repository=hashicorp/vault \
+ --set-string injector.agentImage.tag=1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166
+)"
+if [[ "$vault_injector_render" != *'image: "hashicorp/vault-k8s:1.7.2@sha256:ae3d307658b72a1cf35dab9bdf92c995d45cdc7183af0516857714b5bd0ba84d"'* ||
+ "$vault_injector_render" != *'value: "hashicorp/vault:1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166"'* ]]; then
+ echo "Vault injector or Agent image digest was not rendered by the Helm chart." >&2
+ exit 1
+fi
+
+rendered_images="$(
+ {
+ kubectl kustomize clusters/dev-k3s/manifests/auth-system
+ kubectl kustomize clusters/dev-k3s/manifests/vault
+ } | rg '^[[:space:]]+image: (hashicorp/vault|postgres|quay\\.io/keycloak)'
+)"
+if printf '%s\n' "$rendered_images" | rg -v '@sha256:[a-f0-9]{64}$'; then
+ echo "A third-party runtime image is not pinned by digest." >&2
+ exit 1
+fi
+
+terraform fmt -check -recursive iac/terraform
+terraform_roots=(
+ iac/terraform/live/dev-k3s/vault-core
+ iac/terraform/live/dev-k3s/vault-database
+)
+for root in "${terraform_roots[@]}"; do
+ data_dir="$(mktemp -d)"
+ TF_DATA_DIR="$data_dir" terraform -chdir="$root" init \
+ -backend=false \
+ -input=false \
+ -lockfile=readonly >/dev/null
+ TF_DATA_DIR="$data_dir" terraform -chdir="$root" validate
+ rm -rf "$data_dir"
+done
+
+if rg -n \
+ 'github\\.com/DongHyeonka/Project-Auth-GitOps|bitnami-labs\\.github\\.io/sealed-secrets|/home/donghyeon/dev/Project-Auth-GitOps|terraform/vault(-transit)?/(dev|reconcile)' \
+ --glob '!docs/archive/**' \
+ --glob '!policies/legacy/**' \
+ --glob '!hack/validate.sh' \
+ .; then
+ echo "Current files contain a legacy repository URL, Helm URL, absolute path, or Terraform root." >&2
+ exit 1
+fi
+
+if rg -n 'uses:[[:space:]]+[^#[:space:]]+@v[0-9]' .gitea/workflows; then
+ echo "Gitea Actions must be pinned to an immutable commit SHA." >&2
+ exit 1
+fi
+
+if rg -n 'ApplyOutOfSyncOnly=true' bootstrap clusters; then
+ echo "ApplyOutOfSyncOnly is incompatible with hook-based migrations and must not be enabled." >&2
+ exit 1
+fi
+
+if rg -n 'git[[:space:]]+push[^#]*(HEAD:)?main([[:space:]]|$)' .gitea hack; then
+ echo "Automation must promote changes through a branch and review, not push directly to main." >&2
+ exit 1
+fi
+
+git diff --check
+echo "Repository validation passed."
diff --git a/hack/vault-init.sh b/hack/vault-init.sh
new file mode 100755
index 0000000..5f2a418
--- /dev/null
+++ b/hack/vault-init.sh
@@ -0,0 +1,113 @@
+#!/usr/bin/env bash
+
+set -euo pipefail
+
+SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
+REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
+VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
+VAULT_INIT_OUTPUT="${VAULT_INIT_OUTPUT:-${REPO_ROOT}/.local/vault/dev-k3s-init.json}"
+
+for cmd in jq vault; do
+ if ! command -v "$cmd" >/dev/null 2>&1; then
+ echo "$cmd is required" >&2
+ exit 1
+ fi
+done
+
+export VAULT_ADDR
+
+status_json() {
+ local output=""
+ local status_code=0
+
+ set +e
+ output="$(vault status -format=json 2>/dev/null)"
+ status_code=$?
+ set -e
+
+ if [[ "$status_code" -ne 0 && "$status_code" -ne 2 ]]; then
+ echo "Vault is not reachable at ${VAULT_ADDR}." >&2
+ return "$status_code"
+ fi
+ printf '%s\n' "$output"
+}
+
+unseal() {
+ local status=""
+ local unseal_key=""
+
+ status="$(status_json)"
+ if [[ "$(jq -r '.sealed' <<<"$status")" == "false" ]]; then
+ echo "Vault is already unsealed."
+ return
+ fi
+ if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
+ echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
+ exit 1
+ fi
+
+ unseal_key="$(jq -er '.unseal_keys_b64[0]' "$VAULT_INIT_OUTPUT")"
+ vault operator unseal "$unseal_key" >/dev/null
+ echo "Vault is unsealed."
+}
+
+init() {
+ local status=""
+
+ status="$(status_json)"
+ if [[ "$(jq -r '.initialized' <<<"$status")" == "true" ]]; then
+ echo "Refusing initialization: Vault is already initialized." >&2
+ exit 1
+ fi
+ if [[ -e "$VAULT_INIT_OUTPUT" ]]; then
+ echo "Refusing to overwrite existing init material: ${VAULT_INIT_OUTPUT}" >&2
+ exit 1
+ fi
+
+ umask 077
+ mkdir -p "$(dirname "$VAULT_INIT_OUTPUT")"
+ vault operator init \
+ -key-shares=1 \
+ -key-threshold=1 \
+ -format=json >"$VAULT_INIT_OUTPUT"
+ chmod 0600 "$VAULT_INIT_OUTPUT"
+ unseal
+
+ echo "Dev Vault was initialized with a dev-only 1-of-1 Shamir key."
+ echo "Move ${VAULT_INIT_OUTPUT} to encrypted custody before continuing."
+}
+
+revoke_root() {
+ local root_token=""
+ local temporary=""
+
+ if [[ ! -f "$VAULT_INIT_OUTPUT" ]]; then
+ echo "Init material is unavailable: ${VAULT_INIT_OUTPUT}" >&2
+ exit 1
+ fi
+
+ root_token="$(jq -er '.root_token' "$VAULT_INIT_OUTPUT")"
+ VAULT_TOKEN="$root_token" vault token revoke -self
+
+ temporary="$(mktemp "${VAULT_INIT_OUTPUT}.XXXXXX")"
+ jq 'del(.root_token)' "$VAULT_INIT_OUTPUT" >"$temporary"
+ chmod 0600 "$temporary"
+ mv "$temporary" "$VAULT_INIT_OUTPUT"
+ echo "The initial root token was revoked and removed from the local init file."
+}
+
+case "${1:-}" in
+ init)
+ init
+ ;;
+ unseal)
+ unseal
+ ;;
+ revoke-root)
+ revoke_root
+ ;;
+ *)
+ echo "Usage: $0 " >&2
+ exit 1
+ ;;
+esac
diff --git a/iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example b/iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example
new file mode 100644
index 0000000..b9dd22f
--- /dev/null
+++ b/iac/terraform/backend/dev-k3s/vault-core.s3.hcl.example
@@ -0,0 +1,5 @@
+bucket = "project-gitops-terraform-state"
+key = "dev-k3s/vault-core.tfstate"
+region = "us-east-1"
+encrypt = true
+use_lockfile = true
diff --git a/iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example b/iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example
new file mode 100644
index 0000000..aeff786
--- /dev/null
+++ b/iac/terraform/backend/dev-k3s/vault-database.s3.hcl.example
@@ -0,0 +1,5 @@
+bucket = "project-gitops-terraform-state"
+key = "dev-k3s/vault-database.tfstate"
+region = "us-east-1"
+encrypt = true
+use_lockfile = true
diff --git a/iac/terraform/live/dev-k3s/vault-core/.terraform.lock.hcl b/iac/terraform/live/dev-k3s/vault-core/.terraform.lock.hcl
new file mode 100644
index 0000000..8b8acd8
--- /dev/null
+++ b/iac/terraform/live/dev-k3s/vault-core/.terraform.lock.hcl
@@ -0,0 +1,22 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/vault" {
+ version = "5.7.0"
+ constraints = "~> 5.7.0"
+ hashes = [
+ "h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
+ "zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
+ "zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
+ "zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
+ "zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
+ "zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
+ "zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
+ "zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
+ "zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
+ "zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
+ "zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
+ ]
+}
diff --git a/iac/terraform/live/dev-k3s/vault-core/main.tf b/iac/terraform/live/dev-k3s/vault-core/main.tf
new file mode 100644
index 0000000..2868576
--- /dev/null
+++ b/iac/terraform/live/dev-k3s/vault-core/main.tf
@@ -0,0 +1,171 @@
+terraform {
+ required_version = ">= 1.11.0"
+
+ required_providers {
+ vault = {
+ source = "hashicorp/vault"
+ version = "~> 5.7.0"
+ }
+ }
+
+ backend "s3" {}
+}
+
+provider "vault" {
+ address = var.vault_addr
+ skip_child_token = true
+ token = var.vault_token
+}
+
+locals {
+ policy_dir = "${path.module}/../../../../../policies/vault/dev-k3s"
+
+ workload_policies = {
+ auth-server-dev = file("${local.policy_dir}/auth-server-dev.hcl")
+ auth-db-migration-dev = file("${local.policy_dir}/auth-db-migration-dev.hcl")
+ postgres-dev = file("${local.policy_dir}/postgres-dev.hcl")
+ keycloak-dev = file("${local.policy_dir}/keycloak-dev.hcl")
+ keycloak-client-sync-dev = file("${local.policy_dir}/keycloak-client-sync-dev.hcl")
+ postgres-operator-dev = file("${local.policy_dir}/postgres-operator-dev.hcl")
+ keycloak-operator-dev = file("${local.policy_dir}/keycloak-operator-dev.hcl")
+ }
+}
+
+resource "vault_mount" "kv" {
+ path = var.kv_mount_path
+ type = "kv"
+ options = {
+ version = "2"
+ }
+
+ lifecycle {
+ prevent_destroy = true
+ }
+}
+
+resource "vault_mount" "database" {
+ path = var.database_mount_path
+ type = "database"
+
+ lifecycle {
+ prevent_destroy = true
+ }
+}
+
+resource "vault_mount" "transit" {
+ path = var.transit_mount_path
+ type = "transit"
+
+ lifecycle {
+ prevent_destroy = true
+ }
+}
+
+resource "vault_auth_backend" "kubernetes" {
+ path = var.kubernetes_auth_path
+ type = "kubernetes"
+}
+
+resource "vault_kubernetes_auth_backend_config" "cluster" {
+ backend = vault_auth_backend.kubernetes.path
+ disable_iss_validation = true
+ disable_local_ca_jwt = false
+ kubernetes_host = var.kubernetes_host
+}
+
+resource "vault_transit_secret_backend_key" "project_auth_jwt" {
+ backend = vault_mount.transit.path
+ name = var.jwt_transit_key_name
+ type = "rsa-2048"
+}
+
+resource "vault_policy" "platform_admin" {
+ name = var.platform_admin_policy_name
+ policy = file("${local.policy_dir}/platform-admin-dev.hcl")
+}
+
+resource "vault_policy" "database_automation" {
+ name = var.database_automation_policy_name
+ policy = file("${local.policy_dir}/vault-database-automation-dev.hcl")
+}
+
+module "workload_policies" {
+ source = "../../../modules/vault-policy-set"
+
+ policies = local.workload_policies
+}
+
+resource "vault_kubernetes_auth_backend_role" "operator" {
+ audience = var.kubernetes_token_audience
+ backend = vault_auth_backend.kubernetes.path
+ bound_service_account_names = [var.operator_service_account_name]
+ bound_service_account_namespaces = [var.operator_service_account_namespace]
+ role_name = var.operator_role_name
+ token_policies = [vault_policy.platform_admin.name]
+ token_ttl = var.operator_token_ttl_seconds
+}
+
+module "workload_roles" {
+ source = "../../../modules/vault-kubernetes-roles"
+
+ backend = vault_auth_backend.kubernetes.path
+ roles = {
+ auth-server-dev = {
+ audiences = [var.kubernetes_token_audience]
+ service_account_names = ["auth-server"]
+ service_account_namespaces = ["auth-dev"]
+ token_policies = [module.workload_policies.names["auth-server-dev"]]
+ token_ttl = var.kubernetes_role_ttl_seconds
+ }
+ auth-db-migration-dev = {
+ audiences = [var.kubernetes_token_audience]
+ service_account_names = ["auth-db-migration"]
+ service_account_namespaces = ["auth-dev"]
+ token_policies = [module.workload_policies.names["auth-db-migration-dev"]]
+ token_ttl = var.kubernetes_role_ttl_seconds
+ }
+ postgres-dev = {
+ audiences = [var.kubernetes_token_audience]
+ service_account_names = ["postgres"]
+ service_account_namespaces = ["platform"]
+ token_policies = [module.workload_policies.names["postgres-dev"]]
+ token_ttl = var.kubernetes_role_ttl_seconds
+ }
+ keycloak-dev = {
+ audiences = [var.kubernetes_token_audience]
+ service_account_names = ["keycloak"]
+ service_account_namespaces = ["platform"]
+ token_policies = [module.workload_policies.names["keycloak-dev"]]
+ token_ttl = var.kubernetes_role_ttl_seconds
+ }
+ keycloak-client-sync-dev = {
+ audiences = [var.kubernetes_token_audience]
+ service_account_names = ["keycloak-client-sync"]
+ service_account_namespaces = ["platform"]
+ token_policies = [module.workload_policies.names["keycloak-client-sync-dev"]]
+ token_ttl = var.kubernetes_role_ttl_seconds
+ }
+ }
+}
+
+resource "vault_jwt_auth_backend" "ci" {
+ count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
+
+ bound_issuer = var.ci_jwt_bound_issuer
+ oidc_discovery_url = var.ci_jwt_oidc_discovery_url
+ path = var.ci_jwt_auth_path
+}
+
+resource "vault_jwt_auth_backend_role" "ci" {
+ count = var.ci_jwt_oidc_discovery_url == null ? 0 : 1
+
+ backend = vault_jwt_auth_backend.ci[0].path
+ bound_audiences = var.ci_jwt_bound_audiences
+ bound_claims = var.ci_jwt_bound_claims
+ bound_claims_type = "glob"
+ role_name = var.ci_jwt_role_name
+ role_type = "jwt"
+ token_explicit_max_ttl = var.ci_token_ttl_seconds
+ token_policies = [vault_policy.database_automation.name]
+ user_claim = var.ci_jwt_user_claim
+}
diff --git a/iac/terraform/live/dev-k3s/vault-core/variables.tf b/iac/terraform/live/dev-k3s/vault-core/variables.tf
new file mode 100644
index 0000000..905c167
--- /dev/null
+++ b/iac/terraform/live/dev-k3s/vault-core/variables.tf
@@ -0,0 +1,146 @@
+variable "database_automation_policy_name" {
+ description = "Least-privilege policy used by the approved Vault database runner."
+ type = string
+ default = "vault-database-automation-dev"
+}
+
+variable "ci_jwt_auth_path" {
+ description = "JWT auth mount used by external CI."
+ type = string
+ default = "jwt-ci"
+}
+
+variable "ci_jwt_bound_audiences" {
+ description = "Accepted CI JWT audiences."
+ type = set(string)
+ default = []
+}
+
+variable "ci_jwt_bound_claims" {
+ description = "Claims that bind CI JWTs to the canonical repository and protected branch."
+ type = map(string)
+ default = {}
+}
+
+variable "ci_jwt_bound_issuer" {
+ description = "Expected issuer for CI JWTs."
+ type = string
+ default = null
+ nullable = true
+}
+
+variable "ci_jwt_oidc_discovery_url" {
+ description = "CI OIDC discovery URL. Null keeps JWT auth disabled until the issuer is confirmed."
+ type = string
+ default = null
+ nullable = true
+}
+
+variable "ci_jwt_role_name" {
+ description = "Vault role used by the GitOps configuration workflow."
+ type = string
+ default = "project-gitops-dev"
+}
+
+variable "ci_jwt_user_claim" {
+ description = "JWT claim used as the Vault identity alias."
+ type = string
+ default = "sub"
+}
+
+variable "ci_token_ttl_seconds" {
+ description = "Maximum lifetime for a CI Vault token."
+ type = number
+ default = 3600
+}
+
+variable "database_mount_path" {
+ description = "Workload Vault database secrets mount path."
+ type = string
+ default = "database"
+}
+
+variable "jwt_transit_key_name" {
+ description = "Transit key used for application JWT signing."
+ type = string
+ default = "project-auth-jwt"
+}
+
+variable "kubernetes_auth_path" {
+ description = "Kubernetes auth backend path."
+ type = string
+ default = "kubernetes"
+}
+
+variable "kubernetes_host" {
+ description = "Kubernetes TokenReview API address."
+ type = string
+ default = "https://kubernetes.default.svc.cluster.local:443"
+}
+
+variable "kubernetes_role_ttl_seconds" {
+ description = "TTL for workload Kubernetes auth tokens."
+ type = number
+ default = 3600
+}
+
+variable "kubernetes_token_audience" {
+ description = "Audience used by projected service account tokens."
+ type = string
+ default = "vault"
+}
+
+variable "kv_mount_path" {
+ description = "Workload Vault KV-v2 mount path."
+ type = string
+ default = "kv"
+}
+
+variable "operator_role_name" {
+ description = "Workload Vault Kubernetes auth role for human operators."
+ type = string
+ default = "vault-operator-dev"
+}
+
+variable "operator_service_account_name" {
+ description = "Service account authorized to open workload Vault operator sessions."
+ type = string
+ default = "vault-operator"
+}
+
+variable "operator_service_account_namespace" {
+ description = "Namespace of the workload Vault operator service account."
+ type = string
+ default = "vault"
+}
+
+variable "operator_token_ttl_seconds" {
+ description = "TTL for workload Vault operator sessions."
+ type = number
+ default = 1800
+}
+
+variable "platform_admin_policy_name" {
+ description = "Policy used only for short-lived break-glass administration."
+ type = string
+ default = "platform-admin-dev"
+}
+
+variable "transit_mount_path" {
+ description = "Workload Vault Transit mount path for application cryptography."
+ type = string
+ default = "transit"
+}
+
+variable "vault_addr" {
+ description = "Workload Vault API address reachable by the approved runner."
+ type = string
+ default = "http://127.0.0.1:8200"
+}
+
+variable "vault_token" {
+ description = "Short-lived token used only for this Terraform run."
+ type = string
+ sensitive = true
+ ephemeral = true
+}
diff --git a/iac/terraform/live/dev-k3s/vault-database/.terraform.lock.hcl b/iac/terraform/live/dev-k3s/vault-database/.terraform.lock.hcl
new file mode 100644
index 0000000..8b8acd8
--- /dev/null
+++ b/iac/terraform/live/dev-k3s/vault-database/.terraform.lock.hcl
@@ -0,0 +1,22 @@
+# This file is maintained automatically by "terraform init".
+# Manual edits may be lost in future updates.
+
+provider "registry.terraform.io/hashicorp/vault" {
+ version = "5.7.0"
+ constraints = "~> 5.7.0"
+ hashes = [
+ "h1:Pm0AcUSYmBPZgRahQX/ahiYcjtZODSAEc2rK8r8MQ18=",
+ "zh:1dd9ab6d23f61a5e522efcb462f1fd6f4a210c77b9038c8e12fa5fa663b45d01",
+ "zh:3c98d37ead857c980f7b9285f8c3e1eb7a8fd6d6799275c311c6997973389cc9",
+ "zh:3df895fbaed383e3748ba1b50f5f1046f75503483bc3d783992059f85c85ba31",
+ "zh:3e9faaa0a85c6f03c7fd7f8b7008bb3fbb8777f26c001875947cafa47f91c657",
+ "zh:52a057d0c6cde7cbfd9ceb78a3781dcfc81cf108c533f454530ea6bb87a9bea8",
+ "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
+ "zh:8521c3825254a5f7fbff8f42ca57cabf052366f0420f5f239ebebf8292c03d0e",
+ "zh:953563d429e40087eb34faf22f28e781e50eee27cfc9ac1ad04308ba592a647f",
+ "zh:a52dd76bb7f5b86cb8de7380d2e68b47ec4445782c16ee205e6a013be35a57b6",
+ "zh:bdad38c95a14c8cce1eeadcc539cf9bf74902ce7c662b79105ad993bb48ec073",
+ "zh:d3c676d7d12c15b58518fa3ee7fc398a13893b4057fe9bf4bc1fe635f3fb995a",
+ "zh:f8673b6c06da80e912c9e32dd4853f07bfca386968d5b33c9fceb6f68b519959",
+ ]
+}
diff --git a/iac/terraform/live/dev-k3s/vault-database/main.tf b/iac/terraform/live/dev-k3s/vault-database/main.tf
new file mode 100644
index 0000000..9490781
--- /dev/null
+++ b/iac/terraform/live/dev-k3s/vault-database/main.tf
@@ -0,0 +1,75 @@
+terraform {
+ required_version = ">= 1.11.0"
+
+ required_providers {
+ vault = {
+ source = "hashicorp/vault"
+ version = "~> 5.7.0"
+ }
+ }
+
+ backend "s3" {}
+}
+
+provider "vault" {
+ address = var.vault_addr
+ skip_child_token = true
+ token = var.vault_token
+}
+
+locals {
+ migration_role_name = "auth-db-migration-dev"
+ operator_role_name = "postgres-operator-dev"
+
+ creation_statements = [
+ <<-EOT
+ CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
+ GRANT "${var.auth_db_role}" TO "{{name}}";
+ EOT
+ ]
+
+ revocation_statements = [
+ <<-EOT
+ REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
+ DROP OWNED BY "{{name}}";
+ REVOKE "${var.auth_db_role}" FROM "{{name}}";
+ DROP ROLE IF EXISTS "{{name}}";
+ EOT
+ ]
+}
+
+resource "vault_database_secret_backend_connection" "platform_postgres" {
+ allowed_roles = [local.migration_role_name, local.operator_role_name]
+ backend = var.database_mount_path
+ name = var.database_config_name
+ plugin_name = "postgresql-database-plugin"
+ verify_connection = true
+
+ postgresql {
+ connection_url = "postgresql://{{username}}:{{password}}@${var.postgres_host}:${var.postgres_port}/${var.postgres_database}?sslmode=${var.postgres_sslmode}"
+ password_authentication = "scram-sha-256"
+ password_wo = var.postgres_admin_password
+ password_wo_version = var.postgres_admin_password_version
+ username = var.postgres_admin_username
+ }
+}
+
+resource "vault_database_secret_backend_role" "auth_db_migration" {
+ backend = var.database_mount_path
+ creation_statements = local.creation_statements
+ db_name = vault_database_secret_backend_connection.platform_postgres.name
+ default_ttl = var.auth_db_migration_default_ttl_seconds
+ max_ttl = var.auth_db_migration_max_ttl_seconds
+ name = local.migration_role_name
+ revocation_statements = local.revocation_statements
+}
+
+resource "vault_database_secret_backend_role" "postgres_operator" {
+ backend = var.database_mount_path
+ creation_statements = local.creation_statements
+ db_name = vault_database_secret_backend_connection.platform_postgres.name
+ default_ttl = var.postgres_operator_default_ttl_seconds
+ max_ttl = var.postgres_operator_max_ttl_seconds
+ name = local.operator_role_name
+ revocation_statements = local.revocation_statements
+}
diff --git a/iac/terraform/live/dev-k3s/vault-database/variables.tf b/iac/terraform/live/dev-k3s/vault-database/variables.tf
new file mode 100644
index 0000000..0503c0c
--- /dev/null
+++ b/iac/terraform/live/dev-k3s/vault-database/variables.tf
@@ -0,0 +1,101 @@
+variable "auth_db_migration_default_ttl_seconds" {
+ description = "Default TTL for migration credentials."
+ type = number
+ default = 3600
+}
+
+variable "auth_db_migration_max_ttl_seconds" {
+ description = "Maximum TTL for migration credentials."
+ type = number
+ default = 86400
+}
+
+variable "auth_db_role" {
+ description = "Stable PostgreSQL owner role used by dynamic users."
+ type = string
+ default = "project_auth"
+}
+
+variable "database_config_name" {
+ description = "Vault database connection name."
+ type = string
+ default = "platform-postgres-dev"
+}
+
+variable "database_mount_path" {
+ description = "Foundation-owned database secrets mount path."
+ type = string
+ default = "database"
+}
+
+variable "postgres_admin_password" {
+ description = "PostgreSQL admin password passed only through a write-only provider field."
+ type = string
+ sensitive = true
+ ephemeral = true
+}
+
+variable "postgres_admin_password_version" {
+ description = "Increment whenever postgres_admin_password is rotated."
+ type = number
+}
+
+variable "postgres_admin_username" {
+ description = "Dedicated database administration username."
+ type = string
+ default = "postgres"
+}
+
+variable "postgres_database" {
+ description = "Database in which dynamic migration objects are owned and revoked."
+ type = string
+ default = "project_auth"
+}
+
+variable "postgres_host" {
+ description = "Platform PostgreSQL service DNS name."
+ type = string
+ default = "postgres.platform.svc.cluster.local"
+}
+
+variable "postgres_operator_default_ttl_seconds" {
+ description = "Default TTL for operator database credentials."
+ type = number
+ default = 3600
+}
+
+variable "postgres_operator_max_ttl_seconds" {
+ description = "Maximum TTL for operator database credentials."
+ type = number
+ default = 28800
+}
+
+variable "postgres_port" {
+ description = "Platform PostgreSQL service port."
+ type = number
+ default = 5432
+}
+
+variable "postgres_sslmode" {
+ description = "PostgreSQL SSL mode. Dev currently uses disable; production must use verify-full."
+ type = string
+ default = "disable"
+
+ validation {
+ condition = contains(["disable", "require", "verify-ca", "verify-full"], var.postgres_sslmode)
+ error_message = "postgres_sslmode must be disable, require, verify-ca, or verify-full."
+ }
+}
+
+variable "vault_addr" {
+ description = "Workload Vault API address."
+ type = string
+ default = "http://127.0.0.1:8200"
+}
+
+variable "vault_token" {
+ description = "Short-lived token carrying vault-database-automation-dev."
+ type = string
+ sensitive = true
+ ephemeral = true
+}
diff --git a/iac/terraform/modules/vault-kubernetes-roles/main.tf b/iac/terraform/modules/vault-kubernetes-roles/main.tf
new file mode 100644
index 0000000..27229a4
--- /dev/null
+++ b/iac/terraform/modules/vault-kubernetes-roles/main.tf
@@ -0,0 +1,11 @@
+resource "vault_kubernetes_auth_backend_role" "this" {
+ for_each = var.roles
+
+ audience = one(each.value.audiences)
+ backend = var.backend
+ bound_service_account_names = each.value.service_account_names
+ bound_service_account_namespaces = each.value.service_account_namespaces
+ role_name = each.key
+ token_policies = each.value.token_policies
+ token_ttl = each.value.token_ttl
+}
diff --git a/iac/terraform/modules/vault-kubernetes-roles/variables.tf b/iac/terraform/modules/vault-kubernetes-roles/variables.tf
new file mode 100644
index 0000000..74e739c
--- /dev/null
+++ b/iac/terraform/modules/vault-kubernetes-roles/variables.tf
@@ -0,0 +1,15 @@
+variable "backend" {
+ description = "Kubernetes auth backend path."
+ type = string
+}
+
+variable "roles" {
+ description = "Kubernetes auth roles keyed by Vault role name."
+ type = map(object({
+ audiences = set(string)
+ service_account_names = set(string)
+ service_account_namespaces = set(string)
+ token_policies = set(string)
+ token_ttl = number
+ }))
+}
diff --git a/iac/terraform/modules/vault-kubernetes-roles/versions.tf b/iac/terraform/modules/vault-kubernetes-roles/versions.tf
new file mode 100644
index 0000000..9f13a12
--- /dev/null
+++ b/iac/terraform/modules/vault-kubernetes-roles/versions.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 1.11.0"
+
+ required_providers {
+ vault = {
+ source = "hashicorp/vault"
+ version = "~> 5.7.0"
+ }
+ }
+}
diff --git a/iac/terraform/modules/vault-policy-set/main.tf b/iac/terraform/modules/vault-policy-set/main.tf
new file mode 100644
index 0000000..162199d
--- /dev/null
+++ b/iac/terraform/modules/vault-policy-set/main.tf
@@ -0,0 +1,11 @@
+resource "vault_policy" "this" {
+ for_each = var.policies
+
+ name = each.key
+ policy = each.value
+}
+
+output "names" {
+ description = "Policy names keyed by their requested names."
+ value = { for name, policy in vault_policy.this : name => policy.name }
+}
diff --git a/iac/terraform/modules/vault-policy-set/variables.tf b/iac/terraform/modules/vault-policy-set/variables.tf
new file mode 100644
index 0000000..baf9785
--- /dev/null
+++ b/iac/terraform/modules/vault-policy-set/variables.tf
@@ -0,0 +1,4 @@
+variable "policies" {
+ description = "Map of Vault policy names to HCL policy documents."
+ type = map(string)
+}
diff --git a/iac/terraform/modules/vault-policy-set/versions.tf b/iac/terraform/modules/vault-policy-set/versions.tf
new file mode 100644
index 0000000..9f13a12
--- /dev/null
+++ b/iac/terraform/modules/vault-policy-set/versions.tf
@@ -0,0 +1,10 @@
+terraform {
+ required_version = ">= 1.11.0"
+
+ required_providers {
+ vault = {
+ source = "hashicorp/vault"
+ version = "~> 5.7.0"
+ }
+ }
+}
diff --git a/infra/platform/base/files/postgres/01-init-project-auth-databases.sh b/infra/platform/base/files/postgres/01-init-project-auth-databases.sh
deleted file mode 100644
index effac6b..0000000
--- a/infra/platform/base/files/postgres/01-init-project-auth-databases.sh
+++ /dev/null
@@ -1,10 +0,0 @@
-#!/bin/sh
-set -eu
-
-psql -v ON_ERROR_STOP=1 --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
diff --git a/infra/platform/overlays/dev/configmap.yaml b/infra/platform/overlays/dev/configmap.yaml
deleted file mode 100644
index 6031ef4..0000000
--- a/infra/platform/overlays/dev/configmap.yaml
+++ /dev/null
@@ -1,14 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: platform-config
-data:
- POSTGRES_SUPERUSER: postgres
- POSTGRES_DEFAULT_DB: postgres
- AUTH_DB_NAME: project_auth
- AUTH_DB_USER: project_auth
- KEYCLOAK_DB_NAME: keycloak
- KEYCLOAK_DB_USER: keycloak
- KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME: admin
- KEYCLOAK_CLIENT_ID: project-auth-server
- AUTH_SERVER_BASE_URL: http://auth-public.auth-dev.svc.cluster.local
diff --git a/infra/platform/overlays/prod/configmap.yaml b/infra/platform/overlays/prod/configmap.yaml
deleted file mode 100644
index 8c44c3b..0000000
--- a/infra/platform/overlays/prod/configmap.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: v1
-kind: ConfigMap
-metadata:
- name: platform-config
-data:
- POSTGRES_SUPERUSER: postgres
- POSTGRES_DEFAULT_DB: postgres
- AUTH_DB_NAME: project_auth
- AUTH_DB_USER: project_auth
- KEYCLOAK_DB_NAME: keycloak
- KEYCLOAK_DB_USER: keycloak
- KEYCLOAK_BOOTSTRAP_ADMIN_USERNAME: admin
- KEYCLOAK_CLIENT_ID: project-auth-server
- AUTH_SERVER_BASE_URL: http://auth-server.auth-prod.svc.cluster.local
- VAULT_TRANSIT_KEY_NAME: project-auth-jwt
diff --git a/infra/platform/overlays/prod/kustomization.yaml b/infra/platform/overlays/prod/kustomization.yaml
deleted file mode 100644
index 3734425..0000000
--- a/infra/platform/overlays/prod/kustomization.yaml
+++ /dev/null
@@ -1,9 +0,0 @@
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-
-namespace: platform-prod
-
-resources:
- - ../../base
- - namespace.yaml
- - configmap.yaml
diff --git a/infra/platform/overlays/prod/namespace.yaml b/infra/platform/overlays/prod/namespace.yaml
deleted file mode 100644
index 64b79ac..0000000
--- a/infra/platform/overlays/prod/namespace.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apiVersion: v1
-kind: Namespace
-metadata:
- name: platform-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
diff --git a/infra/vault-transit/base/deployment.yaml b/infra/vault-transit/base/deployment.yaml
deleted file mode 100644
index 9031125..0000000
--- a/infra/vault-transit/base/deployment.yaml
+++ /dev/null
@@ -1,73 +0,0 @@
-apiVersion: apps/v1
-kind: Deployment
-metadata:
- name: vault-transit
-spec:
- replicas: 1
- strategy:
- type: Recreate
- selector:
- matchLabels:
- app: vault-transit
- template:
- metadata:
- labels:
- app: vault-transit
- spec:
- containers:
- - name: vault-transit
- image: hashicorp/vault:1.18
- command:
- - /bin/sh
- - -ec
- env:
- - name: VAULT_ADDR
- value: http://127.0.0.1:8200
- args:
- - |
- cp /vault/config/vault.hcl /tmp/vault.hcl
- exec vault server -config=/tmp/vault.hcl
- ports:
- - containerPort: 8200
- name: http
- - containerPort: 8201
- name: cluster
- volumeMounts:
- - name: vault-transit-config
- mountPath: /vault/config
- readOnly: true
- - name: vault-transit-data
- mountPath: /vault/data
- readinessProbe:
- exec:
- command:
- - sh
- - -c
- - vault status -address=http://127.0.0.1:8200 >/dev/null 2>&1; code=$?; [ "$code" -eq 0 ] || [ "$code" -eq 2 ]
- initialDelaySeconds: 10
- periodSeconds: 10
- timeoutSeconds: 5
- livenessProbe:
- exec:
- command:
- - sh
- - -c
- - vault status -address=http://127.0.0.1:8200 >/dev/null 2>&1; code=$?; [ "$code" -eq 0 ] || [ "$code" -eq 2 ]
- initialDelaySeconds: 20
- periodSeconds: 15
- timeoutSeconds: 5
- resources:
- requests:
- cpu: 100m
- memory: 256Mi
- limits:
- cpu: 500m
- memory: 512Mi
- volumes:
- - name: vault-transit-config
- configMap:
- name: vault-transit-config
- defaultMode: 0555
- - name: vault-transit-data
- persistentVolumeClaim:
- claimName: vault-transit-data
diff --git a/infra/vault-transit/base/files/vault/vault.hcl b/infra/vault-transit/base/files/vault/vault.hcl
deleted file mode 100644
index 9c5d21b..0000000
--- a/infra/vault-transit/base/files/vault/vault.hcl
+++ /dev/null
@@ -1,15 +0,0 @@
-ui = true
-disable_mlock = true
-api_addr = "http://vault-transit.vault-transit.svc.cluster.local:8200"
-cluster_addr = "http://vault-transit.vault-transit.svc.cluster.local:8201"
-
-listener "tcp" {
- address = "0.0.0.0:8200"
- cluster_address = "0.0.0.0:8201"
- tls_disable = 1
-}
-
-storage "raft" {
- path = "/vault/data"
- node_id = "vault-transit-dev-0"
-}
diff --git a/infra/vault-transit/base/kustomization.yaml b/infra/vault-transit/base/kustomization.yaml
deleted file mode 100644
index 5383a68..0000000
--- a/infra/vault-transit/base/kustomization.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: kustomize.config.k8s.io/v1beta1
-kind: Kustomization
-
-resources:
- - service.yaml
- - deployment.yaml
- - pvc.yaml
-
-generatorOptions:
- disableNameSuffixHash: true
-
-configMapGenerator:
- - name: vault-transit-config
- files:
- - files/vault/vault.hcl
diff --git a/infra/vault-transit/base/pvc.yaml b/infra/vault-transit/base/pvc.yaml
deleted file mode 100644
index 1a3d517..0000000
--- a/infra/vault-transit/base/pvc.yaml
+++ /dev/null
@@ -1,10 +0,0 @@
-apiVersion: v1
-kind: PersistentVolumeClaim
-metadata:
- name: vault-transit-data
-spec:
- accessModes:
- - ReadWriteOnce
- resources:
- requests:
- storage: 5Gi
diff --git a/infra/vault-transit/base/service.yaml b/infra/vault-transit/base/service.yaml
deleted file mode 100644
index d8f62d5..0000000
--- a/infra/vault-transit/base/service.yaml
+++ /dev/null
@@ -1,15 +0,0 @@
-apiVersion: v1
-kind: Service
-metadata:
- name: vault-transit
-spec:
- selector:
- app: vault-transit
- ports:
- - name: http
- port: 8200
- targetPort: 8200
- - name: cluster
- port: 8201
- targetPort: 8201
- type: ClusterIP
diff --git a/infra/vault-transit/overlays/dev/namespace.yaml b/infra/vault-transit/overlays/dev/namespace.yaml
deleted file mode 100644
index 789f06f..0000000
--- a/infra/vault-transit/overlays/dev/namespace.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apiVersion: v1
-kind: Namespace
-metadata:
- name: vault-transit
- 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
diff --git a/infra/vault/overlays/prod/namespace.yaml b/infra/vault/overlays/prod/namespace.yaml
deleted file mode 100644
index b83e1e3..0000000
--- a/infra/vault/overlays/prod/namespace.yaml
+++ /dev/null
@@ -1,11 +0,0 @@
-apiVersion: v1
-kind: Namespace
-metadata:
- name: vault-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
diff --git a/infra/platform/base/files/keycloak/project-auth-realm.json b/platform/auth-system/base/files/keycloak/project-auth-realm.json
similarity index 100%
rename from infra/platform/base/files/keycloak/project-auth-realm.json
rename to platform/auth-system/base/files/keycloak/project-auth-realm.json
diff --git a/platform/auth-system/base/files/postgres/01-init-project-auth-databases.sh b/platform/auth-system/base/files/postgres/01-init-project-auth-databases.sh
new file mode 100644
index 0000000..0290c72
--- /dev/null
+++ b/platform/auth-system/base/files/postgres/01-init-project-auth-databases.sh
@@ -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
diff --git a/infra/platform/base/keycloak-client-sync-job.yaml b/platform/auth-system/base/keycloak-client-sync-job.yaml
similarity index 62%
rename from infra/platform/base/keycloak-client-sync-job.yaml
rename to platform/auth-system/base/keycloak-client-sync-job.yaml
index 592e52b..3b95659 100644
--- a/infra/platform/base/keycloak-client-sync-job.yaml
+++ b/platform/auth-system/base/keycloak-client-sync-job.yaml
@@ -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.platform.svc.cluster.local \
+ --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,6 +56,19 @@ 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:
diff --git a/infra/platform/base/keycloak-client-sync-serviceaccount.yaml b/platform/auth-system/base/keycloak-client-sync-serviceaccount.yaml
similarity index 100%
rename from infra/platform/base/keycloak-client-sync-serviceaccount.yaml
rename to platform/auth-system/base/keycloak-client-sync-serviceaccount.yaml
diff --git a/infra/platform/base/keycloak-deployment.yaml b/platform/auth-system/base/keycloak-deployment.yaml
similarity index 96%
rename from infra/platform/base/keycloak-deployment.yaml
rename to platform/auth-system/base/keycloak-deployment.yaml
index 1fd3266..4fb6b27 100644
--- a/infra/platform/base/keycloak-deployment.yaml
+++ b/platform/auth-system/base/keycloak-deployment.yaml
@@ -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
diff --git a/infra/platform/base/keycloak-service.yaml b/platform/auth-system/base/keycloak-service.yaml
similarity index 100%
rename from infra/platform/base/keycloak-service.yaml
rename to platform/auth-system/base/keycloak-service.yaml
diff --git a/infra/platform/base/keycloak-serviceaccount.yaml b/platform/auth-system/base/keycloak-serviceaccount.yaml
similarity index 100%
rename from infra/platform/base/keycloak-serviceaccount.yaml
rename to platform/auth-system/base/keycloak-serviceaccount.yaml
diff --git a/infra/platform/base/kustomization.yaml b/platform/auth-system/base/kustomization.yaml
similarity index 91%
rename from infra/platform/base/kustomization.yaml
rename to platform/auth-system/base/kustomization.yaml
index 540e5da..8a2f1a8 100644
--- a/infra/platform/base/kustomization.yaml
+++ b/platform/auth-system/base/kustomization.yaml
@@ -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:
diff --git a/infra/platform/base/postgres-service.yaml b/platform/auth-system/base/postgres-service.yaml
similarity index 100%
rename from infra/platform/base/postgres-service.yaml
rename to platform/auth-system/base/postgres-service.yaml
diff --git a/infra/platform/base/postgres-serviceaccount.yaml b/platform/auth-system/base/postgres-serviceaccount.yaml
similarity index 100%
rename from infra/platform/base/postgres-serviceaccount.yaml
rename to platform/auth-system/base/postgres-serviceaccount.yaml
diff --git a/infra/platform/base/postgres-statefulset.yaml b/platform/auth-system/base/postgres-statefulset.yaml
similarity index 93%
rename from infra/platform/base/postgres-statefulset.yaml
rename to platform/auth-system/base/postgres-statefulset.yaml
index 22f7507..11b3332 100644
--- a/infra/platform/base/postgres-statefulset.yaml
+++ b/platform/auth-system/base/postgres-statefulset.yaml
@@ -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
@@ -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
diff --git a/infra/vault/base/files/vault/vault.hcl b/platform/security/vault/base/files/vault/vault.hcl
similarity index 57%
rename from infra/vault/base/files/vault/vault.hcl
rename to platform/security/vault/base/files/vault/vault.hcl
index 0677b83..2cc0b15 100644
--- a/infra/vault/base/files/vault/vault.hcl
+++ b/platform/security/vault/base/files/vault/vault.hcl
@@ -9,14 +9,6 @@ listener "tcp" {
tls_disable = 1
}
-seal "transit" {
- address = "http://vault-transit.vault-transit.svc.cluster.local:8200"
- disable_renewal = "false"
- key_name = "workload-vault-dev-unseal"
- mount_path = "transit/"
- tls_skip_verify = "true"
-}
-
storage "raft" {
path = "/vault/data"
node_id = "vault-dev-0"
diff --git a/infra/vault/base/kustomization.yaml b/platform/security/vault/base/kustomization.yaml
similarity index 86%
rename from infra/vault/base/kustomization.yaml
rename to platform/security/vault/base/kustomization.yaml
index 29f15c2..6db08ab 100644
--- a/infra/vault/base/kustomization.yaml
+++ b/platform/security/vault/base/kustomization.yaml
@@ -3,14 +3,12 @@ kind: Kustomization
resources:
- vault-serviceaccount.yaml
+ - vault-operator-serviceaccount.yaml
- vault-auth-delegator.clusterrolebinding.yaml
- vault-pvc.yaml
- vault-service.yaml
- vault-deployment.yaml
-generatorOptions:
- disableNameSuffixHash: true
-
configMapGenerator:
- name: vault-config
files:
diff --git a/infra/vault/base/vault-auth-delegator.clusterrolebinding.yaml b/platform/security/vault/base/vault-auth-delegator.clusterrolebinding.yaml
similarity index 100%
rename from infra/vault/base/vault-auth-delegator.clusterrolebinding.yaml
rename to platform/security/vault/base/vault-auth-delegator.clusterrolebinding.yaml
diff --git a/infra/vault/base/vault-deployment.yaml b/platform/security/vault/base/vault-deployment.yaml
similarity index 83%
rename from infra/vault/base/vault-deployment.yaml
rename to platform/security/vault/base/vault-deployment.yaml
index 1b9ec95..6a82e84 100644
--- a/infra/vault/base/vault-deployment.yaml
+++ b/platform/security/vault/base/vault-deployment.yaml
@@ -17,18 +17,13 @@ spec:
serviceAccountName: vault-server
containers:
- name: vault
- image: hashicorp/vault:1.18
+ image: hashicorp/vault:1.18.5@sha256:750bb37c1638fa194ab37053a81618c61bb0491ddec6fccac87c07a8e6cd8166
command:
- /bin/sh
- -ec
env:
- name: VAULT_ADDR
value: http://127.0.0.1:8200
- - name: VAULT_TOKEN
- valueFrom:
- secretKeyRef:
- name: vault-transit-seal
- key: VAULT_TRANSIT_SEAL_TOKEN
args:
- |
cp /vault/config/vault.hcl /tmp/vault.hcl
@@ -49,7 +44,7 @@ spec:
command:
- sh
- -c
- - vault status -address=http://127.0.0.1:8200 >/dev/null 2>&1; code=$?; [ "$code" -eq 0 ] || [ "$code" -eq 2 ]
+ - vault status -address=http://127.0.0.1:8200 >/dev/null 2>&1
initialDelaySeconds: 10
periodSeconds: 10
timeoutSeconds: 5
@@ -62,6 +57,15 @@ spec:
initialDelaySeconds: 20
periodSeconds: 15
timeoutSeconds: 5
+ startupProbe:
+ exec:
+ command:
+ - sh
+ - -c
+ - vault status -address=http://127.0.0.1:8200 >/dev/null 2>&1; code=$?; [ "$code" -eq 0 ] || [ "$code" -eq 2 ]
+ periodSeconds: 5
+ timeoutSeconds: 5
+ failureThreshold: 30
resources:
requests:
cpu: 100m
diff --git a/platform/security/vault/base/vault-operator-serviceaccount.yaml b/platform/security/vault/base/vault-operator-serviceaccount.yaml
new file mode 100644
index 0000000..a933a09
--- /dev/null
+++ b/platform/security/vault/base/vault-operator-serviceaccount.yaml
@@ -0,0 +1,5 @@
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: vault-operator
+automountServiceAccountToken: false
diff --git a/infra/vault/base/vault-pvc.yaml b/platform/security/vault/base/vault-pvc.yaml
similarity index 71%
rename from infra/vault/base/vault-pvc.yaml
rename to platform/security/vault/base/vault-pvc.yaml
index 2e414b5..285ef68 100644
--- a/infra/vault/base/vault-pvc.yaml
+++ b/platform/security/vault/base/vault-pvc.yaml
@@ -2,6 +2,8 @@ apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: vault-data
+ annotations:
+ argocd.argoproj.io/sync-options: Prune=false
spec:
accessModes:
- ReadWriteOnce
diff --git a/infra/vault/base/vault-service.yaml b/platform/security/vault/base/vault-service.yaml
similarity index 100%
rename from infra/vault/base/vault-service.yaml
rename to platform/security/vault/base/vault-service.yaml
diff --git a/infra/vault/base/vault-serviceaccount.yaml b/platform/security/vault/base/vault-serviceaccount.yaml
similarity index 100%
rename from infra/vault/base/vault-serviceaccount.yaml
rename to platform/security/vault/base/vault-serviceaccount.yaml
diff --git a/policies/legacy/dev/README.md b/policies/legacy/dev/README.md
new file mode 100644
index 0000000..c48dfdd
--- /dev/null
+++ b/policies/legacy/dev/README.md
@@ -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.
diff --git a/runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl b/policies/legacy/dev/vault-transit-automation-dev.hcl
similarity index 100%
rename from runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl
rename to policies/legacy/dev/vault-transit-automation-dev.hcl
diff --git a/runbooks/vault/dev/policies/workload-automation-dev.hcl b/policies/legacy/dev/workload-automation-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/workload-automation-dev.hcl
rename to policies/legacy/dev/workload-automation-dev.hcl
diff --git a/runbooks/vault/dev/policies/auth-db-migration-dev.hcl b/policies/vault/dev-k3s/auth-db-migration-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/auth-db-migration-dev.hcl
rename to policies/vault/dev-k3s/auth-db-migration-dev.hcl
diff --git a/runbooks/vault/dev/policies/auth-server-dev.hcl b/policies/vault/dev-k3s/auth-server-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/auth-server-dev.hcl
rename to policies/vault/dev-k3s/auth-server-dev.hcl
diff --git a/runbooks/vault/dev/policies/keycloak-client-sync-dev.hcl b/policies/vault/dev-k3s/keycloak-client-sync-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/keycloak-client-sync-dev.hcl
rename to policies/vault/dev-k3s/keycloak-client-sync-dev.hcl
diff --git a/runbooks/vault/dev/policies/keycloak-dev.hcl b/policies/vault/dev-k3s/keycloak-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/keycloak-dev.hcl
rename to policies/vault/dev-k3s/keycloak-dev.hcl
diff --git a/runbooks/vault/dev/policies/keycloak-operator-dev.hcl b/policies/vault/dev-k3s/keycloak-operator-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/keycloak-operator-dev.hcl
rename to policies/vault/dev-k3s/keycloak-operator-dev.hcl
diff --git a/runbooks/vault/dev/policies/platform-admin-dev.hcl b/policies/vault/dev-k3s/platform-admin-dev.hcl
similarity index 82%
rename from runbooks/vault/dev/policies/platform-admin-dev.hcl
rename to policies/vault/dev-k3s/platform-admin-dev.hcl
index 39aa9c3..1d60924 100644
--- a/runbooks/vault/dev/policies/platform-admin-dev.hcl
+++ b/policies/vault/dev-k3s/platform-admin-dev.hcl
@@ -33,3 +33,11 @@ path "auth/token/create-orphan" {
path "auth/token/lookup" {
capabilities = ["update"]
}
+
+path "sys/storage/raft/snapshot" {
+ capabilities = ["read", "update", "sudo"]
+}
+
+path "sys/storage/raft/snapshot-force" {
+ capabilities = ["update", "sudo"]
+}
diff --git a/runbooks/vault/dev/policies/postgres-dev.hcl b/policies/vault/dev-k3s/postgres-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/postgres-dev.hcl
rename to policies/vault/dev-k3s/postgres-dev.hcl
diff --git a/runbooks/vault/dev/policies/postgres-operator-dev.hcl b/policies/vault/dev-k3s/postgres-operator-dev.hcl
similarity index 100%
rename from runbooks/vault/dev/policies/postgres-operator-dev.hcl
rename to policies/vault/dev-k3s/postgres-operator-dev.hcl
diff --git a/policies/vault/dev-k3s/vault-database-automation-dev.hcl b/policies/vault/dev-k3s/vault-database-automation-dev.hcl
new file mode 100644
index 0000000..7b59005
--- /dev/null
+++ b/policies/vault/dev-k3s/vault-database-automation-dev.hcl
@@ -0,0 +1,14 @@
+# Managed by vault-core. The database runner may reconcile only the named
+# PostgreSQL connection and dynamic roles owned by the vault-database state.
+
+path "database/config/platform-postgres-dev" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+path "database/roles/auth-db-migration-dev" {
+ capabilities = ["create", "read", "update", "delete"]
+}
+
+path "database/roles/postgres-operator-dev" {
+ capabilities = ["create", "read", "update", "delete"]
+}
diff --git a/renovate.json b/renovate.json
new file mode 100644
index 0000000..1af63fb
--- /dev/null
+++ b/renovate.json
@@ -0,0 +1,38 @@
+{
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
+ "extends": [
+ "config:recommended"
+ ],
+ "enabledManagers": [
+ "argocd",
+ "kustomize",
+ "terraform",
+ "terraform-version"
+ ],
+ "dependencyDashboard": true,
+ "pinDigests": true,
+ "packageRules": [
+ {
+ "description": "Require review for cluster controllers and stateful platform components",
+ "matchFileNames": [
+ "clusters/dev-k3s/applications/foundation/**",
+ "clusters/dev-k3s/applications/platform/**",
+ "platform/**",
+ "iac/**"
+ ],
+ "automerge": false
+ },
+ {
+ "description": "Group non-major Terraform provider updates",
+ "matchManagers": [
+ "terraform"
+ ],
+ "matchUpdateTypes": [
+ "minor",
+ "patch",
+ "digest"
+ ],
+ "groupName": "terraform dependencies"
+ }
+ ]
+}
diff --git a/runbooks/vault-transit/dev/README.md b/runbooks/vault-transit/dev/README.md
deleted file mode 100644
index a82d109..0000000
--- a/runbooks/vault-transit/dev/README.md
+++ /dev/null
@@ -1,149 +0,0 @@
-## Vault Transit Dev Bootstrap
-
-이 문서는 dev 환경의 **unseal provider Vault** 를 Terraform으로 선언적으로 bootstrap 하는 절차를 정리합니다.
-
-이 Vault는 애플리케이션 secret을 직접 저장하지 않고, 업무용 Vault의 transit auto-unseal provider와 workload seed source 역할만 담당합니다.
-
-### 준비물
-
-- `kubectl`
-- `vault`
-- `jq`
-- `terraform`
-- dev 클러스터에 접근 가능한 kubeconfig
-
-### 0. 기동 상태 확인
-
-`init / unseal` 전에 먼저 `vault-transit` pod가 실제로 기동 가능한 상태인지 확인합니다.
-
-```bash
-kubectl -n vault-transit get deploy,pods,svc,pvc
-kubectl -n vault-transit rollout status deploy/vault-transit --timeout=180s
-```
-
-정상 기준:
-
-- `deployment/vault-transit` 이 `1/1 Ready`
-- pod가 `Running`
-- `CrashLoopBackOff` 가 아님
-
-기동이 안 되면 아래를 먼저 봅니다.
-
-```bash
-kubectl -n vault-transit describe deployment vault-transit
-kubectl -n vault-transit logs deploy/vault-transit --tail=200
-kubectl -n vault-transit get events --sort-by=.lastTimestamp | tail -n 30
-```
-
-최근 dev 기준 대표 원인은 아래였습니다.
-
-- `Cluster address must be set when using raft storage`
- 원인: raft storage 사용 시 `api_addr`, `cluster_addr`, listener `cluster_address` 가 빠져 있었음
-- `Could not chown /vault/config`
- 원인: ConfigMap mount가 read-only 인데 이미지 entrypoint가 해당 경로를 `chown` 하려 함
-
-현재 base manifest는 위 이슈를 피하기 위해:
-
-- `api_addr`, `cluster_addr`, `cluster_address` 추가
-- service/deployment `8201` cluster 포트 추가
-- deployment `strategy: Recreate`
-- `/vault/config/vault.hcl` 을 `/tmp/vault.hcl` 로 복사 후 `vault server` 실행
-
-형태로 정리되어 있습니다.
-
-### 1. 포트 포워딩
-
-```bash
-kubectl port-forward -n vault-transit svc/vault-transit 18200:8200
-```
-
-### 2. init / unseal
-
-```bash
-export VAULT_ADDR=http://127.0.0.1:18200
-vault operator init -format=json > .local/vault-transit-dev-init.json
-vault operator unseal "$(jq -r '.unseal_keys_b64[0]' .local/vault-transit-dev-init.json)"
-export TF_VAR_vault_token="$(jq -r '.root_token' .local/vault-transit-dev-init.json)"
-export TF_VAR_vault_addr="$VAULT_ADDR"
-```
-
-### 3. Terraform bootstrap
-
-```bash
-terraform -chdir=terraform/vault-transit/dev init -input=false
-terraform -chdir=terraform/vault-transit/dev apply -input=false -auto-approve
-```
-
-이 apply 는 아래를 선언적으로 맞춥니다.
-
-- `kv` / `transit` secrets engine 활성화
-- workload Vault auto-unseal key 생성
-- `workload-vault-transit-dev`, `vault-transit-admin-dev`, `vault-transit-automation-dev` policy reconcile
-- workflow용 `vault-transit-dev-workflow` AppRole reconcile
-- `vault/vault-transit-seal` Kubernetes Secret 갱신
-
-필요한 CI credential은 Terraform output 으로 확인합니다.
-
-```bash
-terraform -chdir=terraform/vault-transit/dev output workflow_role_id
-terraform -chdir=terraform/vault-transit/dev output -raw workflow_secret_id
-```
-
-이 두 값은 `VAULT_TRANSIT_DEV_ROLE_ID`, `VAULT_TRANSIT_DEV_SECRET_ID` 로 CI secret store 에 저장합니다.
-
-### 4. 이후 역할
-
-- 업무용 Vault([vault app](/home/donghyeon/dev/Project-Auth-GitOps/argocd/applications/dev/infra/vault.yaml)) 는 `vault-transit-seal` Secret 의 토큰으로 transit auto-unseal 을 수행합니다.
-- `vault-transit` provider Vault 자체는 dev/on-prem 전제에서 여전히 **수동 unseal** 입니다.
-- 수동 bootstrap 은 `terraform/vault-transit/dev`, `terraform/vault/dev` 를 사용합니다.
-- self-hosted runner routine reconcile 은 provider AppRole 로 로그인한 뒤 `terraform/vault-transit/reconcile`, `terraform/vault/reconcile` 를 차례로 `apply` 합니다.
-
-### 5. Workload seed 값 입력
-
-provider Vault는 여전히 workload seed source of truth 이므로, app 비밀값은 한 번 입력해야 합니다.
-
-```bash
-export VAULT_ADDR=http://127.0.0.1:18200
-export VAULT_TOKEN="$(vault write -field=token auth/approle/login \
- role_id="" \
- secret_id="")"
-
-./scripts/vault-transit/dev/populate-workload-seeds.sh
-```
-
-이 스크립트는 값을 **프롬프트로 입력받기 때문에 shell history에 실제 secret이 남지 않습니다.**
-
-주의:
-
-- 이 스크립트는 `TF_VAR_transit_vault_token` 이 아니라 **`VAULT_TOKEN`** 을 사용합니다.
-- `VAULT_ADDR` 는 provider Vault 포트포워드인 `http://127.0.0.1:18200` 이어야 합니다.
-
-`populate-workload-seeds.example.sh` 는 필요한 key 구조를 보여주는 참고용 예시입니다.
-
-입력 경로는 목적 기준으로 나뉩니다.
-
-- `kv/dev/workload/platform/postgres/superuser`
-- `kv/dev/workload/platform/postgres/auth-server`
-- `kv/dev/workload/platform/postgres/keycloak`
-- `kv/dev/workload/platform/keycloak/bootstrap-admin`
-- `kv/dev/workload/platform/keycloak/client-auth-server`
-
-`kv/dev/workload/bootstrap` 의 workload AppRole credential 은 manual bootstrap 단계의 `terraform/vault/dev` 가 자동으로 씁니다. 사람이 따로 넣지 않습니다.
-
-### 6. 운영자 토큰이 필요한 경우
-
-장기 토큰을 Terraform state 에 저장하지 않기 위해 `vault-transit-admin-dev` 토큰은 자동 발급하지 않습니다.
-직접 점검이 필요하면 privileged token 으로 아래처럼 짧은 토큰을 발급합니다.
-
-```bash
-export VAULT_ADDR=http://127.0.0.1:18200
-export VAULT_TOKEN="$TF_VAR_vault_token"
-
-vault token create -orphan -policy=vault-transit-admin-dev -ttl=1h
-```
-
-### 주의
-
-- manual bootstrap state 는 [`.terraform-state/vault-transit-dev.tfstate`](/home/donghyeon/dev/Project-Auth-GitOps/.terraform-state/vault-transit-dev.tfstate) 에 저장됩니다.
-- routine reconcile state 는 [`.terraform-state/vault-transit-reconcile.tfstate`](/home/donghyeon/dev/Project-Auth-GitOps/.terraform-state/vault-transit-reconcile.tfstate) 에 저장됩니다.
-- Vault provider state 에는 민감한 값이 들어가므로 self-hosted runner 와 로컬 작업 디렉터리를 동일하게 보호해야 합니다.
diff --git a/runbooks/vault-transit/dev/policies/vault-transit-admin-dev.hcl b/runbooks/vault-transit/dev/policies/vault-transit-admin-dev.hcl
deleted file mode 100644
index ad7574a..0000000
--- a/runbooks/vault-transit/dev/policies/vault-transit-admin-dev.hcl
+++ /dev/null
@@ -1,39 +0,0 @@
-path "transit/*" {
- capabilities = ["create", "read", "update", "delete", "list"]
-}
-
-path "kv/*" {
- capabilities = ["create", "read", "update", "delete", "list"]
-}
-
-path "sys/internal/ui/mounts/*" {
- capabilities = ["read"]
-}
-
-path "sys/mounts/*" {
- capabilities = ["create", "read", "update", "delete", "list", "sudo"]
-}
-
-path "sys/auth/*" {
- capabilities = ["create", "read", "update", "delete", "list", "sudo"]
-}
-
-path "sys/policies/acl/*" {
- capabilities = ["create", "read", "update", "delete", "list"]
-}
-
-path "auth/approle/*" {
- capabilities = ["create", "read", "update", "delete", "list"]
-}
-
-path "auth/token/create-orphan" {
- capabilities = ["update"]
-}
-
-path "auth/token/revoke" {
- capabilities = ["update"]
-}
-
-path "auth/token/lookup" {
- capabilities = ["update"]
-}
diff --git a/runbooks/vault-transit/dev/policies/workload-vault-transit-dev.hcl b/runbooks/vault-transit/dev/policies/workload-vault-transit-dev.hcl
deleted file mode 100644
index 3815834..0000000
--- a/runbooks/vault-transit/dev/policies/workload-vault-transit-dev.hcl
+++ /dev/null
@@ -1,15 +0,0 @@
-path "transit/encrypt/workload-vault-dev-unseal" {
- capabilities = ["update"]
-}
-
-path "transit/decrypt/workload-vault-dev-unseal" {
- capabilities = ["update"]
-}
-
-path "transit/rewrap/workload-vault-dev-unseal" {
- capabilities = ["update"]
-}
-
-path "transit/keys/workload-vault-dev-unseal" {
- capabilities = ["read"]
-}
diff --git a/runbooks/vault-transit/dev/populate-workload-seeds.example.sh b/runbooks/vault-transit/dev/populate-workload-seeds.example.sh
deleted file mode 100644
index b379e00..0000000
--- a/runbooks/vault-transit/dev/populate-workload-seeds.example.sh
+++ /dev/null
@@ -1,21 +0,0 @@
-#!/usr/bin/env sh
-
-set -eu
-
-vault kv put kv/dev/workload/platform/postgres/superuser \
- POSTGRES_SUPERUSER_PASSWORD=change-me
-
-vault kv put kv/dev/workload/platform/postgres/auth-server \
- APP_DATASOURCE_USERNAME=project_auth \
- APP_DATASOURCE_PASSWORD=change-me \
- AUTH_DB_PASSWORD=change-me
-
-vault kv put kv/dev/workload/platform/postgres/keycloak \
- KEYCLOAK_DB_PASSWORD=change-me
-
-vault kv put kv/dev/workload/platform/keycloak/bootstrap-admin \
- KC_BOOTSTRAP_ADMIN_PASSWORD=change-me
-
-vault kv put kv/dev/workload/platform/keycloak/client-auth-server \
- APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET=change-me \
- KEYCLOAK_CLIENT_SECRET=change-me
diff --git a/runbooks/vault/dev/README.md b/runbooks/vault/dev/README.md
deleted file mode 100644
index 9029fdb..0000000
--- a/runbooks/vault/dev/README.md
+++ /dev/null
@@ -1,174 +0,0 @@
-## Vault Dev Bootstrap
-
-이 문서는 dev 환경의 **workload Vault** 를 Terraform으로 선언적으로 bootstrap / reconcile 하는 절차를 정리합니다.
-
-이제 책임은 두 단계로 분리합니다.
-
-- bootstrap
- - privileged workload token 필요
- - 수동 runbook 사용
-- reconcile
- - bootstrap 완료 후 `vault-dev-reconcile` workflow 가 routine apply 수행
-
-사전 조건:
-
-- [vault-transit bootstrap runbook](/home/donghyeon/dev/Project-Auth-GitOps/runbooks/vault-transit/dev/README.md) 을 먼저 완료해야 합니다.
-- `vault` namespace에 `vault-transit-seal` Secret 이 준비되어 있어야 workload Vault 가 auto-unseal 됩니다.
-
-현재 저장소의 source of truth 는 아래 두 Terraform 루트로 분리합니다.
-
-- bootstrap/manual: `terraform/vault/dev`
-- reconcile/CI: `terraform/vault/reconcile`
-
-`bootstrap-runbook.sh` 는 bootstrap 루트를, routine workflow 는 reconcile 루트를 호출합니다.
-
-### 준비물
-
-- `kubectl`
-- `vault`
-- `jq`
-- `terraform`
-- dev 클러스터에 접근 가능한 kubeconfig
-
-### 1. Vault 포트 포워딩
-
-```bash
-kubectl port-forward -n vault svc/vault 8200:8200
-```
-
-### 2. Vault init / bootstrap token 준비
-
-```bash
-export VAULT_ADDR=http://127.0.0.1:8200
-vault operator init -format=json > .local/vault-dev-init.json
-export TF_VAR_workload_vault_addr="$VAULT_ADDR"
-export TF_VAR_workload_vault_token="$(jq -r '.root_token' .local/vault-dev-init.json)"
-```
-
-Transit auto-unseal 구조이므로 정상 상태에서는 `vault operator unseal` 을 반복하지 않습니다.
-
-### 3. Provider seed credential 연결
-
-workload Terraform 은 provider Vault 에서 seed 값을 읽고, 생성한 workload workflow AppRole credential 을 다시 provider Vault bootstrap path 로 써넣습니다.
-
-```bash
-export TF_VAR_transit_vault_addr=http://127.0.0.1:18200
-export TF_VAR_transit_vault_token="$(VAULT_ADDR="$TF_VAR_transit_vault_addr" vault write -field=token auth/approle/login \
- role_id="" \
- secret_id="")"
-```
-
-### 4. Terraform bootstrap
-
-```bash
-terraform -chdir=terraform/vault/dev init -input=false
-terraform -chdir=terraform/vault/dev apply -input=false -auto-approve
-```
-
-이 apply 는 아래를 선언적으로 맞춥니다.
-
-- `kv`, `database`, `transit` mount 활성화
-- Kubernetes auth backend / role reconcile
-- AppRole backend / workflow AppRole reconcile
-- dev runtime KV 를 provider Vault seed 에 맞춰 동기화
-- JWT transit key 생성
-- Postgres database backend / dynamic role 정의
-- `kv/dev/workload/bootstrap` 에 workload workflow AppRole credential publish
-
-필요하면 output 으로 workload workflow AppRole 값을 직접 확인할 수 있습니다.
-
-```bash
-terraform -chdir=terraform/vault/dev output workflow_role_id
-terraform -chdir=terraform/vault/dev output -raw workflow_secret_id
-```
-
-### 정책 분리
-
-현재 dev 정책은 아래처럼 역할별로 나눕니다.
-
-- `auth-server-dev`
- 이유: 앱 런타임은 `platform/postgres/auth-server`, `platform/keycloak/client-auth-server`, JWT transit signing만 접근하면 충분합니다.
-- `auth-db-migration-dev`
- 이유: migration job 은 `database/creds/auth-db-migration-dev` 로 짧은 DB credential 을 받아 실행합니다.
-- `postgres-dev`
- 이유: DB pod 는 `platform/postgres/superuser`, `platform/postgres/auth-server`, `platform/postgres/keycloak` 만 읽으면 됩니다.
-- `keycloak-dev`
- 이유: Keycloak pod 는 `platform/postgres/keycloak`, `platform/keycloak/bootstrap-admin` 만 읽으면 됩니다.
-- `keycloak-client-sync-dev`
- 이유: client sync job 은 `platform/keycloak/bootstrap-admin`, `platform/keycloak/client-auth-server` 만 읽으면 됩니다.
-- `workload-automation-dev`
- 이유: Terraform apply 가 policy, role, auth, transit, KV, AppRole, database 설정을 모두 reconcile 합니다.
-- `platform-admin-dev`
- 이유: 사람이 비상 복구나 수동 운영 작업을 할 때 쓰는 운영자 정책입니다.
-- `postgres-operator-dev`
- 이유: 사람이 dev DB 에 직접 접속할 때는 `database/creds/postgres-operator-dev` 만 읽는 짧은 토큰으로 제한합니다.
-- `keycloak-operator-dev`
- 이유: 사람이 Keycloak 에 직접 로그인할 때는 bootstrap admin credential 만 읽는 짧은 토큰으로 제한합니다.
-
-### 자동화용 CI Secret
-
-`vault-dev-reconcile` workflow 를 사용하려면 최소 아래 secret 이 필요합니다.
-
-- `KUBECONFIG_DEV_B64`
-- `VAULT_TRANSIT_DEV_ROLE_ID`
-- `VAULT_TRANSIT_DEV_SECRET_ID`
-
-권장 흐름은 아래와 같습니다.
-
-1. `vault-transit` runbook 으로 provider Vault 를 1회 init / unseal / bootstrap 합니다.
-2. provider Vault 에 app seed 값을 입력합니다.
-3. workload Vault 를 1회 init 합니다.
-4. `terraform/vault/dev` 를 root token 으로 1회 apply 합니다.
-5. 이 apply 가 workload workflow AppRole credential 을 `kv/dev/workload/bootstrap` 에 써넣습니다.
-6. 이후부터는 workflow 가 provider bootstrap 확인, `terraform/vault/reconcile` apply, Argo CD app apply 를 자동 수행합니다.
-
-bootstrap용 privileged token은 GitHub secret에 올리지 않고 운영자 로컬에서만 사용하는 것을 권장합니다.
-
-### 정적 seed 로 최초 1회 넣어야 하는 값
-
-아래 값들은 최초 1회 사람이 입력하거나 상위 secret source 에서 sync 해야 합니다.
-
-- `kv/dev/workload/platform/postgres/superuser`
- 값: `POSTGRES_SUPERUSER_PASSWORD`
-- `kv/dev/workload/platform/postgres/auth-server`
- 값: `APP_DATASOURCE_USERNAME`, `APP_DATASOURCE_PASSWORD`, `AUTH_DB_PASSWORD`
-- `kv/dev/workload/platform/postgres/keycloak`
- 값: `KEYCLOAK_DB_PASSWORD`
-- `kv/dev/workload/platform/keycloak/bootstrap-admin`
- 값: `KC_BOOTSTRAP_ADMIN_PASSWORD`
-- `kv/dev/workload/platform/keycloak/client-auth-server`
- 값: `APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET`, `KEYCLOAK_CLIENT_SECRET`
-
-이 값들은 provider Vault seed path 가 source of truth 이고, workload Terraform apply 가 이를 workload Vault KV 로 동기화합니다.
-
-### 사람 직접 접근용 토큰 발급
-
-장기 운영자 토큰은 Terraform state 에 저장하지 않습니다.
-필요할 때 privileged token 으로 `platform-admin-dev` 토큰을 짧게 발급한 뒤 아래 스크립트를 사용합니다.
-
-```bash
-export VAULT_ADDR=http://127.0.0.1:8200
-export VAULT_TOKEN=
-
-vault token create -orphan -policy=platform-admin-dev -ttl=1h
-```
-
-발급된 `platform-admin-dev` 토큰으로:
-
-```bash
-export VAULT_TOKEN=
-./scripts/vault/dev/issue-operator-tokens.sh
-```
-
-이후:
-
-- `postgres-operator-dev`: `vault read database/creds/postgres-operator-dev`
-- `keycloak-operator-dev`: `vault kv get kv/dev/platform/keycloak/bootstrap-admin`
-
-를 수행해 dev 접속 정보를 확인할 수 있습니다.
-
-### 주의
-
-- manual bootstrap state 는 [`.terraform-state/vault-dev.tfstate`](/home/donghyeon/dev/Project-Auth-GitOps/.terraform-state/vault-dev.tfstate) 에 저장됩니다.
-- routine reconcile state 는 [`.terraform-state/vault-reconcile.tfstate`](/home/donghyeon/dev/Project-Auth-GitOps/.terraform-state/vault-reconcile.tfstate) 에 저장됩니다.
-- Vault provider state 에는 민감한 값이 들어가므로 self-hosted runner 와 로컬 작업 디렉터리를 동일하게 보호해야 합니다.
diff --git a/scripts/ci/reconcile-vault-dev.sh b/scripts/ci/reconcile-vault-dev.sh
deleted file mode 100644
index 532b105..0000000
--- a/scripts/ci/reconcile-vault-dev.sh
+++ /dev/null
@@ -1,292 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-REPO_ROOT="$(cd "${SCRIPT_DIR}/../.." && pwd)"
-
-source "${REPO_ROOT}/scripts/vault/dev/provider-lib.sh"
-
-usage() {
- cat <<'EOF'
-Usage: scripts/ci/reconcile-vault-dev.sh
-
-Commands:
- prepare-infra
- reconcile-transit
- reconcile-workload
- apply-apps
-EOF
-}
-
-log() {
- printf '[reconcile-vault-dev] %s\n' "$*"
-}
-
-require_cmd() {
- local cmd="$1"
- if ! command -v "$cmd" >/dev/null 2>&1; then
- echo "$cmd is required" >&2
- exit 1
- fi
-}
-
-require_env() {
- local name="$1"
- if [[ -z "${!name:-}" ]]; then
- echo "$name must be set" >&2
- exit 1
- fi
-}
-
-wait_http_ready() {
- local url="$1"
- local label="$2"
- if ! curl -fsS --retry 30 --retry-delay 2 --retry-connrefused "$url" >/dev/null 2>&1; then
- echo "${label} is not reachable at ${url}" >&2
- return 1
- fi
-}
-
-start_port_forward() {
- local namespace="$1"
- local service="$2"
- local local_port="$3"
- local remote_port="$4"
- local log_file="$5"
-
- kubectl -n "$namespace" port-forward "svc/${service}" "${local_port}:${remote_port}" >"$log_file" 2>&1 &
- local pf_pid=$!
- trap 'kill "$pf_pid" >/dev/null 2>&1 || true' EXIT
- printf '%s\n' "$pf_pid"
-}
-
-use_port_forward() {
- [[ "${RECONCILE_USE_PORT_FORWARD:-true}" == "true" ]]
-}
-
-transit_login() {
- VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \
- vault write -field=token auth/approle/login \
- role_id="${VAULT_TRANSIT_DEV_ROLE_ID}" \
- secret_id="${VAULT_TRANSIT_DEV_SECRET_ID}"
-}
-
-ensure_transit_state_resource() {
- local address="$1"
- local import_id="$2"
-
- if ! terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" state show "$address" >/dev/null 2>&1; then
- log "Importing missing vault-transit state for ${address}"
- terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" import "$address" "$import_id"
- fi
-}
-
-ensure_workload_state_resource() {
- local address="$1"
- local import_id="$2"
-
- if ! terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" state show "$address" >/dev/null 2>&1; then
- log "Importing missing workload-vault state for ${address}"
- terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" import "$address" "$import_id"
- fi
-}
-
-prepare_infra() {
- require_cmd kubectl
- require_env TF_STATE_DIR
-
- mkdir -p "${TF_STATE_DIR}"
- "${REPO_ROOT}/scripts/vault/dev/apply-argocd-dev-infra.sh"
-
- kubectl -n vault-transit wait --for=create deployment/vault-transit --timeout=300s
- kubectl -n vault-transit wait --for=condition=available deployment/vault-transit --timeout=300s
- kubectl -n vault wait --for=create deployment/vault --timeout=300s
- kubectl -n vault wait --for=condition=available deployment/vault --timeout=300s
-}
-
-reconcile_transit() {
- require_cmd kubectl
- require_cmd vault
- require_cmd terraform
- require_cmd curl
- require_cmd jq
- require_env TF_STATE_DIR
- require_env TRANSIT_VAULT_ADDR
- require_env VAULT_TRANSIT_DEV_ROLE_ID
- require_env VAULT_TRANSIT_DEV_SECRET_ID
-
- if use_port_forward; then
- start_port_forward vault-transit vault-transit 18200 8200 /tmp/vault-transit-port-forward.log >/dev/null
- fi
- wait_http_ready "${TRANSIT_VAULT_ADDR}/v1/sys/health" "Transit provider Vault API"
-
- VAULT_ADDR="${TRANSIT_VAULT_ADDR}" "${REPO_ROOT}/scripts/vault-transit/dev/ensure-unsealed.sh"
-
- local transit_tf_token
- transit_tf_token="$(transit_login)"
-
- VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \
- VAULT_TOKEN="${transit_tf_token}" \
- vault policy write \
- vault-transit-automation-dev \
- "${REPO_ROOT}/runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl"
-
- transit_tf_token="$(transit_login)"
-
- local transit_state_path="${TF_STATE_DIR}/vault-transit-reconcile.tfstate"
-
- TF_VAR_vault_addr="${TRANSIT_VAULT_ADDR}" \
- TF_VAR_vault_token="${transit_tf_token}" \
- terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" init \
- -input=false \
- -reconfigure \
- -backend-config="path=${transit_state_path}"
-
- if ! terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" state list >/dev/null 2>&1; then
- echo "vault-transit Terraform state is unreadable. Re-seed ${transit_state_path}." >&2
- exit 1
- fi
-
- export TF_VAR_vault_addr="${TRANSIT_VAULT_ADDR}"
- export TF_VAR_vault_token="${transit_tf_token}"
-
- ensure_transit_state_resource vault_policy.workload_vault_transit_dev workload-vault-transit-dev
- ensure_transit_state_resource vault_policy.vault_transit_admin_dev vault-transit-admin-dev
- ensure_transit_state_resource vault_policy.vault_transit_automation_dev vault-transit-automation-dev
- ensure_transit_state_resource vault_approle_auth_backend_role.workflow auth/approle/role/vault-transit-dev-workflow
-
- TF_VAR_vault_addr="${TRANSIT_VAULT_ADDR}" \
- TF_VAR_vault_token="${transit_tf_token}" \
- terraform -chdir="${REPO_ROOT}/terraform/vault-transit/reconcile" apply -input=false -auto-approve
-}
-
-reconcile_workload() {
- require_cmd kubectl
- require_cmd vault
- require_cmd terraform
- require_cmd curl
- require_env TF_STATE_DIR
- require_env TRANSIT_VAULT_ADDR
- require_env WORKLOAD_VAULT_ADDR
- require_env VAULT_TRANSIT_DEV_ROLE_ID
- require_env VAULT_TRANSIT_DEV_SECRET_ID
-
- if use_port_forward; then
- start_port_forward vault vault 8200 8200 /tmp/vault-workload-port-forward.log >/dev/null
- fi
- wait_http_ready "${WORKLOAD_VAULT_ADDR}/v1/sys/health" "Workload Vault API"
-
- VAULT_ADDR="${WORKLOAD_VAULT_ADDR}" "${REPO_ROOT}/scripts/vault/dev/ensure-unsealed.sh"
-
- local transit_tf_token
- transit_tf_token="$(transit_login)"
-
- local workload_role_id
- local workload_secret_id
- workload_role_id="$(
- VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \
- VAULT_TOKEN="${transit_tf_token}" \
- vault kv get -field=VAULT_WORKLOAD_DEV_ROLE_ID kv/dev/workload/bootstrap 2>/dev/null || true
- )"
- workload_secret_id="$(
- VAULT_ADDR="${TRANSIT_VAULT_ADDR}" \
- VAULT_TOKEN="${transit_tf_token}" \
- vault kv get -field=VAULT_WORKLOAD_DEV_SECRET_ID kv/dev/workload/bootstrap 2>/dev/null || true
- )"
-
- if [[ -z "${workload_role_id}" || -z "${workload_secret_id}" ]]; then
- echo "Workload Vault bootstrap AppRole is missing from provider Vault." >&2
- echo "Execute scripts/vault/dev/bootstrap-runbook.sh manually with a privileged workload token to seed kv/dev/workload/bootstrap." >&2
- exit 1
- fi
-
- local workload_tf_token
- workload_tf_token="$(
- VAULT_ADDR="${WORKLOAD_VAULT_ADDR}" \
- vault write -field=token auth/approle/login \
- role_id="${workload_role_id}" \
- secret_id="${workload_secret_id}"
- )"
-
- local workload_state_path="${TF_STATE_DIR}/vault-reconcile.tfstate"
-
- TF_VAR_workload_vault_addr="${WORKLOAD_VAULT_ADDR}" \
- TF_VAR_workload_vault_token="${workload_tf_token}" \
- TF_VAR_transit_vault_addr="${TRANSIT_VAULT_ADDR}" \
- TF_VAR_transit_vault_token="${transit_tf_token}" \
- terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" init \
- -input=false \
- -reconfigure \
- -backend-config="path=${workload_state_path}"
-
- export TF_VAR_workload_vault_addr="${WORKLOAD_VAULT_ADDR}"
- export TF_VAR_workload_vault_token="${workload_tf_token}"
- export TF_VAR_transit_vault_addr="${TRANSIT_VAULT_ADDR}"
- export TF_VAR_transit_vault_token="${transit_tf_token}"
-
- ensure_workload_state_resource vault_policy.auth_server auth-server-dev
- ensure_workload_state_resource vault_policy.auth_db_migration auth-db-migration-dev
- ensure_workload_state_resource vault_policy.postgres postgres-dev
- ensure_workload_state_resource vault_policy.keycloak keycloak-dev
- ensure_workload_state_resource vault_policy.keycloak_client_sync keycloak-client-sync-dev
- ensure_workload_state_resource vault_policy.postgres_operator postgres-operator-dev
- ensure_workload_state_resource vault_policy.keycloak_operator keycloak-operator-dev
- ensure_workload_state_resource vault_policy.platform_admin platform-admin-dev
- ensure_workload_state_resource vault_policy.workload_automation workload-automation-dev
- ensure_workload_state_resource vault_kubernetes_auth_backend_role.auth_server auth/kubernetes/role/auth-server-dev
- ensure_workload_state_resource vault_kubernetes_auth_backend_role.auth_db_migration auth/kubernetes/role/auth-db-migration-dev
- ensure_workload_state_resource vault_kubernetes_auth_backend_role.postgres auth/kubernetes/role/postgres-dev
- ensure_workload_state_resource vault_kubernetes_auth_backend_role.keycloak auth/kubernetes/role/keycloak-dev
- ensure_workload_state_resource vault_kubernetes_auth_backend_role.keycloak_client_sync auth/kubernetes/role/keycloak-client-sync-dev
- ensure_workload_state_resource vault_approle_auth_backend_role.workflow auth/approle/role/workload-dev-workflow
- ensure_workload_state_resource vault_database_secret_backend_connection.platform_postgres database/config/platform-postgres-dev
- ensure_workload_state_resource vault_database_secret_backend_role.auth_db_migration database/roles/auth-db-migration-dev
- ensure_workload_state_resource vault_database_secret_backend_role.postgres_operator database/roles/postgres-operator-dev
- ensure_workload_state_resource vault_kv_secret_v2.platform_postgres_superuser kv/data/dev/platform/postgres/superuser
- ensure_workload_state_resource vault_kv_secret_v2.platform_postgres_auth_server kv/data/dev/platform/postgres/auth-server
- ensure_workload_state_resource vault_kv_secret_v2.platform_postgres_keycloak kv/data/dev/platform/postgres/keycloak
- ensure_workload_state_resource vault_kv_secret_v2.platform_keycloak_bootstrap_admin kv/data/dev/platform/keycloak/bootstrap-admin
- ensure_workload_state_resource vault_kv_secret_v2.platform_keycloak_client_auth_server kv/data/dev/platform/keycloak/client-auth-server
-
- TF_VAR_workload_vault_addr="${WORKLOAD_VAULT_ADDR}" \
- TF_VAR_workload_vault_token="${workload_tf_token}" \
- TF_VAR_transit_vault_addr="${TRANSIT_VAULT_ADDR}" \
- TF_VAR_transit_vault_token="${transit_tf_token}" \
- terraform -chdir="${REPO_ROOT}/terraform/vault/reconcile" apply -input=false -auto-approve
-
- if kubectl -n vault get deployment -l app.kubernetes.io/name=vault-agent-injector -o name | grep -q .; then
- kubectl -n vault wait --for=condition=available deployment -l app.kubernetes.io/name=vault-agent-injector --timeout=180s
- fi
-}
-
-apply_apps() {
- require_cmd kubectl
- "${REPO_ROOT}/scripts/vault/dev/apply-argocd-dev-apps.sh"
-}
-
-main() {
- require_cmd bash
- require_cmd kubectl
-
- case "${1:-}" in
- prepare-infra)
- prepare_infra
- ;;
- reconcile-transit)
- reconcile_transit
- ;;
- reconcile-workload)
- reconcile_workload
- ;;
- apply-apps)
- apply_apps
- ;;
- *)
- usage >&2
- exit 1
- ;;
- esac
-}
-
-main "$@"
diff --git a/scripts/ci/update-image-tag.sh b/scripts/ci/update-image-tag.sh
deleted file mode 100644
index d5c26f6..0000000
--- a/scripts/ci/update-image-tag.sh
+++ /dev/null
@@ -1,131 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-resolve_inputs() {
- local service=""
- local target_env=""
- local image_tag=""
- local image_name=""
- local kustomization=""
-
- if [[ "${GITHUB_EVENT_NAME:-}" == "workflow_dispatch" ]]; then
- service="${INPUT_SERVICE:-}"
- target_env="${INPUT_TARGET_ENV:-}"
- image_tag="${INPUT_IMAGE_TAG:-}"
- else
- service="${EVENT_SERVICE:-}"
- target_env="${EVENT_TARGET_ENV:-}"
- image_tag="${EVENT_IMAGE_TAG:-}"
- fi
-
- if [[ -z "$service" || -z "$target_env" || -z "$image_tag" ]]; then
- echo "Missing service/target_env/image_tag input" >&2
- exit 1
- fi
-
- 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
-
- case "$target_env" in
- dev|prod)
- ;;
- *)
- echo "Unsupported environment: $target_env" >&2
- exit 1
- ;;
- esac
-
- if [[ ! "$image_tag" =~ ^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$ ]]; then
- echo "Unsupported image tag format: $image_tag" >&2
- exit 1
- fi
-
- kustomization="apps/${service}/overlays/${target_env}/kustomization.yaml"
-
- {
- echo "SERVICE=$service"
- echo "TARGET_ENV=$target_env"
- echo "IMAGE_TAG=$image_tag"
- echo "IMAGE_NAME=$image_name"
- echo "KUSTOMIZATION=$kustomization"
- } >> "$GITHUB_ENV"
-}
-
-update_kustomization() {
- if ! command -v kustomize >/dev/null 2>&1; then
- echo "kustomize is required to update the image tag" >&2
- exit 1
- fi
-
- if [[ ! -f "${KUSTOMIZATION:-}" ]]; then
- echo "Kustomization not found: ${KUSTOMIZATION:-}" >&2
- exit 1
- fi
-
- cd "$(dirname "$KUSTOMIZATION")"
- kustomize edit set image "${IMAGE_NAME}=${IMAGE_NAME}:${IMAGE_TAG}"
-}
-
-validate_overlay() {
- if ! command -v kubectl >/dev/null 2>&1; then
- echo "kubectl is required to validate the overlay" >&2
- exit 1
- fi
-
- kubectl kustomize "$(dirname "$KUSTOMIZATION")" >/dev/null
-}
-
-check_changes() {
- if git diff --quiet -- "$KUSTOMIZATION"; then
- echo "changed=false" >> "$GITHUB_OUTPUT"
- echo "No changes to commit"
- else
- echo "changed=true" >> "$GITHUB_OUTPUT"
- git diff -- "$KUSTOMIZATION"
- fi
-}
-
-commit_dev() {
- git config user.name "github-actions[bot]"
- git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- git add "$KUSTOMIZATION"
- git commit -m "chore(gitops): update ${SERVICE} ${TARGET_ENV} image to ${IMAGE_TAG}"
- git push origin "HEAD:${DEFAULT_BRANCH}"
-}
-
-main() {
- case "${1:-}" in
- resolve-inputs)
- resolve_inputs
- ;;
- update-kustomization)
- update_kustomization
- ;;
- validate-overlay)
- validate_overlay
- ;;
- check-changes)
- check_changes
- ;;
- commit-dev)
- commit_dev
- ;;
- *)
- echo "Usage: scripts/ci/update-image-tag.sh " >&2
- exit 1
- ;;
- esac
-}
-
-main "$@"
diff --git a/scripts/vault-transit/dev/bootstrap-runbook.sh b/scripts/vault-transit/dev/bootstrap-runbook.sh
deleted file mode 100755
index 44448c6..0000000
--- a/scripts/vault-transit/dev/bootstrap-runbook.sh
+++ /dev/null
@@ -1,55 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
-
-source "${SCRIPT_DIR}/../../vault/dev/provider-lib.sh"
-
-if ! command -v terraform >/dev/null 2>&1; then
- echo "terraform CLI is required" >&2
- exit 1
-fi
-
-export VAULT_TRANSIT_ADDR="${VAULT_TRANSIT_ADDR:-${VAULT_ADDR:-http://127.0.0.1:18200}}"
-export TF_VAR_vault_addr="$VAULT_TRANSIT_ADDR"
-export TF_VAR_vault_token="$(provider_vault_token)"
-
-if [[ -n "${SEAL_KEY_NAME:-}" ]]; then
- export TF_VAR_seal_key_name="$SEAL_KEY_NAME"
-fi
-
-if [[ -n "${SEAL_TOKEN_PERIOD:-}" ]]; then
- export TF_VAR_seal_token_period="$SEAL_TOKEN_PERIOD"
-fi
-
-if [[ -n "${TARGET_NAMESPACE:-}" ]]; then
- export TF_VAR_target_namespace="$TARGET_NAMESPACE"
-fi
-
-if [[ -n "${TARGET_SECRET_NAME:-}" ]]; then
- export TF_VAR_target_secret_name="$TARGET_SECRET_NAME"
-fi
-
-if [[ -n "${WORKFLOW_POLICY_NAME:-}" ]]; then
- export TF_VAR_workflow_policy_name="$WORKFLOW_POLICY_NAME"
-fi
-
-if [[ -n "${WORKFLOW_ROLE_NAME:-}" ]]; then
- export TF_VAR_workflow_role_name="$WORKFLOW_ROLE_NAME"
-fi
-
-mkdir -p "${REPO_ROOT}/.terraform-state"
-
-if [[ -n "${TF_STATE_DIR:-}" ]]; then
- mkdir -p "${TF_STATE_DIR}"
- terraform -chdir="${REPO_ROOT}/terraform/vault-transit/dev" init \
- -input=false \
- -reconfigure \
- -backend-config="path=${TF_STATE_DIR}/vault-transit-dev.tfstate"
-else
- terraform -chdir="${REPO_ROOT}/terraform/vault-transit/dev" init -input=false
-fi
-
-terraform -chdir="${REPO_ROOT}/terraform/vault-transit/dev" apply -input=false -auto-approve
diff --git a/scripts/vault-transit/dev/ensure-unsealed.sh b/scripts/vault-transit/dev/ensure-unsealed.sh
deleted file mode 100755
index 217719b..0000000
--- a/scripts/vault-transit/dev/ensure-unsealed.sh
+++ /dev/null
@@ -1,41 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:18200}"
-
-export VAULT_ADDR
-
-if ! command -v vault >/dev/null 2>&1; then
- echo "vault CLI is required" >&2
- exit 1
-fi
-
-status_json="$(mktemp)"
-trap 'rm -f "$status_json"' EXIT
-
-if vault status -format=json >"$status_json" 2>/dev/null; then
- :
-else
- if [[ ! -s "$status_json" ]]; then
- echo "Vault transit provider is unreachable at ${VAULT_ADDR}" >&2
- exit 1
- fi
-fi
-
-initialized="$(jq -r '.initialized' "$status_json")"
-sealed="$(jq -r '.sealed' "$status_json")"
-
-if [[ "$initialized" != "true" ]]; then
- echo "Transit provider Vault is not initialized." >&2
- exit 1
-fi
-
-if [[ "$sealed" == "true" ]]; then
- cat >&2 <<'EOF'
-Transit provider Vault is sealed.
-Manual unseal is required before the CI reconcile workflow can continue.
-Run the provider Vault runbook from runbooks/vault-transit/dev/README.md and unseal the vault-transit instance first.
-EOF
- exit 1
-fi
diff --git a/scripts/vault-transit/dev/populate-workload-seeds.sh b/scripts/vault-transit/dev/populate-workload-seeds.sh
deleted file mode 100755
index 4db5715..0000000
--- a/scripts/vault-transit/dev/populate-workload-seeds.sh
+++ /dev/null
@@ -1,90 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-if ! command -v vault >/dev/null 2>&1; then
- echo "vault CLI is required" >&2
- exit 1
-fi
-
-prompt_secret() {
- local var_name="$1"
- local label="$2"
- local value=""
- local confirm=""
-
- while true; do
- read -r -s -p "${label}: " value
- echo
- read -r -s -p "${label} (confirm): " confirm
- echo
-
- if [[ "$value" != "$confirm" ]]; then
- echo "Values did not match. Try again." >&2
- continue
- fi
-
- if [[ -z "$value" ]]; then
- echo "Value must not be empty." >&2
- continue
- fi
-
- printf -v "$var_name" '%s' "$value"
- break
- done
-}
-
-prompt_value() {
- local var_name="$1"
- local label="$2"
- local default_value="${3:-}"
- local value=""
-
- if [[ -n "$default_value" ]]; then
- read -r -p "${label} [${default_value}]: " value
- value="${value:-$default_value}"
- else
- read -r -p "${label}: " value
- fi
-
- if [[ -z "$value" ]]; then
- echo "Value must not be empty." >&2
- exit 1
- fi
-
- printf -v "$var_name" '%s' "$value"
-}
-
-echo "Populate provider Vault workload seeds"
-echo "VAULT_ADDR=${VAULT_ADDR:-unset}"
-echo "This script prompts securely so values are not exposed in shell history."
-echo "The workload workflow AppRole bootstrap path is managed by Terraform."
-echo
-
-prompt_secret PLATFORM_POSTGRES_SUPERUSER_PASSWORD "Platform Postgres superuser password"
-prompt_value AUTH_SERVER_DATASOURCE_USERNAME "Postgres auth-server username" "project_auth"
-prompt_secret AUTH_SERVER_DATASOURCE_PASSWORD "Postgres auth-server password"
-prompt_secret PLATFORM_KEYCLOAK_DB_PASSWORD "Postgres keycloak password"
-prompt_secret PLATFORM_KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD "Platform Keycloak bootstrap admin password"
-prompt_secret AUTH_SERVER_KEYCLOAK_CLIENT_SECRET "Keycloak auth-server client secret"
-
-vault kv put kv/dev/workload/platform/postgres/superuser \
- POSTGRES_SUPERUSER_PASSWORD="$PLATFORM_POSTGRES_SUPERUSER_PASSWORD" >/dev/null
-
-vault kv put kv/dev/workload/platform/postgres/auth-server \
- APP_DATASOURCE_USERNAME="$AUTH_SERVER_DATASOURCE_USERNAME" \
- APP_DATASOURCE_PASSWORD="$AUTH_SERVER_DATASOURCE_PASSWORD" \
- AUTH_DB_PASSWORD="$AUTH_SERVER_DATASOURCE_PASSWORD" >/dev/null
-
-vault kv put kv/dev/workload/platform/postgres/keycloak \
- KEYCLOAK_DB_PASSWORD="$PLATFORM_KEYCLOAK_DB_PASSWORD" >/dev/null
-
-vault kv put kv/dev/workload/platform/keycloak/bootstrap-admin \
- KC_BOOTSTRAP_ADMIN_PASSWORD="$PLATFORM_KEYCLOAK_BOOTSTRAP_ADMIN_PASSWORD" >/dev/null
-
-vault kv put kv/dev/workload/platform/keycloak/client-auth-server \
- APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET="$AUTH_SERVER_KEYCLOAK_CLIENT_SECRET" \
- KEYCLOAK_CLIENT_SECRET="$AUTH_SERVER_KEYCLOAK_CLIENT_SECRET" >/dev/null
-
-echo
-echo "Provider Vault workload seed values updated."
diff --git a/scripts/vault/dev/apply-argocd-dev-apps.sh b/scripts/vault/dev/apply-argocd-dev-apps.sh
deleted file mode 100755
index 63bbd1f..0000000
--- a/scripts/vault/dev/apply-argocd-dev-apps.sh
+++ /dev/null
@@ -1,14 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-for cmd in kubectl; do
- if ! command -v "$cmd" >/dev/null 2>&1; then
- echo "$cmd is required" >&2
- exit 1
- fi
-done
-
-kubectl apply -f argocd/projects/dev
-kubectl apply -f argocd/applications/dev/infra/platform.yaml
-kubectl apply -f argocd/applications/dev/apps
diff --git a/scripts/vault/dev/apply-argocd-dev-infra.sh b/scripts/vault/dev/apply-argocd-dev-infra.sh
deleted file mode 100755
index 691a641..0000000
--- a/scripts/vault/dev/apply-argocd-dev-infra.sh
+++ /dev/null
@@ -1,16 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-for cmd in kubectl; do
- if ! command -v "$cmd" >/dev/null 2>&1; then
- echo "$cmd is required" >&2
- exit 1
- fi
-done
-
-kubectl apply -f argocd/projects/dev
-kubectl apply -f argocd/applications/dev/infra/sealed-secrets.yaml
-kubectl apply -f argocd/applications/dev/infra/vault-transit.yaml
-kubectl apply -f argocd/applications/dev/infra/vault.yaml
-kubectl apply -f argocd/applications/dev/infra/vault-agent-injector.yaml
diff --git a/scripts/vault/dev/bootstrap-runbook.sh b/scripts/vault/dev/bootstrap-runbook.sh
deleted file mode 100755
index 162464a..0000000
--- a/scripts/vault/dev/bootstrap-runbook.sh
+++ /dev/null
@@ -1,35 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
-REPO_ROOT="$(cd "${SCRIPT_DIR}/../../.." && pwd)"
-
-source "${SCRIPT_DIR}/provider-lib.sh"
-source "${SCRIPT_DIR}/workload-auth-lib.sh"
-
-if ! command -v terraform >/dev/null 2>&1; then
- echo "terraform CLI is required" >&2
- exit 1
-fi
-
-export VAULT_WORKLOAD_ADDR="${VAULT_WORKLOAD_ADDR:-${VAULT_ADDR:-http://127.0.0.1:8200}}"
-export VAULT_TRANSIT_ADDR="${VAULT_TRANSIT_ADDR:-http://127.0.0.1:18200}"
-export TF_VAR_workload_vault_addr="$VAULT_WORKLOAD_ADDR"
-export TF_VAR_workload_vault_token="$(workload_vault_token)"
-export TF_VAR_transit_vault_addr="$VAULT_TRANSIT_ADDR"
-export TF_VAR_transit_vault_token="$(provider_vault_token)"
-
-mkdir -p "${REPO_ROOT}/.terraform-state"
-
-if [[ -n "${TF_STATE_DIR:-}" ]]; then
- mkdir -p "${TF_STATE_DIR}"
- terraform -chdir="${REPO_ROOT}/terraform/vault/dev" init \
- -input=false \
- -reconfigure \
- -backend-config="path=${TF_STATE_DIR}/vault-dev.tfstate"
-else
- terraform -chdir="${REPO_ROOT}/terraform/vault/dev" init -input=false
-fi
-
-terraform -chdir="${REPO_ROOT}/terraform/vault/dev" apply -input=false -auto-approve
diff --git a/scripts/vault/dev/ensure-unsealed.sh b/scripts/vault/dev/ensure-unsealed.sh
deleted file mode 100755
index f37e0bc..0000000
--- a/scripts/vault/dev/ensure-unsealed.sh
+++ /dev/null
@@ -1,37 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
-
-export VAULT_ADDR
-
-if ! command -v vault >/dev/null 2>&1; then
- echo "vault CLI is required" >&2
- exit 1
-fi
-
-status_json="$(mktemp)"
-trap 'rm -f "$status_json"' EXIT
-
-if vault status -format=json >"$status_json" 2>/dev/null; then
- :
-else
- if [[ ! -s "$status_json" ]]; then
- echo "Vault is unreachable at ${VAULT_ADDR}" >&2
- exit 1
- fi
-fi
-
-initialized="$(jq -r '.initialized' "$status_json")"
-sealed="$(jq -r '.sealed' "$status_json")"
-
-if [[ "$initialized" != "true" ]]; then
- echo "Workload Vault is not initialized." >&2
- exit 1
-fi
-
-if [[ "$sealed" == "true" ]]; then
- echo "Workload Vault is still sealed. Check the transit unseal provider and vault-transit-seal secret." >&2
- exit 1
-fi
diff --git a/scripts/vault/dev/issue-operator-tokens.sh b/scripts/vault/dev/issue-operator-tokens.sh
deleted file mode 100755
index bf48176..0000000
--- a/scripts/vault/dev/issue-operator-tokens.sh
+++ /dev/null
@@ -1,38 +0,0 @@
-#!/usr/bin/env bash
-
-set -euo pipefail
-
-VAULT_ADDR="${VAULT_ADDR:-http://127.0.0.1:8200}"
-VAULT_TOKEN="${VAULT_TOKEN:-}"
-TOKEN_TTL="${TOKEN_TTL:-1h}"
-
-if [[ -z "$VAULT_TOKEN" ]]; then
- echo "VAULT_TOKEN must be set to a platform-admin-dev token." >&2
- exit 1
-fi
-
-for cmd in vault; do
- if ! command -v "$cmd" >/dev/null 2>&1; then
- echo "$cmd is required" >&2
- exit 1
- fi
-done
-
-export VAULT_ADDR
-export VAULT_TOKEN
-
-postgres_token="$(
- vault token create -orphan -policy=postgres-operator-dev -ttl="$TOKEN_TTL" -field=token
-)"
-
-keycloak_token="$(
- vault token create -orphan -policy=keycloak-operator-dev -ttl="$TOKEN_TTL" -field=token
-)"
-
-echo ""
-echo "postgres-operator-dev token:"
-echo "$postgres_token"
-echo ""
-echo "keycloak-operator-dev token:"
-echo "$keycloak_token"
-echo ""
diff --git a/scripts/vault/dev/provider-lib.sh b/scripts/vault/dev/provider-lib.sh
deleted file mode 100644
index 8b4de79..0000000
--- a/scripts/vault/dev/provider-lib.sh
+++ /dev/null
@@ -1,67 +0,0 @@
-#!/usr/bin/env bash
-
-__provider_vault_token_cache="${__provider_vault_token_cache:-}"
-
-provider_vault_addr() {
- printf '%s\n' "${VAULT_TRANSIT_ADDR:-http://127.0.0.1:18200}"
-}
-
-provider_vault_login_with_approle() {
- local role_id="$1"
- local secret_id="$2"
-
- if [[ -n "$__provider_vault_token_cache" ]]; then
- printf '%s\n' "$__provider_vault_token_cache"
- return 0
- fi
-
- __provider_vault_token_cache="$(
- VAULT_ADDR="$(provider_vault_addr)" \
- vault write -field=token auth/approle/login \
- role_id="$role_id" \
- secret_id="$secret_id"
- )"
-
- printf '%s\n' "$__provider_vault_token_cache"
-}
-
-provider_vault_token() {
- if [[ -n "${VAULT_TRANSIT_DEV_ROLE_ID:-}" && -n "${VAULT_TRANSIT_DEV_SECRET_ID:-}" ]]; then
- provider_vault_login_with_approle \
- "$VAULT_TRANSIT_DEV_ROLE_ID" \
- "$VAULT_TRANSIT_DEV_SECRET_ID"
- return 0
- fi
-
- if [[ -n "${VAULT_TRANSIT_DEV_BOOTSTRAP_TOKEN:-}" ]]; then
- printf '%s\n' "$VAULT_TRANSIT_DEV_BOOTSTRAP_TOKEN"
- return 0
- fi
-
- if [[ -n "${VAULT_TOKEN:-}" ]]; then
- printf '%s\n' "$VAULT_TOKEN"
- return 0
- fi
-
- echo "VAULT_TRANSIT_DEV_BOOTSTRAP_TOKEN or VAULT_TRANSIT_DEV_ROLE_ID/VAULT_TRANSIT_DEV_SECRET_ID or VAULT_TOKEN must be set." >&2
- return 1
-}
-
-provider_kv_get_json() {
- local path="$1"
- VAULT_ADDR="$(provider_vault_addr)" \
- VAULT_TOKEN="$(provider_vault_token)" \
- vault kv get -format=json "$path"
-}
-
-provider_read_workload_bootstrap_token() {
- provider_kv_get_json "kv/dev/workload/bootstrap" | jq -r '.data.data.VAULT_WORKLOAD_DEV_BOOTSTRAP_TOKEN // empty'
-}
-
-provider_read_workload_role_id() {
- provider_kv_get_json "kv/dev/workload/bootstrap" | jq -r '.data.data.VAULT_WORKLOAD_DEV_ROLE_ID // empty'
-}
-
-provider_read_workload_secret_id() {
- provider_kv_get_json "kv/dev/workload/bootstrap" | jq -r '.data.data.VAULT_WORKLOAD_DEV_SECRET_ID // empty'
-}
diff --git a/scripts/vault/dev/workload-auth-lib.sh b/scripts/vault/dev/workload-auth-lib.sh
deleted file mode 100644
index 3c5b828..0000000
--- a/scripts/vault/dev/workload-auth-lib.sh
+++ /dev/null
@@ -1,61 +0,0 @@
-#!/usr/bin/env bash
-
-__workload_vault_token_cache="${__workload_vault_token_cache:-}"
-
-workload_vault_login_with_approle() {
- local role_id="$1"
- local secret_id="$2"
-
- if [[ -n "$__workload_vault_token_cache" ]]; then
- printf '%s\n' "$__workload_vault_token_cache"
- return 0
- fi
-
- __workload_vault_token_cache="$(
- VAULT_ADDR="${VAULT_WORKLOAD_ADDR:-${VAULT_ADDR:-http://127.0.0.1:8200}}" \
- vault write -field=token auth/approle/login \
- role_id="$role_id" \
- secret_id="$secret_id"
- )"
-
- printf '%s\n' "$__workload_vault_token_cache"
-}
-
-workload_vault_token() {
- local role_id=""
- local secret_id=""
- local token=""
-
- if [[ -n "${VAULT_WORKLOAD_DEV_BOOTSTRAP_TOKEN:-}" ]]; then
- printf '%s\n' "$VAULT_WORKLOAD_DEV_BOOTSTRAP_TOKEN"
- return 0
- fi
-
- if [[ -n "${VAULT_WORKLOAD_DEV_ROLE_ID:-}" && -n "${VAULT_WORKLOAD_DEV_SECRET_ID:-}" ]]; then
- workload_vault_login_with_approle \
- "$VAULT_WORKLOAD_DEV_ROLE_ID" \
- "$VAULT_WORKLOAD_DEV_SECRET_ID"
- return 0
- fi
-
- role_id="$(provider_read_workload_role_id 2>/dev/null || true)"
- secret_id="$(provider_read_workload_secret_id 2>/dev/null || true)"
- if [[ -n "$role_id" && -n "$secret_id" ]]; then
- workload_vault_login_with_approle "$role_id" "$secret_id"
- return 0
- fi
-
- token="$(provider_read_workload_bootstrap_token 2>/dev/null || true)"
- if [[ -n "$token" ]]; then
- printf '%s\n' "$token"
- return 0
- fi
-
- if [[ -n "${VAULT_TOKEN:-}" ]]; then
- printf '%s\n' "$VAULT_TOKEN"
- return 0
- fi
-
- echo "VAULT_WORKLOAD_DEV_ROLE_ID/VAULT_WORKLOAD_DEV_SECRET_ID or VAULT_WORKLOAD_DEV_BOOTSTRAP_TOKEN or VAULT_TOKEN must be set." >&2
- return 1
-}
diff --git a/terraform/vault-transit/dev/.terraform.lock.hcl b/terraform/vault-transit/dev/.terraform.lock.hcl
deleted file mode 100644
index bbb7f8a..0000000
--- a/terraform/vault-transit/dev/.terraform.lock.hcl
+++ /dev/null
@@ -1,42 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/hashicorp/kubernetes" {
- version = "2.38.0"
- constraints = "~> 2.32"
- hashes = [
- "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=",
- "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0",
- "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f",
- "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b",
- "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12",
- "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2",
- "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc",
- "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15",
- "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396",
- "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d",
- "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4",
- "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
- "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db",
- ]
-}
-
-provider "registry.terraform.io/hashicorp/vault" {
- version = "4.8.0"
- constraints = "~> 4.8.0"
- hashes = [
- "h1:aHqgWQhDBMeZO9iUKwJYMlh4q+xNMUlMIcjRbF4d02Y=",
- "zh:269ab13433f67684012ae7e15876532b0312f5d0d2002a9cf9febb1279ce5ea6",
- "zh:4babc95bf0c40eb85005db1dc2ca403c46be4a71dd3e409db3711a56f7a5ca0e",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:86e27c1c625ecc24446a11eeffc3ac319b36c2b4e51251db8579256a0dbcf136",
- "zh:a32f31da94824009e26b077374440b52098aecb93c92ff55dc3d31dd37c4ea25",
- "zh:be0a18c6c0425518bab4fbffd82078b82036a88503b5d76064de551c9f646cbf",
- "zh:be5a77fdfd36863ebeec79cd12b1d13322ffad6821d157a0b279789fa06b5937",
- "zh:be8317d142a3caad74c7d936039ae27076a1b2b8312ef5208e2871a5f525977c",
- "zh:c94a84895a3d9954b80e983eed4603330a5cdbbd8eef5b3c99278c2d1402ef3c",
- "zh:de1fb712784dd8415f011ca5346a34f87fab6046c730557615247e511dbc7d98",
- "zh:e3eafae7da550f86cae395d6660b2a0e93ec8d2b0e0e5ef982ec762e961fc952",
- "zh:ff35fb1ab6add288f0f368981e56f780b50405accd1937131cba1137999c8d83",
- ]
-}
diff --git a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/kubernetes/2.38.0/linux_amd64/LICENSE.txt b/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/kubernetes/2.38.0/linux_amd64/LICENSE.txt
deleted file mode 100644
index b9ac071..0000000
--- a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/kubernetes/2.38.0/linux_amd64/LICENSE.txt
+++ /dev/null
@@ -1,375 +0,0 @@
-Copyright (c) 2017 HashiCorp, Inc.
-
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
- means each individual or legal entity that creates, contributes to
- the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
- means the combination of the Contributions of others (if any) used
- by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
- means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
- means Source Code Form to which the initial Contributor has attached
- the notice in Exhibit A, the Executable Form of such Source Code
- Form, and Modifications of such Source Code Form, in each case
- including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
- means
-
- (a) that the initial Contributor has attached the notice described
- in Exhibit B to the Covered Software; or
-
- (b) that the Covered Software was made available under the terms of
- version 1.1 or earlier of the License, but not also under the
- terms of a Secondary License.
-
-1.6. "Executable Form"
- means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
- a separate file or files, that is not Covered Software.
-
-1.8. "License"
- means this document.
-
-1.9. "Licensable"
- means having the right to grant, to the maximum extent possible,
- whether at the time of the initial grant or subsequently, any and
- all of the rights conveyed by this License.
-
-1.10. "Modifications"
- means any of the following:
-
- (a) any file in Source Code Form that results from an addition to,
- deletion from, or modification of the contents of Covered
- Software; or
-
- (b) any new file in Source Code Form that contains any Covered
- Software.
-
-1.11. "Patent Claims" of a Contributor
- means any patent claim(s), including without limitation, method,
- process, and apparatus claims, in any patent Licensable by such
- Contributor that would be infringed, but for the grant of the
- License, by the making, using, selling, offering for sale, having
- made, import, or transfer of either its Contributions or its
- Contributor Version.
-
-1.12. "Secondary License"
- means either the GNU General Public License, Version 2.0, the GNU
- Lesser General Public License, Version 2.1, the GNU Affero General
- Public License, Version 3.0, or any later versions of those
- licenses.
-
-1.13. "Source Code Form"
- means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
- means an individual or a legal entity exercising rights under this
- License. For legal entities, "You" includes any entity that
- controls, is controlled by, or is under common control with You. For
- purposes of this definition, "control" means (a) the power, direct
- or indirect, to cause the direction or management of such entity,
- whether by contract or otherwise, or (b) ownership of more than
- fifty percent (50%) of the outstanding shares or beneficial
- ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
- Licensable by such Contributor to use, reproduce, make available,
- modify, display, perform, distribute, and otherwise exploit its
- Contributions, either on an unmodified basis, with Modifications, or
- as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
- for sale, have made, import, and otherwise transfer either its
- Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
- or
-
-(b) for infringements caused by: (i) Your and any other third party's
- modifications of Covered Software, or (ii) the combination of its
- Contributions with other software (except as part of its Contributor
- Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
- its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
- Form, as described in Section 3.1, and You must inform recipients of
- the Executable Form how they can obtain a copy of such Source Code
- Form by reasonable means in a timely manner, at a charge no more
- than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
- License, or sublicense it under different terms, provided that the
- license for the Executable Form does not attempt to limit or alter
- the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-* *
-* 6. Disclaimer of Warranty *
-* ------------------------- *
-* *
-* Covered Software is provided under this License on an "as is" *
-* basis, without warranty of any kind, either expressed, implied, or *
-* statutory, including, without limitation, warranties that the *
-* Covered Software is free of defects, merchantable, fit for a *
-* particular purpose or non-infringing. The entire risk as to the *
-* quality and performance of the Covered Software is with You. *
-* Should any Covered Software prove defective in any respect, You *
-* (not any Contributor) assume the cost of any necessary servicing, *
-* repair, or correction. This disclaimer of warranty constitutes an *
-* essential part of this License. No use of any Covered Software is *
-* authorized under this License except under this disclaimer. *
-* *
-************************************************************************
-
-************************************************************************
-* *
-* 7. Limitation of Liability *
-* -------------------------- *
-* *
-* Under no circumstances and under no legal theory, whether tort *
-* (including negligence), contract, or otherwise, shall any *
-* Contributor, or anyone who distributes Covered Software as *
-* permitted above, be liable to You for any direct, indirect, *
-* special, incidental, or consequential damages of any character *
-* including, without limitation, damages for lost profits, loss of *
-* goodwill, work stoppage, computer failure or malfunction, or any *
-* and all other commercial damages or losses, even if such party *
-* shall have been informed of the possibility of such damages. This *
-* limitation of liability shall not apply to liability for death or *
-* personal injury resulting from such party's negligence to the *
-* extent applicable law prohibits such limitation. Some *
-* jurisdictions do not allow the exclusion or limitation of *
-* incidental or consequential damages, so this exclusion and *
-* limitation may not apply to You. *
-* *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
- This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
diff --git a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/kubernetes/2.38.0/linux_amd64/terraform-provider-kubernetes_v2.38.0_x5 b/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/kubernetes/2.38.0/linux_amd64/terraform-provider-kubernetes_v2.38.0_x5
deleted file mode 100755
index a02e3e5..0000000
Binary files a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/kubernetes/2.38.0/linux_amd64/terraform-provider-kubernetes_v2.38.0_x5 and /dev/null differ
diff --git a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/LICENSE.txt b/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/LICENSE.txt
deleted file mode 100644
index b9ac071..0000000
--- a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/LICENSE.txt
+++ /dev/null
@@ -1,375 +0,0 @@
-Copyright (c) 2017 HashiCorp, Inc.
-
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
- means each individual or legal entity that creates, contributes to
- the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
- means the combination of the Contributions of others (if any) used
- by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
- means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
- means Source Code Form to which the initial Contributor has attached
- the notice in Exhibit A, the Executable Form of such Source Code
- Form, and Modifications of such Source Code Form, in each case
- including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
- means
-
- (a) that the initial Contributor has attached the notice described
- in Exhibit B to the Covered Software; or
-
- (b) that the Covered Software was made available under the terms of
- version 1.1 or earlier of the License, but not also under the
- terms of a Secondary License.
-
-1.6. "Executable Form"
- means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
- a separate file or files, that is not Covered Software.
-
-1.8. "License"
- means this document.
-
-1.9. "Licensable"
- means having the right to grant, to the maximum extent possible,
- whether at the time of the initial grant or subsequently, any and
- all of the rights conveyed by this License.
-
-1.10. "Modifications"
- means any of the following:
-
- (a) any file in Source Code Form that results from an addition to,
- deletion from, or modification of the contents of Covered
- Software; or
-
- (b) any new file in Source Code Form that contains any Covered
- Software.
-
-1.11. "Patent Claims" of a Contributor
- means any patent claim(s), including without limitation, method,
- process, and apparatus claims, in any patent Licensable by such
- Contributor that would be infringed, but for the grant of the
- License, by the making, using, selling, offering for sale, having
- made, import, or transfer of either its Contributions or its
- Contributor Version.
-
-1.12. "Secondary License"
- means either the GNU General Public License, Version 2.0, the GNU
- Lesser General Public License, Version 2.1, the GNU Affero General
- Public License, Version 3.0, or any later versions of those
- licenses.
-
-1.13. "Source Code Form"
- means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
- means an individual or a legal entity exercising rights under this
- License. For legal entities, "You" includes any entity that
- controls, is controlled by, or is under common control with You. For
- purposes of this definition, "control" means (a) the power, direct
- or indirect, to cause the direction or management of such entity,
- whether by contract or otherwise, or (b) ownership of more than
- fifty percent (50%) of the outstanding shares or beneficial
- ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
- Licensable by such Contributor to use, reproduce, make available,
- modify, display, perform, distribute, and otherwise exploit its
- Contributions, either on an unmodified basis, with Modifications, or
- as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
- for sale, have made, import, and otherwise transfer either its
- Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
- or
-
-(b) for infringements caused by: (i) Your and any other third party's
- modifications of Covered Software, or (ii) the combination of its
- Contributions with other software (except as part of its Contributor
- Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
- its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
- Form, as described in Section 3.1, and You must inform recipients of
- the Executable Form how they can obtain a copy of such Source Code
- Form by reasonable means in a timely manner, at a charge no more
- than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
- License, or sublicense it under different terms, provided that the
- license for the Executable Form does not attempt to limit or alter
- the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-* *
-* 6. Disclaimer of Warranty *
-* ------------------------- *
-* *
-* Covered Software is provided under this License on an "as is" *
-* basis, without warranty of any kind, either expressed, implied, or *
-* statutory, including, without limitation, warranties that the *
-* Covered Software is free of defects, merchantable, fit for a *
-* particular purpose or non-infringing. The entire risk as to the *
-* quality and performance of the Covered Software is with You. *
-* Should any Covered Software prove defective in any respect, You *
-* (not any Contributor) assume the cost of any necessary servicing, *
-* repair, or correction. This disclaimer of warranty constitutes an *
-* essential part of this License. No use of any Covered Software is *
-* authorized under this License except under this disclaimer. *
-* *
-************************************************************************
-
-************************************************************************
-* *
-* 7. Limitation of Liability *
-* -------------------------- *
-* *
-* Under no circumstances and under no legal theory, whether tort *
-* (including negligence), contract, or otherwise, shall any *
-* Contributor, or anyone who distributes Covered Software as *
-* permitted above, be liable to You for any direct, indirect, *
-* special, incidental, or consequential damages of any character *
-* including, without limitation, damages for lost profits, loss of *
-* goodwill, work stoppage, computer failure or malfunction, or any *
-* and all other commercial damages or losses, even if such party *
-* shall have been informed of the possibility of such damages. This *
-* limitation of liability shall not apply to liability for death or *
-* personal injury resulting from such party's negligence to the *
-* extent applicable law prohibits such limitation. Some *
-* jurisdictions do not allow the exclusion or limitation of *
-* incidental or consequential damages, so this exclusion and *
-* limitation may not apply to You. *
-* *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
- This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
diff --git a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/terraform-provider-vault_v4.8.0_x5 b/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/terraform-provider-vault_v4.8.0_x5
deleted file mode 100755
index 60f3e12..0000000
Binary files a/terraform/vault-transit/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/terraform-provider-vault_v4.8.0_x5 and /dev/null differ
diff --git a/terraform/vault-transit/dev/.terraform/terraform.tfstate b/terraform/vault-transit/dev/.terraform/terraform.tfstate
deleted file mode 100644
index 30d7688..0000000
--- a/terraform/vault-transit/dev/.terraform/terraform.tfstate
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "version": 3,
- "terraform_version": "1.14.8",
- "backend": {
- "type": "local",
- "config": {
- "path": "../../../.terraform-state/vault-transit-dev.tfstate",
- "workspace_dir": null
- },
- "hash": 2685574802
- }
-}
\ No newline at end of file
diff --git a/terraform/vault-transit/dev/main.tf b/terraform/vault-transit/dev/main.tf
deleted file mode 100644
index 8fe87f2..0000000
--- a/terraform/vault-transit/dev/main.tf
+++ /dev/null
@@ -1,141 +0,0 @@
-terraform {
- required_version = ">= 1.6.0"
-
- required_providers {
- kubernetes = {
- source = "hashicorp/kubernetes"
- version = "~> 2.32"
- }
- vault = {
- source = "hashicorp/vault"
- version = "~> 4.8.0"
- }
- }
-
- backend "local" {
- path = "../../../.terraform-state/vault-transit-dev.tfstate"
- }
-}
-
-provider "vault" {
- address = var.vault_addr
- skip_child_token = true
- token = var.vault_token
-}
-
-provider "kubernetes" {
- config_path = var.kubeconfig_path
-}
-
-locals {
- workflow_policy_path = "${path.module}/../../../runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl"
- workload_policy_path = "${path.module}/../../../runbooks/vault-transit/dev/policies/workload-vault-transit-dev.hcl"
- admin_policy_path = "${path.module}/../../../runbooks/vault-transit/dev/policies/vault-transit-admin-dev.hcl"
-}
-
-resource "vault_mount" "kv" {
- path = var.kv_mount_path
- type = "kv"
- options = {
- version = "2"
- }
-
- lifecycle {
- prevent_destroy = true
- ignore_changes = [type, options]
- }
-}
-
-resource "vault_mount" "transit" {
- path = var.transit_mount_path
- type = "transit"
-
- lifecycle {
- prevent_destroy = true
- }
-}
-
-resource "vault_transit_secret_backend_key" "workload_unseal" {
- backend = vault_mount.transit.path
- name = var.seal_key_name
- type = "aes256-gcm96"
-}
-
-resource "vault_policy" "workload_vault_transit_dev" {
- name = var.workload_policy_name
- policy = file(local.workload_policy_path)
-}
-
-resource "vault_policy" "vault_transit_admin_dev" {
- name = var.admin_policy_name
- policy = file(local.admin_policy_path)
-}
-
-resource "vault_policy" "vault_transit_automation_dev" {
- name = var.workflow_policy_name
- policy = file(local.workflow_policy_path)
-}
-
-resource "vault_auth_backend" "approle" {
- path = var.approle_auth_path
- type = "approle"
-}
-
-resource "vault_approle_auth_backend_role" "workflow" {
- backend = vault_auth_backend.approle.path
- role_name = var.workflow_role_name
- secret_id_num_uses = 0
- secret_id_ttl = 0
- token_max_ttl = var.workflow_token_max_ttl_seconds
- token_policies = [vault_policy.vault_transit_automation_dev.name]
- token_ttl = var.workflow_token_ttl_seconds
-}
-
-resource "vault_approle_auth_backend_role_secret_id" "workflow" {
- backend = vault_auth_backend.approle.path
- role_name = vault_approle_auth_backend_role.workflow.role_name
-}
-
-resource "vault_token" "seal" {
- display_name = "workload-vault-dev-unseal"
- no_parent = true
- period = var.seal_token_period
- policies = [vault_policy.workload_vault_transit_dev.name]
- renewable = true
-
- lifecycle {
- ignore_changes = all
- }
-}
-
-resource "kubernetes_secret_v1" "vault_transit_seal" {
- wait_for_service_account_token = true
-
- metadata {
- name = var.target_secret_name
- namespace = var.target_namespace
- }
-
- data = {
- VAULT_TRANSIT_SEAL_TOKEN = vault_token.seal.client_token
- }
-
- type = "Opaque"
-}
-
-output "workflow_role_id" {
- description = "Vault transit workflow AppRole role_id."
- value = vault_approle_auth_backend_role.workflow.role_id
-}
-
-output "workflow_secret_id" {
- description = "Vault transit workflow AppRole secret_id."
- value = vault_approle_auth_backend_role_secret_id.workflow.secret_id
- sensitive = true
-}
-
-output "vault_transit_seal_token" {
- description = "Periodic seal token written into the vault-transit-seal Kubernetes Secret."
- value = vault_token.seal.client_token
- sensitive = true
-}
diff --git a/terraform/vault-transit/dev/variables.tf b/terraform/vault-transit/dev/variables.tf
deleted file mode 100644
index d64c608..0000000
--- a/terraform/vault-transit/dev/variables.tf
+++ /dev/null
@@ -1,95 +0,0 @@
-variable "admin_policy_name" {
- description = "Name of the operator policy kept for manual vault-transit maintenance."
- type = string
- default = "vault-transit-admin-dev"
-}
-
-variable "approle_auth_path" {
- description = "Path where the AppRole auth backend is mounted."
- type = string
- default = "approle"
-}
-
-variable "kubeconfig_path" {
- description = "Path to the kubeconfig used for managing the seal Secret."
- type = string
- default = "~/.kube/config"
-}
-
-variable "kv_mount_path" {
- description = "Mount path for the provider KV-v2 engine."
- type = string
- default = "kv"
-}
-
-variable "seal_key_name" {
- description = "Transit key used by workload Vault auto-unseal."
- type = string
- default = "workload-vault-dev-unseal"
-}
-
-variable "seal_token_period" {
- description = "Periodic renewal interval for the workload auto-unseal token."
- type = string
- default = "24h"
-}
-
-variable "target_namespace" {
- description = "Namespace that receives the vault-transit seal Secret."
- type = string
- default = "vault"
-}
-
-variable "target_secret_name" {
- description = "Name of the Kubernetes Secret holding the workload auto-unseal token."
- type = string
- default = "vault-transit-seal"
-}
-
-variable "transit_mount_path" {
- description = "Mount path for the provider transit engine."
- type = string
- default = "transit"
-}
-
-variable "vault_addr" {
- description = "Address of the vault-transit API."
- type = string
- default = "http://127.0.0.1:18200"
-}
-
-variable "vault_token" {
- description = "Privileged token used to reconcile the vault-transit configuration."
- type = string
- sensitive = true
-}
-
-variable "workflow_policy_name" {
- description = "Policy granted to the vault-transit workflow AppRole."
- type = string
- default = "vault-transit-automation-dev"
-}
-
-variable "workflow_role_name" {
- description = "Name of the workflow AppRole used by CI."
- type = string
- default = "vault-transit-dev-workflow"
-}
-
-variable "workflow_token_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for the workflow AppRole login token."
- type = number
- default = 14400
-}
-
-variable "workflow_token_ttl_seconds" {
- description = "Default TTL, in seconds, for the workflow AppRole login token."
- type = number
- default = 3600
-}
-
-variable "workload_policy_name" {
- description = "Policy name granted to the workload Vault auto-unseal token."
- type = string
- default = "workload-vault-transit-dev"
-}
diff --git a/terraform/vault-transit/reconcile/.terraform.lock.hcl b/terraform/vault-transit/reconcile/.terraform.lock.hcl
deleted file mode 100644
index bbb7f8a..0000000
--- a/terraform/vault-transit/reconcile/.terraform.lock.hcl
+++ /dev/null
@@ -1,42 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/hashicorp/kubernetes" {
- version = "2.38.0"
- constraints = "~> 2.32"
- hashes = [
- "h1:5CkveFo5ynsLdzKk+Kv+r7+U9rMrNjfZPT3a0N/fhgE=",
- "zh:0af928d776eb269b192dc0ea0f8a3f0f5ec117224cd644bdacdc682300f84ba0",
- "zh:1be998e67206f7cfc4ffe77c01a09ac91ce725de0abaec9030b22c0a832af44f",
- "zh:326803fe5946023687d603f6f1bab24de7af3d426b01d20e51d4e6fbe4e7ec1b",
- "zh:4a99ec8d91193af961de1abb1f824be73df07489301d62e6141a656b3ebfff12",
- "zh:5136e51765d6a0b9e4dbcc3b38821e9736bd2136cf15e9aac11668f22db117d2",
- "zh:63fab47349852d7802fb032e4f2b6a101ee1ce34b62557a9ad0f0f0f5b6ecfdc",
- "zh:924fb0257e2d03e03e2bfe9c7b99aa73c195b1f19412ca09960001bee3c50d15",
- "zh:b63a0be5e233f8f6727c56bed3b61eb9456ca7a8bb29539fba0837f1badf1396",
- "zh:d39861aa21077f1bc899bc53e7233262e530ba8a3a2d737449b100daeb303e4d",
- "zh:de0805e10ebe4c83ce3b728a67f6b0f9d18be32b25146aa89116634df5145ad4",
- "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
- "zh:faf23e45f0090eef8ba28a8aac7ec5d4fdf11a36c40a8d286304567d71c1e7db",
- ]
-}
-
-provider "registry.terraform.io/hashicorp/vault" {
- version = "4.8.0"
- constraints = "~> 4.8.0"
- hashes = [
- "h1:aHqgWQhDBMeZO9iUKwJYMlh4q+xNMUlMIcjRbF4d02Y=",
- "zh:269ab13433f67684012ae7e15876532b0312f5d0d2002a9cf9febb1279ce5ea6",
- "zh:4babc95bf0c40eb85005db1dc2ca403c46be4a71dd3e409db3711a56f7a5ca0e",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:86e27c1c625ecc24446a11eeffc3ac319b36c2b4e51251db8579256a0dbcf136",
- "zh:a32f31da94824009e26b077374440b52098aecb93c92ff55dc3d31dd37c4ea25",
- "zh:be0a18c6c0425518bab4fbffd82078b82036a88503b5d76064de551c9f646cbf",
- "zh:be5a77fdfd36863ebeec79cd12b1d13322ffad6821d157a0b279789fa06b5937",
- "zh:be8317d142a3caad74c7d936039ae27076a1b2b8312ef5208e2871a5f525977c",
- "zh:c94a84895a3d9954b80e983eed4603330a5cdbbd8eef5b3c99278c2d1402ef3c",
- "zh:de1fb712784dd8415f011ca5346a34f87fab6046c730557615247e511dbc7d98",
- "zh:e3eafae7da550f86cae395d6660b2a0e93ec8d2b0e0e5ef982ec762e961fc952",
- "zh:ff35fb1ab6add288f0f368981e56f780b50405accd1937131cba1137999c8d83",
- ]
-}
diff --git a/terraform/vault-transit/reconcile/main.tf b/terraform/vault-transit/reconcile/main.tf
deleted file mode 100644
index 7fa32db..0000000
--- a/terraform/vault-transit/reconcile/main.tf
+++ /dev/null
@@ -1,51 +0,0 @@
-terraform {
- required_version = ">= 1.6.0"
-
- required_providers {
- vault = {
- source = "hashicorp/vault"
- version = "~> 4.8.0"
- }
- }
-
- backend "local" {
- path = "../../../.terraform-state/vault-transit-reconcile.tfstate"
- }
-}
-
-provider "vault" {
- address = var.vault_addr
- skip_child_token = true
- token = var.vault_token
-}
-
-locals {
- workflow_policy_path = "${path.module}/../../../runbooks/vault-transit/dev/policies/vault-transit-automation-dev.hcl"
- workload_policy_path = "${path.module}/../../../runbooks/vault-transit/dev/policies/workload-vault-transit-dev.hcl"
- admin_policy_path = "${path.module}/../../../runbooks/vault-transit/dev/policies/vault-transit-admin-dev.hcl"
-}
-
-resource "vault_policy" "workload_vault_transit_dev" {
- name = var.workload_policy_name
- policy = file(local.workload_policy_path)
-}
-
-resource "vault_policy" "vault_transit_admin_dev" {
- name = var.admin_policy_name
- policy = file(local.admin_policy_path)
-}
-
-resource "vault_policy" "vault_transit_automation_dev" {
- name = var.workflow_policy_name
- policy = file(local.workflow_policy_path)
-}
-
-resource "vault_approle_auth_backend_role" "workflow" {
- backend = var.approle_auth_path
- role_name = var.workflow_role_name
- secret_id_num_uses = 0
- secret_id_ttl = 0
- token_max_ttl = var.workflow_token_max_ttl_seconds
- token_policies = [vault_policy.vault_transit_automation_dev.name]
- token_ttl = var.workflow_token_ttl_seconds
-}
diff --git a/terraform/vault-transit/reconcile/variables.tf b/terraform/vault-transit/reconcile/variables.tf
deleted file mode 100644
index 48a3e95..0000000
--- a/terraform/vault-transit/reconcile/variables.tf
+++ /dev/null
@@ -1,53 +0,0 @@
-variable "admin_policy_name" {
- description = "Name of the operator policy kept for manual vault-transit maintenance."
- type = string
- default = "vault-transit-admin-dev"
-}
-
-variable "approle_auth_path" {
- description = "Path where the AppRole auth backend is mounted."
- type = string
- default = "approle"
-}
-
-variable "vault_addr" {
- description = "Address of the vault-transit API."
- type = string
- default = "http://127.0.0.1:18200"
-}
-
-variable "vault_token" {
- description = "Workflow token used to reconcile vault-transit policies and workflow AppRole."
- type = string
- sensitive = true
-}
-
-variable "workflow_policy_name" {
- description = "Policy granted to the vault-transit workflow AppRole."
- type = string
- default = "vault-transit-automation-dev"
-}
-
-variable "workflow_role_name" {
- description = "Name of the workflow AppRole used by CI."
- type = string
- default = "vault-transit-dev-workflow"
-}
-
-variable "workflow_token_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for the workflow AppRole login token."
- type = number
- default = 14400
-}
-
-variable "workflow_token_ttl_seconds" {
- description = "Default TTL, in seconds, for the workflow AppRole login token."
- type = number
- default = 3600
-}
-
-variable "workload_policy_name" {
- description = "Policy name granted to the workload Vault auto-unseal token."
- type = string
- default = "workload-vault-transit-dev"
-}
diff --git a/terraform/vault/dev/.terraform.lock.hcl b/terraform/vault/dev/.terraform.lock.hcl
deleted file mode 100644
index 5ea6fcd..0000000
--- a/terraform/vault/dev/.terraform.lock.hcl
+++ /dev/null
@@ -1,22 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/hashicorp/vault" {
- version = "4.8.0"
- constraints = "~> 4.8.0"
- hashes = [
- "h1:aHqgWQhDBMeZO9iUKwJYMlh4q+xNMUlMIcjRbF4d02Y=",
- "zh:269ab13433f67684012ae7e15876532b0312f5d0d2002a9cf9febb1279ce5ea6",
- "zh:4babc95bf0c40eb85005db1dc2ca403c46be4a71dd3e409db3711a56f7a5ca0e",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:86e27c1c625ecc24446a11eeffc3ac319b36c2b4e51251db8579256a0dbcf136",
- "zh:a32f31da94824009e26b077374440b52098aecb93c92ff55dc3d31dd37c4ea25",
- "zh:be0a18c6c0425518bab4fbffd82078b82036a88503b5d76064de551c9f646cbf",
- "zh:be5a77fdfd36863ebeec79cd12b1d13322ffad6821d157a0b279789fa06b5937",
- "zh:be8317d142a3caad74c7d936039ae27076a1b2b8312ef5208e2871a5f525977c",
- "zh:c94a84895a3d9954b80e983eed4603330a5cdbbd8eef5b3c99278c2d1402ef3c",
- "zh:de1fb712784dd8415f011ca5346a34f87fab6046c730557615247e511dbc7d98",
- "zh:e3eafae7da550f86cae395d6660b2a0e93ec8d2b0e0e5ef982ec762e961fc952",
- "zh:ff35fb1ab6add288f0f368981e56f780b50405accd1937131cba1137999c8d83",
- ]
-}
diff --git a/terraform/vault/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/LICENSE.txt b/terraform/vault/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/LICENSE.txt
deleted file mode 100644
index b9ac071..0000000
--- a/terraform/vault/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/LICENSE.txt
+++ /dev/null
@@ -1,375 +0,0 @@
-Copyright (c) 2017 HashiCorp, Inc.
-
-Mozilla Public License Version 2.0
-==================================
-
-1. Definitions
---------------
-
-1.1. "Contributor"
- means each individual or legal entity that creates, contributes to
- the creation of, or owns Covered Software.
-
-1.2. "Contributor Version"
- means the combination of the Contributions of others (if any) used
- by a Contributor and that particular Contributor's Contribution.
-
-1.3. "Contribution"
- means Covered Software of a particular Contributor.
-
-1.4. "Covered Software"
- means Source Code Form to which the initial Contributor has attached
- the notice in Exhibit A, the Executable Form of such Source Code
- Form, and Modifications of such Source Code Form, in each case
- including portions thereof.
-
-1.5. "Incompatible With Secondary Licenses"
- means
-
- (a) that the initial Contributor has attached the notice described
- in Exhibit B to the Covered Software; or
-
- (b) that the Covered Software was made available under the terms of
- version 1.1 or earlier of the License, but not also under the
- terms of a Secondary License.
-
-1.6. "Executable Form"
- means any form of the work other than Source Code Form.
-
-1.7. "Larger Work"
- means a work that combines Covered Software with other material, in
- a separate file or files, that is not Covered Software.
-
-1.8. "License"
- means this document.
-
-1.9. "Licensable"
- means having the right to grant, to the maximum extent possible,
- whether at the time of the initial grant or subsequently, any and
- all of the rights conveyed by this License.
-
-1.10. "Modifications"
- means any of the following:
-
- (a) any file in Source Code Form that results from an addition to,
- deletion from, or modification of the contents of Covered
- Software; or
-
- (b) any new file in Source Code Form that contains any Covered
- Software.
-
-1.11. "Patent Claims" of a Contributor
- means any patent claim(s), including without limitation, method,
- process, and apparatus claims, in any patent Licensable by such
- Contributor that would be infringed, but for the grant of the
- License, by the making, using, selling, offering for sale, having
- made, import, or transfer of either its Contributions or its
- Contributor Version.
-
-1.12. "Secondary License"
- means either the GNU General Public License, Version 2.0, the GNU
- Lesser General Public License, Version 2.1, the GNU Affero General
- Public License, Version 3.0, or any later versions of those
- licenses.
-
-1.13. "Source Code Form"
- means the form of the work preferred for making modifications.
-
-1.14. "You" (or "Your")
- means an individual or a legal entity exercising rights under this
- License. For legal entities, "You" includes any entity that
- controls, is controlled by, or is under common control with You. For
- purposes of this definition, "control" means (a) the power, direct
- or indirect, to cause the direction or management of such entity,
- whether by contract or otherwise, or (b) ownership of more than
- fifty percent (50%) of the outstanding shares or beneficial
- ownership of such entity.
-
-2. License Grants and Conditions
---------------------------------
-
-2.1. Grants
-
-Each Contributor hereby grants You a world-wide, royalty-free,
-non-exclusive license:
-
-(a) under intellectual property rights (other than patent or trademark)
- Licensable by such Contributor to use, reproduce, make available,
- modify, display, perform, distribute, and otherwise exploit its
- Contributions, either on an unmodified basis, with Modifications, or
- as part of a Larger Work; and
-
-(b) under Patent Claims of such Contributor to make, use, sell, offer
- for sale, have made, import, and otherwise transfer either its
- Contributions or its Contributor Version.
-
-2.2. Effective Date
-
-The licenses granted in Section 2.1 with respect to any Contribution
-become effective for each Contribution on the date the Contributor first
-distributes such Contribution.
-
-2.3. Limitations on Grant Scope
-
-The licenses granted in this Section 2 are the only rights granted under
-this License. No additional rights or licenses will be implied from the
-distribution or licensing of Covered Software under this License.
-Notwithstanding Section 2.1(b) above, no patent license is granted by a
-Contributor:
-
-(a) for any code that a Contributor has removed from Covered Software;
- or
-
-(b) for infringements caused by: (i) Your and any other third party's
- modifications of Covered Software, or (ii) the combination of its
- Contributions with other software (except as part of its Contributor
- Version); or
-
-(c) under Patent Claims infringed by Covered Software in the absence of
- its Contributions.
-
-This License does not grant any rights in the trademarks, service marks,
-or logos of any Contributor (except as may be necessary to comply with
-the notice requirements in Section 3.4).
-
-2.4. Subsequent Licenses
-
-No Contributor makes additional grants as a result of Your choice to
-distribute the Covered Software under a subsequent version of this
-License (see Section 10.2) or under the terms of a Secondary License (if
-permitted under the terms of Section 3.3).
-
-2.5. Representation
-
-Each Contributor represents that the Contributor believes its
-Contributions are its original creation(s) or it has sufficient rights
-to grant the rights to its Contributions conveyed by this License.
-
-2.6. Fair Use
-
-This License is not intended to limit any rights You have under
-applicable copyright doctrines of fair use, fair dealing, or other
-equivalents.
-
-2.7. Conditions
-
-Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
-in Section 2.1.
-
-3. Responsibilities
--------------------
-
-3.1. Distribution of Source Form
-
-All distribution of Covered Software in Source Code Form, including any
-Modifications that You create or to which You contribute, must be under
-the terms of this License. You must inform recipients that the Source
-Code Form of the Covered Software is governed by the terms of this
-License, and how they can obtain a copy of this License. You may not
-attempt to alter or restrict the recipients' rights in the Source Code
-Form.
-
-3.2. Distribution of Executable Form
-
-If You distribute Covered Software in Executable Form then:
-
-(a) such Covered Software must also be made available in Source Code
- Form, as described in Section 3.1, and You must inform recipients of
- the Executable Form how they can obtain a copy of such Source Code
- Form by reasonable means in a timely manner, at a charge no more
- than the cost of distribution to the recipient; and
-
-(b) You may distribute such Executable Form under the terms of this
- License, or sublicense it under different terms, provided that the
- license for the Executable Form does not attempt to limit or alter
- the recipients' rights in the Source Code Form under this License.
-
-3.3. Distribution of a Larger Work
-
-You may create and distribute a Larger Work under terms of Your choice,
-provided that You also comply with the requirements of this License for
-the Covered Software. If the Larger Work is a combination of Covered
-Software with a work governed by one or more Secondary Licenses, and the
-Covered Software is not Incompatible With Secondary Licenses, this
-License permits You to additionally distribute such Covered Software
-under the terms of such Secondary License(s), so that the recipient of
-the Larger Work may, at their option, further distribute the Covered
-Software under the terms of either this License or such Secondary
-License(s).
-
-3.4. Notices
-
-You may not remove or alter the substance of any license notices
-(including copyright notices, patent notices, disclaimers of warranty,
-or limitations of liability) contained within the Source Code Form of
-the Covered Software, except that You may alter any license notices to
-the extent required to remedy known factual inaccuracies.
-
-3.5. Application of Additional Terms
-
-You may choose to offer, and to charge a fee for, warranty, support,
-indemnity or liability obligations to one or more recipients of Covered
-Software. However, You may do so only on Your own behalf, and not on
-behalf of any Contributor. You must make it absolutely clear that any
-such warranty, support, indemnity, or liability obligation is offered by
-You alone, and You hereby agree to indemnify every Contributor for any
-liability incurred by such Contributor as a result of warranty, support,
-indemnity or liability terms You offer. You may include additional
-disclaimers of warranty and limitations of liability specific to any
-jurisdiction.
-
-4. Inability to Comply Due to Statute or Regulation
----------------------------------------------------
-
-If it is impossible for You to comply with any of the terms of this
-License with respect to some or all of the Covered Software due to
-statute, judicial order, or regulation then You must: (a) comply with
-the terms of this License to the maximum extent possible; and (b)
-describe the limitations and the code they affect. Such description must
-be placed in a text file included with all distributions of the Covered
-Software under this License. Except to the extent prohibited by statute
-or regulation, such description must be sufficiently detailed for a
-recipient of ordinary skill to be able to understand it.
-
-5. Termination
---------------
-
-5.1. The rights granted under this License will terminate automatically
-if You fail to comply with any of its terms. However, if You become
-compliant, then the rights granted under this License from a particular
-Contributor are reinstated (a) provisionally, unless and until such
-Contributor explicitly and finally terminates Your grants, and (b) on an
-ongoing basis, if such Contributor fails to notify You of the
-non-compliance by some reasonable means prior to 60 days after You have
-come back into compliance. Moreover, Your grants from a particular
-Contributor are reinstated on an ongoing basis if such Contributor
-notifies You of the non-compliance by some reasonable means, this is the
-first time You have received notice of non-compliance with this License
-from such Contributor, and You become compliant prior to 30 days after
-Your receipt of the notice.
-
-5.2. If You initiate litigation against any entity by asserting a patent
-infringement claim (excluding declaratory judgment actions,
-counter-claims, and cross-claims) alleging that a Contributor Version
-directly or indirectly infringes any patent, then the rights granted to
-You by any and all Contributors for the Covered Software under Section
-2.1 of this License shall terminate.
-
-5.3. In the event of termination under Sections 5.1 or 5.2 above, all
-end user license agreements (excluding distributors and resellers) which
-have been validly granted by You or Your distributors under this License
-prior to termination shall survive termination.
-
-************************************************************************
-* *
-* 6. Disclaimer of Warranty *
-* ------------------------- *
-* *
-* Covered Software is provided under this License on an "as is" *
-* basis, without warranty of any kind, either expressed, implied, or *
-* statutory, including, without limitation, warranties that the *
-* Covered Software is free of defects, merchantable, fit for a *
-* particular purpose or non-infringing. The entire risk as to the *
-* quality and performance of the Covered Software is with You. *
-* Should any Covered Software prove defective in any respect, You *
-* (not any Contributor) assume the cost of any necessary servicing, *
-* repair, or correction. This disclaimer of warranty constitutes an *
-* essential part of this License. No use of any Covered Software is *
-* authorized under this License except under this disclaimer. *
-* *
-************************************************************************
-
-************************************************************************
-* *
-* 7. Limitation of Liability *
-* -------------------------- *
-* *
-* Under no circumstances and under no legal theory, whether tort *
-* (including negligence), contract, or otherwise, shall any *
-* Contributor, or anyone who distributes Covered Software as *
-* permitted above, be liable to You for any direct, indirect, *
-* special, incidental, or consequential damages of any character *
-* including, without limitation, damages for lost profits, loss of *
-* goodwill, work stoppage, computer failure or malfunction, or any *
-* and all other commercial damages or losses, even if such party *
-* shall have been informed of the possibility of such damages. This *
-* limitation of liability shall not apply to liability for death or *
-* personal injury resulting from such party's negligence to the *
-* extent applicable law prohibits such limitation. Some *
-* jurisdictions do not allow the exclusion or limitation of *
-* incidental or consequential damages, so this exclusion and *
-* limitation may not apply to You. *
-* *
-************************************************************************
-
-8. Litigation
--------------
-
-Any litigation relating to this License may be brought only in the
-courts of a jurisdiction where the defendant maintains its principal
-place of business and such litigation shall be governed by laws of that
-jurisdiction, without reference to its conflict-of-law provisions.
-Nothing in this Section shall prevent a party's ability to bring
-cross-claims or counter-claims.
-
-9. Miscellaneous
-----------------
-
-This License represents the complete agreement concerning the subject
-matter hereof. If any provision of this License is held to be
-unenforceable, such provision shall be reformed only to the extent
-necessary to make it enforceable. Any law or regulation which provides
-that the language of a contract shall be construed against the drafter
-shall not be used to construe this License against a Contributor.
-
-10. Versions of the License
----------------------------
-
-10.1. New Versions
-
-Mozilla Foundation is the license steward. Except as provided in Section
-10.3, no one other than the license steward has the right to modify or
-publish new versions of this License. Each version will be given a
-distinguishing version number.
-
-10.2. Effect of New Versions
-
-You may distribute the Covered Software under the terms of the version
-of the License under which You originally received the Covered Software,
-or under the terms of any subsequent version published by the license
-steward.
-
-10.3. Modified Versions
-
-If you create software not governed by this License, and you want to
-create a new license for such software, you may create and use a
-modified version of this License if you rename the license and remove
-any references to the name of the license steward (except to note that
-such modified license differs from this License).
-
-10.4. Distributing Source Code Form that is Incompatible With Secondary
-Licenses
-
-If You choose to distribute Source Code Form that is Incompatible With
-Secondary Licenses under the terms of this version of the License, the
-notice described in Exhibit B of this License must be attached.
-
-Exhibit A - Source Code Form License Notice
--------------------------------------------
-
- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-If it is not possible or desirable to put the notice in a particular
-file, then You may include the notice in a location (such as a LICENSE
-file in a relevant directory) where a recipient would be likely to look
-for such a notice.
-
-You may add additional accurate notices of copyright ownership.
-
-Exhibit B - "Incompatible With Secondary Licenses" Notice
----------------------------------------------------------
-
- This Source Code Form is "Incompatible With Secondary Licenses", as
- defined by the Mozilla Public License, v. 2.0.
diff --git a/terraform/vault/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/terraform-provider-vault_v4.8.0_x5 b/terraform/vault/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/terraform-provider-vault_v4.8.0_x5
deleted file mode 100755
index 60f3e12..0000000
Binary files a/terraform/vault/dev/.terraform/providers/registry.terraform.io/hashicorp/vault/4.8.0/linux_amd64/terraform-provider-vault_v4.8.0_x5 and /dev/null differ
diff --git a/terraform/vault/dev/.terraform/terraform.tfstate b/terraform/vault/dev/.terraform/terraform.tfstate
deleted file mode 100644
index 009559f..0000000
--- a/terraform/vault/dev/.terraform/terraform.tfstate
+++ /dev/null
@@ -1,12 +0,0 @@
-{
- "version": 3,
- "terraform_version": "1.14.8",
- "backend": {
- "type": "local",
- "config": {
- "path": "../../../.terraform-state/vault-dev.tfstate",
- "workspace_dir": null
- },
- "hash": 3604471491
- }
-}
\ No newline at end of file
diff --git a/terraform/vault/dev/main.tf b/terraform/vault/dev/main.tf
deleted file mode 100644
index 3a15a67..0000000
--- a/terraform/vault/dev/main.tf
+++ /dev/null
@@ -1,364 +0,0 @@
-terraform {
- required_version = ">= 1.6.0"
-
- required_providers {
- vault = {
- source = "hashicorp/vault"
- version = "~> 4.8.0"
- }
- }
-
- backend "local" {
- path = "../../../.terraform-state/vault-dev.tfstate"
- }
-}
-
-provider "vault" {
- address = var.workload_vault_addr
- skip_child_token = true
- token = var.workload_vault_token
-}
-
-provider "vault" {
- alias = "transit"
- address = var.transit_vault_addr
- skip_child_token = true
- token = var.transit_vault_token
-}
-
-locals {
- policy_dir = "${path.module}/../../../runbooks/vault/dev/policies"
-
- auth_server_policy_name = "auth-server-dev"
- auth_db_migration_policy_name = "auth-db-migration-dev"
- postgres_policy_name = "postgres-dev"
- keycloak_policy_name = "keycloak-dev"
- keycloak_client_sync_policy_name = "keycloak-client-sync-dev"
- postgres_operator_policy_name = "postgres-operator-dev"
- keycloak_operator_policy_name = "keycloak-operator-dev"
- platform_admin_policy_name = "platform-admin-dev"
- workload_automation_policy_name = "workload-automation-dev"
- workload_workflow_role_name = "workload-dev-workflow"
- auth_db_migration_role_name = "auth-db-migration-dev"
- postgres_operator_role_name = "postgres-operator-dev"
-
- migration_creation_statements = [
- <<-EOT
- CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
- GRANT "${var.auth_db_role}" TO "{{name}}";
- EOT
- ]
-
- migration_revocation_statements = [
- <<-EOT
- REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
- DROP OWNED BY "{{name}}";
- REVOKE "${var.auth_db_role}" FROM "{{name}}";
- DROP ROLE IF EXISTS "{{name}}";
- EOT
- ]
-
- operator_creation_statements = [
- <<-EOT
- CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
- GRANT "${var.auth_db_role}" TO "{{name}}";
- EOT
- ]
-
- operator_revocation_statements = [
- <<-EOT
- REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
- DROP OWNED BY "{{name}}";
- REVOKE "${var.auth_db_role}" FROM "{{name}}";
- DROP ROLE IF EXISTS "{{name}}";
- EOT
- ]
-}
-
-resource "vault_mount" "kv" {
- path = var.kv_mount_path
- type = "kv"
- options = {
- version = "2"
- }
-
- lifecycle {
- prevent_destroy = true
- ignore_changes = [type, options]
- }
-}
-
-resource "vault_mount" "database" {
- path = var.database_mount_path
- type = "database"
-
- lifecycle {
- prevent_destroy = true
- }
-}
-
-resource "vault_mount" "transit" {
- path = var.transit_mount_path
- type = "transit"
-
- lifecycle {
- prevent_destroy = true
- }
-}
-
-resource "vault_auth_backend" "kubernetes" {
- path = var.kubernetes_auth_path
- type = "kubernetes"
-}
-
-resource "vault_kubernetes_auth_backend_config" "cluster" {
- backend = vault_auth_backend.kubernetes.path
- disable_iss_validation = true
- kubernetes_host = "https://kubernetes.default.svc.cluster.local:443"
- kubernetes_ca_cert = var.kubernetes_ca_cert
- token_reviewer_jwt = var.kubernetes_token_reviewer_jwt
-}
-
-resource "vault_auth_backend" "approle" {
- path = var.approle_auth_path
- type = "approle"
-}
-
-resource "vault_policy" "auth_server" {
- name = local.auth_server_policy_name
- policy = file("${local.policy_dir}/auth-server-dev.hcl")
-}
-
-resource "vault_policy" "auth_db_migration" {
- name = local.auth_db_migration_policy_name
- policy = file("${local.policy_dir}/auth-db-migration-dev.hcl")
-}
-
-resource "vault_policy" "postgres" {
- name = local.postgres_policy_name
- policy = file("${local.policy_dir}/postgres-dev.hcl")
-}
-
-resource "vault_policy" "keycloak" {
- name = local.keycloak_policy_name
- policy = file("${local.policy_dir}/keycloak-dev.hcl")
-}
-
-resource "vault_policy" "keycloak_client_sync" {
- name = local.keycloak_client_sync_policy_name
- policy = file("${local.policy_dir}/keycloak-client-sync-dev.hcl")
-}
-
-resource "vault_policy" "postgres_operator" {
- name = local.postgres_operator_policy_name
- policy = file("${local.policy_dir}/postgres-operator-dev.hcl")
-}
-
-resource "vault_policy" "keycloak_operator" {
- name = local.keycloak_operator_policy_name
- policy = file("${local.policy_dir}/keycloak-operator-dev.hcl")
-}
-
-resource "vault_policy" "platform_admin" {
- name = local.platform_admin_policy_name
- policy = file("${local.policy_dir}/platform-admin-dev.hcl")
-}
-
-resource "vault_policy" "workload_automation" {
- name = local.workload_automation_policy_name
- policy = file("${local.policy_dir}/workload-automation-dev.hcl")
-}
-
-resource "vault_kubernetes_auth_backend_role" "auth_server" {
- backend = vault_auth_backend.kubernetes.path
- bound_service_account_names = ["auth-server"]
- bound_service_account_namespaces = ["auth-dev"]
- role_name = local.auth_server_policy_name
- token_policies = [vault_policy.auth_server.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "auth_db_migration" {
- backend = vault_auth_backend.kubernetes.path
- bound_service_account_names = ["auth-db-migration"]
- bound_service_account_namespaces = ["auth-dev"]
- role_name = local.auth_db_migration_policy_name
- token_policies = [vault_policy.auth_db_migration.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "postgres" {
- backend = vault_auth_backend.kubernetes.path
- bound_service_account_names = ["postgres"]
- bound_service_account_namespaces = ["platform"]
- role_name = local.postgres_policy_name
- token_policies = [vault_policy.postgres.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "keycloak" {
- backend = vault_auth_backend.kubernetes.path
- bound_service_account_names = ["keycloak"]
- bound_service_account_namespaces = ["platform"]
- role_name = local.keycloak_policy_name
- token_policies = [vault_policy.keycloak.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "keycloak_client_sync" {
- backend = vault_auth_backend.kubernetes.path
- bound_service_account_names = ["keycloak-client-sync"]
- bound_service_account_namespaces = ["platform"]
- role_name = local.keycloak_client_sync_policy_name
- token_policies = [vault_policy.keycloak_client_sync.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_transit_secret_backend_key" "project_auth_jwt" {
- backend = vault_mount.transit.path
- name = var.jwt_transit_key_name
- type = "rsa-2048"
-}
-
-resource "vault_approle_auth_backend_role" "workflow" {
- backend = vault_auth_backend.approle.path
- role_name = local.workload_workflow_role_name
- secret_id_num_uses = 0
- secret_id_ttl = 0
- token_max_ttl = var.workflow_token_max_ttl_seconds
- token_policies = [vault_policy.workload_automation.name]
- token_ttl = var.workflow_token_ttl_seconds
-}
-
-resource "vault_approle_auth_backend_role_secret_id" "workflow" {
- backend = vault_auth_backend.approle.path
- role_name = vault_approle_auth_backend_role.workflow.role_name
-}
-
-data "vault_kv_secret_v2" "provider_postgres_superuser" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/superuser"
-}
-
-data "vault_kv_secret_v2" "provider_postgres_auth_server" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/auth-server"
-}
-
-data "vault_kv_secret_v2" "provider_postgres_keycloak" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/keycloak"
-}
-
-data "vault_kv_secret_v2" "provider_keycloak_bootstrap_admin" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/keycloak/bootstrap-admin"
-}
-
-data "vault_kv_secret_v2" "provider_keycloak_client_auth_server" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/keycloak/client-auth-server"
-}
-
-resource "vault_kv_secret_v2" "workload_bootstrap" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/bootstrap"
- data_json = jsonencode({
- VAULT_WORKLOAD_DEV_ROLE_ID = vault_approle_auth_backend_role.workflow.role_id
- VAULT_WORKLOAD_DEV_SECRET_ID = vault_approle_auth_backend_role_secret_id.workflow.secret_id
- })
-}
-
-resource "vault_kv_secret_v2" "platform_postgres_superuser" {
- mount = vault_mount.kv.path
- name = "dev/platform/postgres/superuser"
- data_json = jsonencode({
- POSTGRES_SUPERUSER_PASSWORD = data.vault_kv_secret_v2.provider_postgres_superuser.data["POSTGRES_SUPERUSER_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_postgres_auth_server" {
- mount = vault_mount.kv.path
- name = "dev/platform/postgres/auth-server"
- data_json = jsonencode({
- APP_DATASOURCE_PASSWORD = data.vault_kv_secret_v2.provider_postgres_auth_server.data["APP_DATASOURCE_PASSWORD"]
- APP_DATASOURCE_USERNAME = data.vault_kv_secret_v2.provider_postgres_auth_server.data["APP_DATASOURCE_USERNAME"]
- AUTH_DB_PASSWORD = data.vault_kv_secret_v2.provider_postgres_auth_server.data["AUTH_DB_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_postgres_keycloak" {
- mount = vault_mount.kv.path
- name = "dev/platform/postgres/keycloak"
- data_json = jsonencode({
- KEYCLOAK_DB_PASSWORD = data.vault_kv_secret_v2.provider_postgres_keycloak.data["KEYCLOAK_DB_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_keycloak_bootstrap_admin" {
- mount = vault_mount.kv.path
- name = "dev/platform/keycloak/bootstrap-admin"
- data_json = jsonencode({
- KC_BOOTSTRAP_ADMIN_PASSWORD = data.vault_kv_secret_v2.provider_keycloak_bootstrap_admin.data["KC_BOOTSTRAP_ADMIN_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_keycloak_client_auth_server" {
- mount = vault_mount.kv.path
- name = "dev/platform/keycloak/client-auth-server"
- data_json = jsonencode({
- APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET = data.vault_kv_secret_v2.provider_keycloak_client_auth_server.data["APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET"]
- KEYCLOAK_CLIENT_SECRET = data.vault_kv_secret_v2.provider_keycloak_client_auth_server.data["KEYCLOAK_CLIENT_SECRET"]
- })
-}
-
-resource "vault_database_secret_backend_connection" "platform_postgres" {
- allowed_roles = [local.auth_db_migration_role_name, local.postgres_operator_role_name]
- backend = vault_mount.database.path
- name = var.database_config_name
- plugin_name = "postgresql-database-plugin"
- verify_connection = false
-
- postgresql {
- connection_url = "postgresql://{{username}}:{{password}}@${var.postgres_host}:${var.postgres_port}/${var.postgres_admin_database}?sslmode=disable"
- password = vault_kv_secret_v2.platform_postgres_superuser.data["POSTGRES_SUPERUSER_PASSWORD"]
- username = var.postgres_superuser
- }
-}
-
-resource "vault_database_secret_backend_role" "auth_db_migration" {
- backend = vault_mount.database.path
- creation_statements = local.migration_creation_statements
- db_name = vault_database_secret_backend_connection.platform_postgres.name
- default_ttl = var.auth_db_migration_default_ttl_seconds
- max_ttl = var.auth_db_migration_max_ttl_seconds
- name = local.auth_db_migration_role_name
- revocation_statements = local.migration_revocation_statements
-}
-
-resource "vault_database_secret_backend_role" "postgres_operator" {
- backend = vault_mount.database.path
- creation_statements = local.operator_creation_statements
- db_name = vault_database_secret_backend_connection.platform_postgres.name
- default_ttl = var.postgres_operator_default_ttl_seconds
- max_ttl = var.postgres_operator_max_ttl_seconds
- name = local.postgres_operator_role_name
- revocation_statements = local.operator_revocation_statements
-}
-
-output "workflow_role_id" {
- description = "Workload Vault workflow AppRole role_id."
- value = vault_approle_auth_backend_role.workflow.role_id
-}
-
-output "workflow_secret_id" {
- description = "Workload Vault workflow AppRole secret_id."
- value = vault_approle_auth_backend_role_secret_id.workflow.secret_id
- sensitive = true
-}
diff --git a/terraform/vault/dev/variables.tf b/terraform/vault/dev/variables.tf
deleted file mode 100644
index 59b6c68..0000000
--- a/terraform/vault/dev/variables.tf
+++ /dev/null
@@ -1,157 +0,0 @@
-variable "approle_auth_path" {
- description = "Path where the workload AppRole auth backend is mounted."
- type = string
- default = "approle"
-}
-
-variable "auth_db_migration_default_ttl_seconds" {
- description = "Default TTL, in seconds, for auth DB migration credentials."
- type = number
- default = 3600
-}
-
-variable "auth_db_migration_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for auth DB migration credentials."
- type = number
- default = 86400
-}
-
-variable "auth_db_role" {
- description = "Existing PostgreSQL role granted to dynamic auth users."
- type = string
- default = "project_auth"
-}
-
-variable "database_config_name" {
- description = "Name of the Vault database connection configuration."
- type = string
- default = "platform-postgres-dev"
-}
-
-variable "database_mount_path" {
- description = "Mount path for the workload database secrets engine."
- type = string
- default = "database"
-}
-
-variable "jwt_transit_key_name" {
- description = "Transit key name used for JWT signing."
- type = string
- default = "project-auth-jwt"
-}
-
-variable "kubernetes_auth_path" {
- description = "Path where the Kubernetes auth backend is mounted."
- type = string
- default = "kubernetes"
-}
-
-variable "kubernetes_ca_cert" {
- description = "CA certificate used by the workload Vault Kubernetes auth backend."
- type = string
- default = null
- sensitive = true
-}
-
-variable "kubernetes_token_reviewer_jwt" {
- description = "Reviewer JWT used by the workload Vault Kubernetes auth backend."
- type = string
- default = null
- sensitive = true
-}
-
-variable "kubernetes_role_ttl_seconds" {
- description = "TTL, in seconds, granted to workload Kubernetes auth logins."
- type = number
- default = 86400
-}
-
-variable "kv_mount_path" {
- description = "Mount path for the workload KV-v2 engine."
- type = string
- default = "kv"
-}
-
-variable "postgres_admin_database" {
- description = "Administrative PostgreSQL database used by the database secret engine."
- type = string
- default = "postgres"
-}
-
-variable "postgres_host" {
- description = "DNS name of the platform PostgreSQL service."
- type = string
- default = "postgres-0.postgres.platform.svc.cluster.local"
-}
-
-variable "postgres_operator_default_ttl_seconds" {
- description = "Default TTL, in seconds, for the operator PostgreSQL role."
- type = number
- default = 3600
-}
-
-variable "postgres_operator_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for the operator PostgreSQL role."
- type = number
- default = 28800
-}
-
-variable "postgres_port" {
- description = "Port of the platform PostgreSQL service."
- type = number
- default = 5432
-}
-
-variable "postgres_superuser" {
- description = "PostgreSQL superuser used by Vault database secrets."
- type = string
- default = "postgres"
-}
-
-variable "seed_kv_mount_path" {
- description = "Mount path for the provider KV-v2 seed data."
- type = string
- default = "kv"
-}
-
-variable "transit_mount_path" {
- description = "Mount path for the workload transit engine."
- type = string
- default = "transit"
-}
-
-variable "transit_vault_addr" {
- description = "Address of the vault-transit API."
- type = string
- default = "http://127.0.0.1:18200"
-}
-
-variable "transit_vault_token" {
- description = "Token used to read provider seed data and publish workload bootstrap credentials."
- type = string
- sensitive = true
-}
-
-variable "workflow_token_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for the workload workflow AppRole login token."
- type = number
- default = 14400
-}
-
-variable "workflow_token_ttl_seconds" {
- description = "Default TTL, in seconds, for the workload workflow AppRole login token."
- type = number
- default = 3600
-}
-
-variable "workload_vault_addr" {
- description = "Address of the workload Vault API."
- type = string
- default = "http://127.0.0.1:8200"
-}
-
-variable "workload_vault_token" {
- description = "Privileged token used to reconcile workload Vault."
- type = string
- sensitive = true
-}
diff --git a/terraform/vault/reconcile/.terraform.lock.hcl b/terraform/vault/reconcile/.terraform.lock.hcl
deleted file mode 100644
index 5ea6fcd..0000000
--- a/terraform/vault/reconcile/.terraform.lock.hcl
+++ /dev/null
@@ -1,22 +0,0 @@
-# This file is maintained automatically by "terraform init".
-# Manual edits may be lost in future updates.
-
-provider "registry.terraform.io/hashicorp/vault" {
- version = "4.8.0"
- constraints = "~> 4.8.0"
- hashes = [
- "h1:aHqgWQhDBMeZO9iUKwJYMlh4q+xNMUlMIcjRbF4d02Y=",
- "zh:269ab13433f67684012ae7e15876532b0312f5d0d2002a9cf9febb1279ce5ea6",
- "zh:4babc95bf0c40eb85005db1dc2ca403c46be4a71dd3e409db3711a56f7a5ca0e",
- "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
- "zh:86e27c1c625ecc24446a11eeffc3ac319b36c2b4e51251db8579256a0dbcf136",
- "zh:a32f31da94824009e26b077374440b52098aecb93c92ff55dc3d31dd37c4ea25",
- "zh:be0a18c6c0425518bab4fbffd82078b82036a88503b5d76064de551c9f646cbf",
- "zh:be5a77fdfd36863ebeec79cd12b1d13322ffad6821d157a0b279789fa06b5937",
- "zh:be8317d142a3caad74c7d936039ae27076a1b2b8312ef5208e2871a5f525977c",
- "zh:c94a84895a3d9954b80e983eed4603330a5cdbbd8eef5b3c99278c2d1402ef3c",
- "zh:de1fb712784dd8415f011ca5346a34f87fab6046c730557615247e511dbc7d98",
- "zh:e3eafae7da550f86cae395d6660b2a0e93ec8d2b0e0e5ef982ec762e961fc952",
- "zh:ff35fb1ab6add288f0f368981e56f780b50405accd1937131cba1137999c8d83",
- ]
-}
diff --git a/terraform/vault/reconcile/main.tf b/terraform/vault/reconcile/main.tf
deleted file mode 100644
index 83d4f30..0000000
--- a/terraform/vault/reconcile/main.tf
+++ /dev/null
@@ -1,283 +0,0 @@
-terraform {
- required_version = ">= 1.6.0"
-
- required_providers {
- vault = {
- source = "hashicorp/vault"
- version = "~> 4.8.0"
- }
- }
-
- backend "local" {
- path = "../../../.terraform-state/vault-reconcile.tfstate"
- }
-}
-
-provider "vault" {
- address = var.workload_vault_addr
- skip_child_token = true
- token = var.workload_vault_token
-}
-
-provider "vault" {
- alias = "transit"
- address = var.transit_vault_addr
- skip_child_token = true
- token = var.transit_vault_token
-}
-
-locals {
- policy_dir = "${path.module}/../../../runbooks/vault/dev/policies"
-
- auth_server_policy_name = "auth-server-dev"
- auth_db_migration_policy_name = "auth-db-migration-dev"
- postgres_policy_name = "postgres-dev"
- keycloak_policy_name = "keycloak-dev"
- keycloak_client_sync_policy_name = "keycloak-client-sync-dev"
- postgres_operator_policy_name = "postgres-operator-dev"
- keycloak_operator_policy_name = "keycloak-operator-dev"
- platform_admin_policy_name = "platform-admin-dev"
- workload_automation_policy_name = "workload-automation-dev"
- workload_workflow_role_name = "workload-dev-workflow"
- auth_db_migration_role_name = "auth-db-migration-dev"
- postgres_operator_role_name = "postgres-operator-dev"
-
- migration_creation_statements = [
- <<-EOT
- CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
- GRANT "${var.auth_db_role}" TO "{{name}}";
- EOT
- ]
-
- migration_revocation_statements = [
- <<-EOT
- REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
- DROP OWNED BY "{{name}}";
- REVOKE "${var.auth_db_role}" FROM "{{name}}";
- DROP ROLE IF EXISTS "{{name}}";
- EOT
- ]
-
- operator_creation_statements = [
- <<-EOT
- CREATE ROLE "{{name}}" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';
- GRANT "${var.auth_db_role}" TO "{{name}}";
- EOT
- ]
-
- operator_revocation_statements = [
- <<-EOT
- REASSIGN OWNED BY "{{name}}" TO "${var.auth_db_role}";
- DROP OWNED BY "{{name}}";
- REVOKE "${var.auth_db_role}" FROM "{{name}}";
- DROP ROLE IF EXISTS "{{name}}";
- EOT
- ]
-}
-
-resource "vault_policy" "auth_server" {
- name = local.auth_server_policy_name
- policy = file("${local.policy_dir}/auth-server-dev.hcl")
-}
-
-resource "vault_policy" "auth_db_migration" {
- name = local.auth_db_migration_policy_name
- policy = file("${local.policy_dir}/auth-db-migration-dev.hcl")
-}
-
-resource "vault_policy" "postgres" {
- name = local.postgres_policy_name
- policy = file("${local.policy_dir}/postgres-dev.hcl")
-}
-
-resource "vault_policy" "keycloak" {
- name = local.keycloak_policy_name
- policy = file("${local.policy_dir}/keycloak-dev.hcl")
-}
-
-resource "vault_policy" "keycloak_client_sync" {
- name = local.keycloak_client_sync_policy_name
- policy = file("${local.policy_dir}/keycloak-client-sync-dev.hcl")
-}
-
-resource "vault_policy" "postgres_operator" {
- name = local.postgres_operator_policy_name
- policy = file("${local.policy_dir}/postgres-operator-dev.hcl")
-}
-
-resource "vault_policy" "keycloak_operator" {
- name = local.keycloak_operator_policy_name
- policy = file("${local.policy_dir}/keycloak-operator-dev.hcl")
-}
-
-resource "vault_policy" "platform_admin" {
- name = local.platform_admin_policy_name
- policy = file("${local.policy_dir}/platform-admin-dev.hcl")
-}
-
-resource "vault_policy" "workload_automation" {
- name = local.workload_automation_policy_name
- policy = file("${local.policy_dir}/workload-automation-dev.hcl")
-}
-
-resource "vault_kubernetes_auth_backend_role" "auth_server" {
- backend = var.kubernetes_auth_path
- bound_service_account_names = ["auth-server"]
- bound_service_account_namespaces = ["auth-dev"]
- role_name = local.auth_server_policy_name
- token_policies = [vault_policy.auth_server.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "auth_db_migration" {
- backend = var.kubernetes_auth_path
- bound_service_account_names = ["auth-db-migration"]
- bound_service_account_namespaces = ["auth-dev"]
- role_name = local.auth_db_migration_policy_name
- token_policies = [vault_policy.auth_db_migration.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "postgres" {
- backend = var.kubernetes_auth_path
- bound_service_account_names = ["postgres"]
- bound_service_account_namespaces = ["platform"]
- role_name = local.postgres_policy_name
- token_policies = [vault_policy.postgres.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "keycloak" {
- backend = var.kubernetes_auth_path
- bound_service_account_names = ["keycloak"]
- bound_service_account_namespaces = ["platform"]
- role_name = local.keycloak_policy_name
- token_policies = [vault_policy.keycloak.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_kubernetes_auth_backend_role" "keycloak_client_sync" {
- backend = var.kubernetes_auth_path
- bound_service_account_names = ["keycloak-client-sync"]
- bound_service_account_namespaces = ["platform"]
- role_name = local.keycloak_client_sync_policy_name
- token_policies = [vault_policy.keycloak_client_sync.name]
- token_ttl = var.kubernetes_role_ttl_seconds
-}
-
-resource "vault_approle_auth_backend_role" "workflow" {
- backend = var.approle_auth_path
- role_name = local.workload_workflow_role_name
- secret_id_num_uses = 0
- secret_id_ttl = 0
- token_max_ttl = var.workflow_token_max_ttl_seconds
- token_policies = [vault_policy.workload_automation.name]
- token_ttl = var.workflow_token_ttl_seconds
-}
-
-data "vault_kv_secret_v2" "provider_postgres_superuser" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/superuser"
-}
-
-data "vault_kv_secret_v2" "provider_postgres_auth_server" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/auth-server"
-}
-
-data "vault_kv_secret_v2" "provider_postgres_keycloak" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/postgres/keycloak"
-}
-
-data "vault_kv_secret_v2" "provider_keycloak_bootstrap_admin" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/keycloak/bootstrap-admin"
-}
-
-data "vault_kv_secret_v2" "provider_keycloak_client_auth_server" {
- provider = vault.transit
- mount = var.seed_kv_mount_path
- name = "dev/workload/platform/keycloak/client-auth-server"
-}
-
-resource "vault_kv_secret_v2" "platform_postgres_superuser" {
- mount = var.kv_mount_path
- name = "dev/platform/postgres/superuser"
- data_json = jsonencode({
- POSTGRES_SUPERUSER_PASSWORD = data.vault_kv_secret_v2.provider_postgres_superuser.data["POSTGRES_SUPERUSER_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_postgres_auth_server" {
- mount = var.kv_mount_path
- name = "dev/platform/postgres/auth-server"
- data_json = jsonencode({
- APP_DATASOURCE_PASSWORD = data.vault_kv_secret_v2.provider_postgres_auth_server.data["APP_DATASOURCE_PASSWORD"]
- APP_DATASOURCE_USERNAME = data.vault_kv_secret_v2.provider_postgres_auth_server.data["APP_DATASOURCE_USERNAME"]
- AUTH_DB_PASSWORD = data.vault_kv_secret_v2.provider_postgres_auth_server.data["AUTH_DB_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_postgres_keycloak" {
- mount = var.kv_mount_path
- name = "dev/platform/postgres/keycloak"
- data_json = jsonencode({
- KEYCLOAK_DB_PASSWORD = data.vault_kv_secret_v2.provider_postgres_keycloak.data["KEYCLOAK_DB_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_keycloak_bootstrap_admin" {
- mount = var.kv_mount_path
- name = "dev/platform/keycloak/bootstrap-admin"
- data_json = jsonencode({
- KC_BOOTSTRAP_ADMIN_PASSWORD = data.vault_kv_secret_v2.provider_keycloak_bootstrap_admin.data["KC_BOOTSTRAP_ADMIN_PASSWORD"]
- })
-}
-
-resource "vault_kv_secret_v2" "platform_keycloak_client_auth_server" {
- mount = var.kv_mount_path
- name = "dev/platform/keycloak/client-auth-server"
- data_json = jsonencode({
- APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET = data.vault_kv_secret_v2.provider_keycloak_client_auth_server.data["APP_SECURITY_OAUTH2_KEYCLOAK_CLIENT_SECRET"]
- KEYCLOAK_CLIENT_SECRET = data.vault_kv_secret_v2.provider_keycloak_client_auth_server.data["KEYCLOAK_CLIENT_SECRET"]
- })
-}
-
-resource "vault_database_secret_backend_connection" "platform_postgres" {
- allowed_roles = [local.auth_db_migration_role_name, local.postgres_operator_role_name]
- backend = var.database_mount_path
- name = var.database_config_name
- plugin_name = "postgresql-database-plugin"
- verify_connection = false
-
- postgresql {
- connection_url = "postgresql://{{username}}:{{password}}@${var.postgres_host}:${var.postgres_port}/${var.postgres_admin_database}?sslmode=disable"
- password = vault_kv_secret_v2.platform_postgres_superuser.data["POSTGRES_SUPERUSER_PASSWORD"]
- username = var.postgres_superuser
- }
-}
-
-resource "vault_database_secret_backend_role" "auth_db_migration" {
- backend = var.database_mount_path
- creation_statements = local.migration_creation_statements
- db_name = vault_database_secret_backend_connection.platform_postgres.name
- default_ttl = var.auth_db_migration_default_ttl_seconds
- max_ttl = var.auth_db_migration_max_ttl_seconds
- name = local.auth_db_migration_role_name
- revocation_statements = local.migration_revocation_statements
-}
-
-resource "vault_database_secret_backend_role" "postgres_operator" {
- backend = var.database_mount_path
- creation_statements = local.operator_creation_statements
- db_name = vault_database_secret_backend_connection.platform_postgres.name
- default_ttl = var.postgres_operator_default_ttl_seconds
- max_ttl = var.postgres_operator_max_ttl_seconds
- name = local.postgres_operator_role_name
- revocation_statements = local.operator_revocation_statements
-}
diff --git a/terraform/vault/reconcile/variables.tf b/terraform/vault/reconcile/variables.tf
deleted file mode 100644
index fdf3528..0000000
--- a/terraform/vault/reconcile/variables.tf
+++ /dev/null
@@ -1,131 +0,0 @@
-variable "approle_auth_path" {
- description = "Path where the workload AppRole auth backend is mounted."
- type = string
- default = "approle"
-}
-
-variable "auth_db_migration_default_ttl_seconds" {
- description = "Default TTL, in seconds, for auth DB migration credentials."
- type = number
- default = 3600
-}
-
-variable "auth_db_migration_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for auth DB migration credentials."
- type = number
- default = 86400
-}
-
-variable "auth_db_role" {
- description = "Existing PostgreSQL role granted to dynamic auth users."
- type = string
- default = "project_auth"
-}
-
-variable "database_config_name" {
- description = "Name of the Vault database connection configuration."
- type = string
- default = "platform-postgres-dev"
-}
-
-variable "database_mount_path" {
- description = "Mount path for the workload database secrets engine."
- type = string
- default = "database"
-}
-
-variable "kubernetes_auth_path" {
- description = "Path where the Kubernetes auth backend is mounted."
- type = string
- default = "kubernetes"
-}
-
-variable "kubernetes_role_ttl_seconds" {
- description = "TTL, in seconds, granted to workload Kubernetes auth logins."
- type = number
- default = 86400
-}
-
-variable "kv_mount_path" {
- description = "Mount path for the workload KV-v2 engine."
- type = string
- default = "kv"
-}
-
-variable "postgres_admin_database" {
- description = "Administrative PostgreSQL database used by the database secret engine."
- type = string
- default = "postgres"
-}
-
-variable "postgres_host" {
- description = "DNS name of the platform PostgreSQL service."
- type = string
- default = "postgres-0.postgres.platform.svc.cluster.local"
-}
-
-variable "postgres_operator_default_ttl_seconds" {
- description = "Default TTL, in seconds, for the operator PostgreSQL role."
- type = number
- default = 3600
-}
-
-variable "postgres_operator_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for the operator PostgreSQL role."
- type = number
- default = 28800
-}
-
-variable "postgres_port" {
- description = "Port of the platform PostgreSQL service."
- type = number
- default = 5432
-}
-
-variable "postgres_superuser" {
- description = "PostgreSQL superuser used by Vault database secrets."
- type = string
- default = "postgres"
-}
-
-variable "seed_kv_mount_path" {
- description = "Mount path for the provider KV-v2 seed data."
- type = string
- default = "kv"
-}
-
-variable "transit_vault_addr" {
- description = "Address of the vault-transit API."
- type = string
- default = "http://127.0.0.1:18200"
-}
-
-variable "transit_vault_token" {
- description = "Token used to read provider seed data."
- type = string
- sensitive = true
-}
-
-variable "workflow_token_max_ttl_seconds" {
- description = "Maximum TTL, in seconds, for the workload workflow AppRole login token."
- type = number
- default = 14400
-}
-
-variable "workflow_token_ttl_seconds" {
- description = "Default TTL, in seconds, for the workload workflow AppRole login token."
- type = number
- default = 3600
-}
-
-variable "workload_vault_addr" {
- description = "Address of the workload Vault API."
- type = string
- default = "http://127.0.0.1:8200"
-}
-
-variable "workload_vault_token" {
- description = "Workflow token used to reconcile workload Vault."
- type = string
- sensitive = true
-}
diff --git a/apps/api-server/base/deployment.yaml b/workloads/api-server/base/deployment.yaml
similarity index 97%
rename from apps/api-server/base/deployment.yaml
rename to workloads/api-server/base/deployment.yaml
index fb4321d..38c7803 100644
--- a/apps/api-server/base/deployment.yaml
+++ b/workloads/api-server/base/deployment.yaml
@@ -2,6 +2,8 @@ apiVersion: apps/v1
kind: Deployment
metadata:
name: api-server
+ annotations:
+ argocd.argoproj.io/sync-wave: "10"
spec:
replicas: 1
selector:
diff --git a/apps/api-server/base/kustomization.yaml b/workloads/api-server/base/kustomization.yaml
similarity index 100%
rename from apps/api-server/base/kustomization.yaml
rename to workloads/api-server/base/kustomization.yaml
diff --git a/apps/api-server/base/service.yaml b/workloads/api-server/base/service.yaml
similarity index 100%
rename from apps/api-server/base/service.yaml
rename to workloads/api-server/base/service.yaml
diff --git a/apps/api-server/base/serviceaccount.yaml b/workloads/api-server/base/serviceaccount.yaml
similarity index 100%
rename from apps/api-server/base/serviceaccount.yaml
rename to workloads/api-server/base/serviceaccount.yaml
diff --git a/apps/auth-server/base/auth-db-migration-serviceaccount.yaml b/workloads/auth-server/base/auth-db-migration-serviceaccount.yaml
similarity index 62%
rename from apps/auth-server/base/auth-db-migration-serviceaccount.yaml
rename to workloads/auth-server/base/auth-db-migration-serviceaccount.yaml
index c434495..da8af47 100644
--- a/apps/auth-server/base/auth-db-migration-serviceaccount.yaml
+++ b/workloads/auth-server/base/auth-db-migration-serviceaccount.yaml
@@ -2,9 +2,6 @@ apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-db-migration
- annotations:
- argocd.argoproj.io/hook: PreSync
- argocd.argoproj.io/sync-wave: "-2"
automountServiceAccountToken: false
imagePullSecrets:
- name: ghcr-regcred
diff --git a/apps/auth-server/base/db-migration-job.yaml b/workloads/auth-server/base/db-migration-job.yaml
similarity index 95%
rename from apps/auth-server/base/db-migration-job.yaml
rename to workloads/auth-server/base/db-migration-job.yaml
index 3d1060b..65c111b 100644
--- a/apps/auth-server/base/db-migration-job.yaml
+++ b/workloads/auth-server/base/db-migration-job.yaml
@@ -3,10 +3,11 @@ kind: Job
metadata:
name: auth-db-migration
annotations:
- argocd.argoproj.io/hook: PreSync
+ argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation,HookSucceeded
- argocd.argoproj.io/sync-wave: "-1"
+ argocd.argoproj.io/sync-wave: "5"
spec:
+ activeDeadlineSeconds: 600
backoffLimit: 1
template:
metadata:
diff --git a/apps/auth-server/base/deployment.yaml b/workloads/auth-server/base/deployment.yaml
similarity index 98%
rename from apps/auth-server/base/deployment.yaml
rename to workloads/auth-server/base/deployment.yaml
index f54c091..a7a7c54 100644
--- a/apps/auth-server/base/deployment.yaml
+++ b/workloads/auth-server/base/deployment.yaml
@@ -3,7 +3,7 @@ kind: Deployment
metadata:
name: auth-server
annotations:
- argocd.argoproj.io/sync-wave: "0"
+ argocd.argoproj.io/sync-wave: "10"
spec:
replicas: 1
diff --git a/apps/auth-server/base/kustomization.yaml b/workloads/auth-server/base/kustomization.yaml
similarity index 100%
rename from apps/auth-server/base/kustomization.yaml
rename to workloads/auth-server/base/kustomization.yaml
diff --git a/apps/auth-server/base/service.yaml b/workloads/auth-server/base/service.yaml
similarity index 100%
rename from apps/auth-server/base/service.yaml
rename to workloads/auth-server/base/service.yaml
diff --git a/apps/auth-server/base/serviceaccount.yaml b/workloads/auth-server/base/serviceaccount.yaml
similarity index 100%
rename from apps/auth-server/base/serviceaccount.yaml
rename to workloads/auth-server/base/serviceaccount.yaml