refactor: 폴더 구조 변경

This commit is contained in:
donghyeon-ka
2026-08-02 00:22:19 +09:00
parent f9c463f87a
commit be2f8e4863
1869 changed files with 4565 additions and 295591 deletions
+76 -130
View File
@@ -1,147 +1,93 @@
# Architecture 상세
# Architecture
README 의 Architecture / Key Components 를 보충하는 문서. 폴더 구조 전체와 워크로드, 이미지 정책을 모은다.
## Ownership model
## 폴더 구조
Catalog는 환경이 아니라 workload ownership으로 나눕니다.
```
Project-Infra/
├── .gitignore # vault-init-keys.json 제외
├── .kube-linter.yaml # kube-linter 규칙 (컨텍스트 오탐 4종 제외)
├── README.md # 본 프로젝트 진입 문서
├── guide.md # 운영 절차서
├── docs/ # README 보조 분할 문서
├── k8s/
│ ├── base/
│ │ ├── managing/
│ │ │ ├── namespace/ # mnt namespace + PSS restricted 라벨
│ │ │ └── migration-flyway/ # Flyway Job (공식 이미지)
│ │ │
│ │ ├── app/ # 애플리케이션 워크로드 (소유권 = 개발팀)
│ │ │ ├── identity/auth/
│ │ │ │ ├── stateful/identity-postgres/
│ │ │ │ └── stateless/auth-server/
│ │ │ ├── storage/minio/stateful/minio/
│ │ │ └── test/stateless/test-server-{1,2,3}/
│ │ │
│ │ └── plugins/ # 플랫폼 플러그인 (다른 워크로드가 의존)
│ │ ├── vault/ # Vault StatefulSet (공식 이미지)
│ │ ├── docker-registry/ # Registry Deployment (공식 이미지)
│ │ ├── oauth2-proxy/ # ForwardAuth 용 auth proxy
│ │ └── vso/ # VSO CRD 리소스 (VaultConnection / VaultAuth / VaultStaticSecret)
│ │
│ ├── components/
│ │ └── forward-auth/ # oauth2-proxy + Traefik ForwardAuth 재사용 component
│ │
│ ├── overlays/
│ │ ├── dev/
│ │ │ ├── kustomization.yaml # dev 전체 집계 (namespace: mnt)
│ │ │ ├── networkpolicy-baseline.yaml # default-deny + DNS egress
│ │ │ ├── platform/
│ │ │ │ ├── traefik/ # HelmChartConfig + Middleware + TLSOption
│ │ │ │ ├── cert-manager/ # cert-manager v1.20.2
│ │ │ │ ├── cert-manager-issuers/ # letsencrypt-staging/prod ClusterIssuer
│ │ │ │ └── keycloak-operator/ # Keycloak Operator 26.6.1
│ │ │ ├── tls/ # cert-manager 적용 후 Certificate
│ │ │ ├── vault/ # Vault overlay + NetworkPolicy + storage patch
│ │ │ ├── registry/ # Registry overlay + NetworkPolicy + storage patch
│ │ │ ├── vso/ # VSO CRDs (Helm 설치 후 별도 apply)
│ │ │ ├── database/ # identity-postgres + VaultStaticSecret
│ │ │ ├── auth/ # auth-server + Ingress(project.com) + flyway
│ │ │ ├── keycloak/ # Keycloak CR + public Ingress
│ │ │ ├── keycloak-realm/ # KeycloakRealmImport (Git-managed realm/client)
│ │ │ ├── storage/ # minio + VaultStaticSecret + certConfig FQDN patch
│ │ │ └── test/ # test-server 1/2/3
│ │ ├── components/forward-auth/ # dev overlay 에 포함되는 ForwardAuth component
│ │ ├── staging/ # 의도적으로 비어둠
│ │ └── prod/ # 의도적으로 비어둠
│ │
│ └── scripts/
│ ├── bin/ # 사용자 진입점 (bootstrap.sh / teardown.sh)
│ ├── ci/validate.sh # kustomize + kubeconform + kube-linter
│ ├── lib/ # 공통 라이브러리 (common.sh / vault.sh)
│ └── tasks/ # 재사용 작업 (vault-init / vault-seed-apps / vso-install)
└── terraform/ # contracts 만 존재, 추후 구현
| Area | Owned resources |
| --- | --- |
| `gitops/apps/auth-server` | auth-server |
| `gitops/apps/identity-postgres` | identity-postgres |
| `gitops/apps/auth-migration` | versioned Flyway Job |
| `gitops/platform` | Vault, Keycloak, MinIO, registry와 operator |
| `gitops/platform/forward-auth` | oauth2-proxy와 Traefik ForwardAuth integration |
환경별 차이는 각 catalog의 `overlays/<env>`에 두고, rollout ownership은
`gitops/clusters/<env>/<cluster>/stages`가 가집니다. 따라서 경로만 보고
리소스 소유자와 실제 적용 단계를 구분할 수 있습니다.
## Deployment graph
```text
namespaces
|
00-platform ---- cert-manager / Keycloak Operator / Traefik policy
|
+---- Helm: MinIO Operator / Vault Secrets Operator
|
10-vault ---- Vault Running -> init/unseal/policies/roles -> KV seed
|
20-secrets ---- VaultConnection / VaultAuth / pre-data VaultStaticSecret
|
30-data ---- PostgreSQL / MinIO / Keycloak
|
+---- MinIO registry bucket/access-key provisioning -> Vault
|
35-registry ---- registry VaultStaticSecret / docker-registry
|
40-operations ---- auth-server-migrate-0-1-0 / platform-realm-v1
|
50-apps ---- auth-server / oauth2-proxy / ingress
```
## 네임스페이스 전략
`all/`은 이 그래프를 하나로 렌더하지만 실행 순서를 보장하지 않습니다.
따라서 validation 전용입니다.
`mnt` 단일 namespace. 학습 단계의 단순성 우선. 실무에서는 역할별 namespace(`auth`, `storage`, `security`, `registry`) 분리가 원칙이며, base 는 환경 중립이라 overlay 재구성으로 분리 가능하다.
## Runtime boundaries
- Pod Security Standards: `pod-security.kubernetes.io/enforce=restricted` (audit + warn 동시).
- 모든 리소스는 overlay 의 `namespace: mnt` 로 일괄 주입.
| Workload | Kind | Namespace | Base |
| --- | --- | --- | --- |
| Vault | StatefulSet | `mnt` | `gitops/platform/vault` |
| identity-postgres | StatefulSet | `mnt` | `gitops/apps/identity-postgres` |
| MinIO | Tenant CR | `mnt` | `gitops/platform/minio` |
| Keycloak | Keycloak CR | `mnt` | `gitops/platform/keycloak` |
| docker-registry | Deployment | `mnt` | `gitops/platform/registry` |
| auth-server | Deployment | `mnt` | `gitops/apps/auth-server` |
| oauth2-proxy | Deployment | `mnt` | `gitops/platform/forward-auth` |
| DB migration | Job | `mnt` | `gitops/apps/auth-migration` |
| realm import | KeycloakRealmImport | `mnt` | `gitops/apps/keycloak-realm-import` |
## 워크로드 목록
## One-shot operation contract
| 워크로드 | 종류 | 위치 | 참조 Secret |
|---|---|---|---|
| `identity-postgres` | StatefulSet | `base/app/identity/auth/stateful/` | `identity-postgres-superuser`, `keycloak-db`, `auth-server-db` |
| [`auth-server`](https://github.com/donghyeon-ka/project-auth-server/tree/develop) | Deployment | `base/app/identity/auth/stateless/` | `auth-server-db` |
| `keycloak` | Keycloak CR (Operator 생성 StatefulSet) | `overlays/dev/keycloak/` | `keycloak-db-operator`, `keycloak-bootstrap-admin-operator` |
| `minio` | Tenant CRD | `base/app/storage/minio/stateful/` | `minio-tenant-env` |
| `test-server-1/2/3` | Deployment | `base/app/test/stateless/` | — |
| `migration-flyway` | Job (PreSync / sync-wave=-1) | `base/managing/migration-flyway/` | `auth-server-db` |
| `vault` | StatefulSet | `base/plugins/vault/` | — |
| `docker-registry` | Deployment | `base/plugins/docker-registry/` | `docker-registry-basic-auth`, `docker-registry-pull-credentials` |
Flyway Job 이름은 migration release를 포함합니다:
`auth-server-migrate-0-1-0`. SQL을 변경해 새 migration release를 만들 때는
Job instance/name도 함께 올립니다. 같은 이름의 완료된 Job을 지웠다가
묵시적으로 재실행하지 않습니다.
auth-server / keycloak 모두 `jdbc:postgresql://identity-postgres:5432/<db>` 로 short name 접속 (같은 namespace).
KeycloakRealmImport도 `platform-realm-v1`처럼 versioned name을 씁니다.
Operator는 기존 import CR의 spec 변경을 일반 workload rollout처럼
재실행하지 않으므로, realm 변경은 새 version의 명시적 operation으로 냅니다.
### Flyway 실행 순서
## Namespace decision
dev overlay 가 migration-flyway Job 에 ArgoCD annotation 을 patch:
현재 lab은 `mnt` 단일 namespace입니다. 이는 운영 권장 구조가 아니라 기존
runtime을 깨지 않고 먼저 deployment lifecycle을 분리하기 위한 전환 단계입니다.
```
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: PreSync
```
역할별 namespace 분리는 다음을 원자적으로 바꿔야 합니다.
ArgoCD 배포 시 Job 이 앱보다 먼저 돌고 스키마 마이그레이션을 마친 뒤 `auth-server` 가 뜬다.
- Service DNS와 issuer/JWK/DB endpoint
- Vault Kubernetes auth의 bound ServiceAccount/namespace
- VSO destination Secret 위치
- cross-namespace NetworkPolicy
- Flyway와 DB init credential ownership
- operator watch namespace와 RBAC
### Keycloak hostname patch
따라서 단순 폴더 이동과 함께 수행하지 않습니다.
dev overlay JSON patch 가 Keycloak ConfigMap 에 다음을 주입:
## Environment meaning
- `KC_HOSTNAME=https://keycloak.dev.example.com`
- `KC_HOSTNAME_ADMIN=https://keycloak-admin.dev.example.com`
staging / prod 는 자체 hostname 을 overlay 에서 주입.
### MinIO certConfig.dnsNames
base 는 short name(`minio`, `minio-hl`) 만 둔다. dev overlay 에서 `minio.mnt.svc.cluster.local`, `*.minio-hl.mnt.svc.cluster.local` 을 patch — base 환경 중립성 원칙.
## 이미지 정책
| 구분 | 이미지 | 근거 |
|---|---|---|
| 공식 upstream | `hashicorp/vault:1.17.2` | HashiCorp 공식 |
| | `registry:2.8.3` | Docker library 공식 |
| | `postgres:16.4` | PostgreSQL 공식 |
| | `quay.io/keycloak/keycloak:26.6.1` | Keycloak Operator 26.6.1 관리 |
| | `minio/minio:RELEASE.2025-01-20T14-49-07Z` | MinIO 공식 |
| | `flyway/flyway:10.20.1` | Flyway 공식 |
| 사용자 개발 | `registry.example.com/auth-platform/auth-server:0.1.0` | 조직 개발 서비스 |
| | `registry.example.com/test-platform/test-server-{1,2,3}:0.1.0` | 조직 개발 서비스 |
prod 승격 시 공식 이미지도 digest pin(`@sha256:…`)으로 전환.
## Docker Registry
| 항목 | 값 |
|---|---|
| 이미지 | `registry:2.8.3` |
| 내부 서비스 | `docker-registry.mnt.svc.cluster.local:5000` |
| 외부 Ingress | `registry.project.com` (`/v2` only) |
| 인증 | 내부 Service 무인증, 외부 Ingress + kubelet pull 만 credential 사용 |
| 저장 | MinIO S3 bucket `docker-registry` |
### 인증 경계
Registry 자체 auth 는 켜지 않는다. 인증 경계는 두 곳:
- 외부 Ingress: Traefik `Middleware/docker-registry-basic-auth` 가 VSO 로 생성된 `docker-registry-basic-auth` Secret 의 htpasswd 를 검증
- 내부 pull: 앱 ServiceAccount 에 `docker-registry-pull-credentials` imagePullSecret
따라서 `docker-registry-ingress-traefik` NetworkPolicy + BasicAuth Secret + imagePullSecret 이 함께 있어야 push/pull 양쪽이 안전하다.
- `lab`: 폐기 가능한 K3s 검증 환경. stateful overlay의 local-path, HTTP
ingress, 단일 replica 허용.
- `staging`: production과 같은 보안·TLS·backup path의 승격 검증 환경.
- `prod`: HA, digest pin, backup/restore evidence, TLS, disruption budget가
준비되지 않으면 생성하지 않습니다.
+157
View File
@@ -0,0 +1,157 @@
# Repository Structure
## 설계 목표
이 구조는 특정 제품의 파일 배치보다 변경 주기와 소유권을 우선합니다.
- 한 리소스에는 한 명확한 소유자가 있다.
- 재사용 구현과 실제 배포 진입점을 분리한다.
- 소규모 구성은 선택 영역을 생략할 수 있다.
- 규모가 커져도 기존 경계를 바꾸지 않고 같은 종류의 leaf를 추가한다.
- 사람이 실행하는 명령과 CI 검증이 같은 진입점을 사용한다.
## 소유권 매트릭스
| 대상 | 소유 경로 | 직접 실행 여부 | 변경 주기 |
|---|---|---:|---|
| state backend, 초기 identity | `bootstrap/foundation` | 예 | 매우 낮음 |
| 네트워크, IAM, DNS, 클러스터 | `infrastructure/live` | 예 | 낮음 |
| 재사용 IaC 단위 | `infrastructure/components` | 아니요 | 중간 |
| 재사용 IaC 조합 | `infrastructure/stacks` | 아니요 | 중간 |
| GitOps 컨트롤러와 root 연결 | `bootstrap/gitops` | 예 | 낮음 |
| 클러스터 desired state | `gitops/clusters` | reconcile 진입점 | 지속적 |
| cluster-wide addon | `gitops/platform` | 아니요 | 중간 |
| 애플리케이션 배포 정의 | `gitops/apps` | 아니요 | 높음 |
| 정책과 tenant 정의 | `gitops/policies`, `gitops/tenants` | 아니요 | 중간 |
Catalog 영역(`components`, `stacks`, `platform`, `policies`, `tenants`, `apps`)은
직접 배포하지 않습니다. 실제 진입점이 필요한 항목만 조합해서 참조합니다.
## 의존 방향
```text
bootstrap/foundation
│ output
infrastructure/components ◀── infrastructure/stacks
▲ ▲
└──────── infrastructure/live ─┘
│ cluster endpoint/identity
bootstrap/gitops
│ root reference
platform ─┐
policies ─┼────────▶ gitops/clusters
tenants ─┤
apps ─┘
```
역방향 의존은 만들지 않습니다. 예를 들어 reusable component가 특정
`live/prod` 값을 읽거나, app base가 특정 cluster overlay를 참조하면 안 됩니다.
## Infrastructure 경계
### `components`
네트워크, identity, registry, Kubernetes cluster처럼 작고 응집된 재사용
단위입니다. Terraform/OpenTofu를 선택했다면 일반적으로 backend가 없는 child
module에 해당합니다.
### `stacks`
여러 component를 반복해서 같은 방식으로 조합할 때만 사용합니다. 작은 프로젝트는
이 계층 없이 `live`가 component를 직접 호출할 수 있습니다. stack이 다른 stack을
깊게 중첩하기보다는 live root에서 평평하게 조합하는 방식을 권장합니다.
### `live`
실제로 plan/apply하는 root입니다. leaf 하나는 다음을 만족해야 합니다.
- 독립된 state와 locking
- 명시적인 provider와 backend 설정
- 고정된 component/module/chart 버전
- 비밀이 아닌 환경 입력만 저장소에 커밋
- 출력값과 downstream contract 문서화
작은 구성은 `live/dev/cluster`로 충분합니다. 계정과 리전이 늘어나면
`live/<provider>/<account>/<region>/<environment>/<stack>`처럼 경로를 확장합니다.
자동화는 경로의 고정 깊이에 의존하지 말고 실행 가능한 root 파일을 기준으로
대상을 찾도록 작성합니다.
서로 다른 환경의 root가 상대 경로로 다른 환경 구현을 import하면 안 됩니다.
공유가 필요하면 versioned component나 명시적인 remote output/data contract를
사용합니다.
## GitOps 경계
### `clusters`
클러스터가 reconcile하는 유일한 진입점입니다. 공통 리소스를 복사하지 않고
platform, policy, tenant, app catalog에서 필요한 항목만 참조합니다.
현재 프로젝트의 `lab`은 아직 GitOps controller가 없으므로
`clusters/lab/main/namespaces``stages/*`를 bootstrap script가 순서대로 적용합니다.
`clusters/lab/main/all`은 reconcile root가 아닌 감사용 aggregate입니다.
작은 구성은 `clusters/dev/main`, 다중 리전 구성은
`clusters/<environment>/<region>/<cluster>` 형태를 사용할 수 있습니다. 여기에도
고정된 경로 깊이를 강제하지 않습니다.
### `platform`
cluster-wide controller와 addon을 둡니다. 예시는 다음과 같습니다.
- ingress/gateway, external DNS, certificate
- storage class/CSI, autoscaling
- metrics, logs, traces, alerting
- secret operator와 delivery controller
각 component는 `base`와 필요한 `overlays`를 같은 디렉터리 안에 응집시킵니다.
환경 차이는 전체 파일 복사 대신 Kustomize patch 또는 별도 values로 표현합니다.
### `policies`, `tenants`, `apps`
- `policies`: cluster-wide admission 규칙, 거버넌스와 예외
- `tenants`: 구체적인 namespace, RBAC, quota, limit range, NetworkPolicy
- `apps`: application source code가 아닌 배포 정의
애플리케이션 팀이 별도 저장소를 소유하면 `apps`에는 그 저장소/OCI artifact를
참조하는 GitOps 리소스만 둘 수 있습니다.
Cloud DNS zone/delegation과 cloud IAM role은 `infrastructure`가 소유합니다.
External DNS controller, 동적 record 요청과 Kubernetes ServiceAccount binding은
`gitops/platform`이 소유합니다. 두 계층 사이에는 zone ID, role ARN 같은
명시적인 output contract만 전달합니다.
## Bootstrap 경계
Bootstrap은 선언형 관리가 스스로 시작될 수 없는 최소 범위만 담당합니다.
- `foundation`: state backend, 최초 CI identity와 같은 선행 조건
- `gitops`: Flux 또는 Argo CD 중 선택한 컨트롤러 설치와 root reference
현재 `bootstrap/gitops`에는 구현이 없으며 controller 도입 전까지 lab의 staged
bootstrap이 이 역할을 대신합니다.
ingress, cert-manager, observability 같은 addon은 bootstrap이 아니라 GitOps가
소유합니다. bootstrap 이후의 변경을 계속 수동 명령으로 누적하지 않습니다.
## 규모 확장 기준
| 단계 | 추가하는 것 | 그대로 유지하는 것 |
|---|---|---|
| 소형 | 단일 live root, 단일 cluster root, 최소 platform | lifecycle/ownership 경계 |
| 중형 | reusable stack, staging/prod, 정책, 관측성 | component와 entrypoint 분리 |
| 대형 | 계정·리전별 state, 다중 cluster, tenants, CODEOWNERS | 한 리소스 한 소유자 |
| 조직 분리 | lifecycle/team별 repository 분리 가능 | 각 repository 내부의 동일한 계약 |
repository를 분리하는 시점은 폴더 수가 아니라 권한, 배포 주기와 소유 팀이
실제로 달라졌을 때입니다.
## 설계 참고 자료
- [Kubernetes: Kustomize를 이용한 선언형 객체 관리](https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/)
- [Flux: GitOps repository 구조](https://fluxcd.io/flux/guides/repository-structure/)
- [OpenTofu: reusable module](https://opentofu.org/docs/language/modules/)
- [OpenTofu: 평평한 module composition](https://opentofu.org/docs/language/modules/develop/composition/)
@@ -0,0 +1,37 @@
# 0001. 수명주기와 소유권 경계
- 상태: 승인
- 날짜: 2026-07-26
- 결정자: repository maintainers
## 배경
인프라 저장소는 규모가 커지면서 cloud provisioning, cluster bootstrap,
platform addon과 application 배포가 뒤섞이기 쉽습니다. 이 경우 동일 리소스를
여러 도구가 관리하거나, 작은 변경이 불필요하게 넓은 권한과 state를 요구합니다.
## 결정
저장소를 다음 세 수명주기로 분리합니다.
1. `bootstrap`: 선언형 관리가 시작되기 위한 최소 선행 조건
2. `infrastructure`: cloud/cluster 리소스 provisioning
3. `gitops`: Kubernetes desired state의 지속적 reconciliation
재사용 구현은 catalog 영역에 두고, `infrastructure/live`
`gitops/clusters`만 실제 환경 진입점으로 사용합니다. 하나의 리소스는 하나의
수명주기와 하나의 도구만 소유합니다.
## 결과
- state, 권한과 배포 실패 범위를 작게 유지할 수 있습니다.
- 소규모는 선택 디렉터리를 사용하지 않고도 시작할 수 있습니다.
- 계정, 리전과 클러스터가 늘어날 때 같은 leaf를 추가해 확장할 수 있습니다.
- 초기에는 디렉터리가 더 많아 보이지만 각 위치의 책임이 명확해집니다.
## 대안
- 환경별 전체 복사: 시작은 단순하지만 공통 변경의 drift와 중복이 빠르게 증가합니다.
- 도구별 최상위 폴더: 구현 도구는 잘 보이지만 리소스 소유권과 수명주기가 섞입니다.
- 모든 리소스를 단일 state로 관리: 작은 데모에는 가능하지만 권한과 장애 범위가
지나치게 커집니다.
+24
View File
@@ -0,0 +1,24 @@
# Architecture Decision Records
프로젝트의 장기 구조에 영향을 주는 선택은 ADR로 남깁니다.
파일명은 `NNNN-kebab-case-title.md`를 사용하고 다음 형식을 따릅니다.
```markdown
# NNNN. 제목
- 상태: 제안 | 승인 | 폐기 | 대체
- 날짜: YYYY-MM-DD
- 결정자: 팀 또는 역할
## 배경
## 결정
## 결과
## 대안
```
기존 결정을 바꿀 때 문서를 지우지 말고 새 ADR에서 이전 ADR을 대체했다고
표시합니다.
@@ -96,41 +96,38 @@ spec:
## 좋은 예시 2: multi-region prod overlay 디렉터리 (kr-main + kr-dr)
```text
k8s/
base/
app/
units/
identity/
auth/
gitops/
apps/
identity-auth/
base/
kustomization.yaml
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
overlays/
dev/
staging/
prod/
kr-main/
kustomization.yaml
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
plugins/
ingress-nginx/
cert-manager/
external-secrets/
managing/
flyway-migrate-identity/
overlays/
dev/
kustomization.yaml
staging/
kustomization.yaml
prod/
kr-main/
kustomization.yaml
patches/
auth-replicas.yaml
auth-resources.yaml
auth-topology-spread.yaml
kr-dr/
kustomization.yaml
patches/
auth-replicas.yaml
auth-image-pull-mirror.yaml
patches/
kr-dr/
kustomization.yaml
patches/
flyway-migrate-identity/
base/
overlays/
platform/
ingress-nginx/
cert-manager/
secret-delivery/
clusters/
dev/main/
staging/main/
prod/kr-main/
prod/kr-dr/
```
**왜 좋은가:**
@@ -375,7 +372,7 @@ spec:
image: registry.example.com/auth:1.24.3
```
**문제:** `selector.matchLabels`는 Deployment/StatefulSet에서 **immutable**이다. `version`은 배포마다 바뀌고 `environment`는 overlay가 주입한다 → 첫 배포 이후 재apply 시 `field is immutable` 에러로 영구 차단. selector에는 불변 3종(`name`/`instance`/`component`)만.
**문제:** `selector.matchLabels`는 Deployment/StatefulSet에서 **immutable**이다. `version`은 배포마다 바뀌고 `environment`는 overlay가 주입한다 → 첫 배포 이후 재apply 시 `field is immutable` 에러로 영구 차단. selector에는 불변 2종(`name`/`instance`)만.
---
+1 -1
View File
@@ -600,7 +600,7 @@ resources:
- aescbc:
keys:
- name: fallback-2026-q1
secret: c2VjcmV0LTMyLWJ5dGUtZmFsbGJhY2sta2V5LTIwMjZxMS1leGFtcGxl
secret: <kms-fallback-key-base64>
- identity: {}
```
+2 -1
View File
@@ -1,6 +1,7 @@
# db / migration 예시
모든 YAML은 `kubectl apply` 가능하다. 상세 Flyway Job 예시는 `examples/infra/flyway.md` 참조.
모든 YAML은 `kubectl apply` 가능하다. 상세 Flyway Job 예시는
`docs/examples/infra/flyway.md` 참조.
---
+1 -1
View File
@@ -105,7 +105,7 @@ metadata:
app.kubernetes.io/managed-by: keycloak-operator
spec:
instances: 3
image: registry.example.com/platform/keycloak:26.0.7-optimized
image: registry.example.com/platform/keycloak:26.0.7-optimized # gitleaks:allow
startOptimized: true
db:
vendor: postgres
+46 -42
View File
@@ -8,35 +8,39 @@ kubectl kustomize <dir> | kubectl apply --server-side --field-manager=ci --dry-r
---
## 좋은 예시 1: base / components / overlays 전체 구조 + 실제 base `kustomization.yaml`
## 좋은 예시 1: catalog unit의 base / components / overlays 구조
```text
k8s/
base/
app/units/identity/auth/
kustomization.yaml
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
components/
with-topology-spread-zone/
kustomization.yaml
patch.yaml
with-pdb-tier1/
kustomization.yaml
patch.yaml
overlays/
gitops/
apps/
identity-auth/
base/
kustomization.yaml
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
components/
with-topology-spread-zone/
kustomization.yaml
patch.yaml
with-pdb-tier1/
kustomization.yaml
patch.yaml
overlays/
prod/kr-main/
kustomization.yaml
patches/
auth-resources.yaml
auth-ingress-host.yaml
clusters/
prod/kr-main/
kustomization.yaml
patches/
auth-resources.yaml
auth-ingress-host.yaml
```
```yaml
# k8s/base/app/units/identity/auth/kustomization.yaml
# gitops/apps/identity-auth/base/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
@@ -66,7 +70,7 @@ labels:
## 좋은 예시 2: base Deployment (완전 apply-ready)
```yaml
# k8s/base/app/units/identity/auth/deployment.yaml
# gitops/apps/identity-auth/base/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -206,12 +210,12 @@ spec:
## 좋은 예시 3: overlay prod/kr-main — 환경 차이만
```yaml
# k8s/overlays/prod/kr-main/kustomization.yaml
# gitops/apps/identity-auth/overlays/prod/kr-main/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod-identity-auth
resources:
- ../../../base/app/units/identity/auth
- ../../../base
components:
- ../../../components/with-topology-spread-zone
- ../../../components/with-pdb-tier1
@@ -246,7 +250,7 @@ patches:
```
```yaml
# k8s/overlays/prod/kr-main/patches/auth-resources.yaml
# gitops/apps/identity-auth/overlays/prod/kr-main/patches/auth-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
@@ -302,7 +306,7 @@ spec:
## 좋은 예시 4: Kustomize Component — `with-pdb-tier1`
```yaml
# k8s/components/with-pdb-tier1/kustomization.yaml
# gitops/apps/identity-auth/components/with-pdb-tier1/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
@@ -310,7 +314,7 @@ resources:
```
```yaml
# k8s/components/with-pdb-tier1/pdb.yaml
# gitops/apps/identity-auth/components/with-pdb-tier1/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
@@ -343,7 +347,7 @@ spec:
## 좋은 예시 5: ConfigMap generator + hash suffix를 활용한 자동 rollout
```yaml
# k8s/base/app/units/identity/auth/kustomization.yaml (with generator)
# gitops/apps/identity-auth/base/kustomization.yaml (with generator)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
@@ -373,7 +377,7 @@ generatorOptions:
## 좋은 예시 6: HPA v2 + behavior (base 리소스)
```yaml
# k8s/base/app/units/identity/auth/hpa.yaml
# gitops/apps/identity-auth/base/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
@@ -433,12 +437,12 @@ spec:
## 나쁜 예시 1: `commonLabels`로 environment 주입 → selector immutable 에러
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD)
# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod-identity-auth
resources:
- ../../base/app/units/identity/auth
- ../../base
commonLabels:
example.com/environment: prod
```
@@ -450,10 +454,10 @@ commonLabels:
## 나쁜 예시 2: overlay가 base를 거의 재작성
```text
k8s/base/app/units/identity/auth/deployment.yaml (150 lines)
k8s/overlays/prod/deployment.yaml (140 lines, 95% identical)
k8s/overlays/staging/deployment.yaml (140 lines)
k8s/overlays/dev/deployment.yaml (135 lines)
gitops/apps/identity-auth/base/deployment.yaml (150 lines)
gitops/apps/identity-auth/overlays/prod/deployment.yaml (140 lines, 95% identical)
gitops/apps/identity-auth/overlays/staging/deployment.yaml (140 lines)
gitops/apps/identity-auth/overlays/dev/deployment.yaml (135 lines)
```
**문제:** overlay가 base의 95%를 복붙 + 몇 줄 수정. drift 발생 시점부터 base가 의미 없어진다. 해결: overlay는 `patches:` + `images:` + `replicas:` + `labels:`만 쓰고 전체 리소스는 base에서 가져온다.
@@ -463,7 +467,7 @@ k8s/overlays/dev/deployment.yaml (135 lines)
## 나쁜 예시 3: 운영 secret을 `secretGenerator`로 plaintext Git 커밋
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD)
# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD)
secretGenerator:
- name: auth-secrets
literals:
@@ -478,7 +482,7 @@ secretGenerator:
## 나쁜 예시 4: `patchesStrategicMerge` / `patchesJson6902` (deprecated)
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD, v5 deprecated)
# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD, v5 deprecated)
patchesStrategicMerge:
- patches/auth-resources.yaml
patchesJson6902:
@@ -497,7 +501,7 @@ patchesJson6902:
## 나쁜 예시 5: base에 환경 host / domain 고정
```yaml
# k8s/base/app/units/identity/auth/ingress.yaml (BAD)
# gitops/apps/identity-auth/base/ingress.yaml (BAD)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
@@ -523,11 +527,11 @@ spec:
## 나쁜 예시 6: `bases:` 사용 (v2.1에서 `resources:`로 통합됨)
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD)
# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base/app/units/identity/auth
- ../../base
```
**문제:** `bases:`는 v2.1에서 `resources:`에 흡수됨. 신규 코드에서 사용 금지. 해결: `resources:` 사용.
@@ -537,7 +541,7 @@ bases:
## 나쁜 예시 7: HPA가 있는 Deployment에 overlay `replicas:`로 고정값 주입
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD — conflicts with HPA)
# gitops/apps/identity-auth/overlays/prod/kustomization.yaml (BAD — conflicts with HPA)
replicas:
- name: auth
count: 3
+26 -48
View File
@@ -581,7 +581,7 @@ metadata:
namespace: minio-prod
type: Opaque
stringData:
token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..."
token: "<minio-prometheus-token>"
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
@@ -636,42 +636,31 @@ data:
#!/bin/sh
set -eu
mc alias set minio https://minio.minio-prod.svc.cluster.local "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --api S3v4
mc --config-dir /tmp/mc alias import minio <<EOF
{
"url": "https://minio.minio-prod.svc.cluster.local",
"accessKey": "$MINIO_ROOT_USER",
"secretKey": "$MINIO_ROOT_PASSWORD",
"api": "s3v4",
"path": "auto"
}
EOF
# Object Lock은 bucket 생성 시점에만 활성화 가능
mc mb --with-lock minio/critical-audit || true
mc retention set --default COMPLIANCE 2555d minio/critical-audit # 7년 보관
mc --config-dir /tmp/mc mb --with-lock minio/critical-audit || true
mc --config-dir /tmp/mc retention set --default COMPLIANCE 2555d minio/critical-audit
# Versioning + lifecycle
mc mb minio/app-data || true
mc version enable minio/app-data
mc ilm add --expire-noncurrent-days 90 minio/app-data
mc ilm add --expire-incomplete-upload-days 7 minio/app-data
mc --config-dir /tmp/mc mb minio/app-data || true
mc --config-dir /tmp/mc version enable minio/app-data
mc --config-dir /tmp/mc ilm add --expire-noncurrent-days 90 minio/app-data
mc --config-dir /tmp/mc ilm add --expire-incomplete-upload-days 7 minio/app-data
# SSE-KMS 기본 적용
mc encrypt set sse-kms minio-app-key minio/app-data
mc encrypt set sse-kms minio-critical-key minio/critical-audit
# Service account 발급 (앱 전용, 최소 권한 policy)
mc admin policy create minio auth-server-rw /policies/auth-server-rw.json
mc admin user svcacct add minio "$MINIO_ROOT_USER" \
--access-key "$AUTH_SERVER_ACCESS_KEY" \
--secret-key "$AUTH_SERVER_SECRET_KEY" \
--policy /policies/auth-server-rw.json || true
mc --config-dir /tmp/mc encrypt set sse-kms minio-app-key minio/app-data
mc --config-dir /tmp/mc encrypt set sse-kms minio-critical-key minio/critical-audit
echo "bootstrap complete"
auth-server-rw.json: |
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": ["s3:PutObject", "s3:GetObject", "s3:DeleteObject", "s3:ListBucket"],
"Resource": ["arn:aws:s3:::app-data/*", "arn:aws:s3:::app-data"]
}
]
}
---
apiVersion: batch/v1
kind: Job
@@ -703,21 +692,11 @@ spec:
secretKeyRef:
name: minio-root-creds
key: password
- name: AUTH_SERVER_ACCESS_KEY
valueFrom:
secretKeyRef:
name: auth-server-minio-svcacct
key: access_key
- name: AUTH_SERVER_SECRET_KEY
valueFrom:
secretKeyRef:
name: auth-server-minio-svcacct
key: secret_key
volumeMounts:
- name: scripts
mountPath: /scripts
- name: policies
mountPath: /policies
- name: tmp
mountPath: /tmp
securityContext:
runAsNonRoot: true
runAsUser: 1000
@@ -734,12 +713,10 @@ spec:
items:
- key: init.sh
path: init.sh
- name: policies
configMap:
name: minio-bootstrap
items:
- key: auth-server-rw.json
path: auth-server-rw.json
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 64Mi
```
**왜 좋은가:**
@@ -747,7 +724,8 @@ spec:
- `mc mb --with-lock`은 bucket 생성 시점에만 Object Lock 활성화 가능 — Job이 그 타이밍을 보장
- COMPLIANCE 모드 7년 retention = 감사/규제 요구 충족 (root도 bypass 불가)
- `app-data` bucket은 versioning + lifecycle (90일 noncurrent expire + 7일 incomplete abort)
- service account는 특정 bucket prefix만 접근 가능한 policy로 제한
- 애플리케이션 access key 발급은 별도 bootstrap에서 inline 최소 권한 policy
함께 수행하고, 생성 secret은 Vault로 직접 전달
- `backoffLimit: 3` + idempotent 명령 (`|| true`) → 재실행 안전
---
@@ -6,13 +6,13 @@
```bash
# 1) render
kubectl kustomize k8s/overlays/prod > /tmp/render.yaml
kubectl kustomize gitops/clusters/prod/main/stages/50-apps > /tmp/render.yaml
# 2) diff
kubectl diff -k k8s/overlays/prod
kubectl diff -k gitops/clusters/prod/main/stages/50-apps
# 3) apply
kubectl apply -k k8s/overlays/prod
kubectl apply -k gitops/clusters/prod/main/stages/50-apps
# 4) rollout status with timeout
kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m
@@ -607,10 +607,10 @@ kubectl get pods -A -o wide --field-selector spec.nodeName="${NODE}"
git checkout v1.24.0
# 2) diff
kubectl diff -k k8s/overlays/prod
kubectl diff -k gitops/clusters/prod/main/stages/50-apps
# 3) apply
kubectl apply -k k8s/overlays/prod
kubectl apply -k gitops/clusters/prod/main/stages/50-apps
# 4) rollout status
kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m
@@ -627,7 +627,7 @@ kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m
## 나쁜 예시 1: diff 없이 apply
```bash
kubectl apply -k k8s/overlays/prod
kubectl apply -k gitops/clusters/prod/main/stages/50-apps
```
**문제:**
+2 -2
View File
@@ -106,8 +106,8 @@ Environment:
CONFIRM=yes non-interactive confirmation (alternative to --yes)
Examples:
render-diff-apply --overlay k8s/overlays/prod --context prod-eu
CONFIRM=yes render-diff-apply --overlay k8s/overlays/prod --context prod-eu --timeout 15m
render-diff-apply --overlay gitops/clusters/prod/main/stages/50-apps --context prod-eu
CONFIRM=yes render-diff-apply --overlay gitops/clusters/prod/main/stages/50-apps --context prod-eu --timeout 15m
EOF
}
+1 -1
View File
@@ -216,7 +216,7 @@ spec:
- stateless 장기 실행 → Deployment 정답
- PDB + HPA + topologySpread (zone+hostname) 모두 tier-1에 맞게 동반
- digest pinning, restricted PodSecurity 호환, preStop sleep으로 graceful drain
- selector에는 불변 3종만 (version/environment 없음)
- selector에는 불변 2종만 (`name`/`instance`; version/environment 없음)
---
+146
View File
@@ -0,0 +1,146 @@
# Getting Started
## 현재 구현과 템플릿 확인하기
이 저장소에는 실제 `lab` 구성이 이미 조립되어 있습니다.
1. `gitops/clusters/lab/main`에서 namespace와 ordered stage 조립 방식을
확인합니다.
2. 다음 명령으로 platform과 app이 감사용 cluster root에서 합쳐지는 결과를
확인합니다. `all`은 render/schema/policy 감사 전용이며 직접 apply하지
않습니다.
```bash
kubectl kustomize gitops/clusters/lab/main/all >/dev/null
```
3. 다중 계정·리전·클러스터가 필요하면 account/project, region, cluster 경계를
ADR로 결정한 뒤 `infrastructure/live/<env>/<leaf>`와
`gitops/clusters/<env>/<cluster>`로 확장합니다.
4. 새 파일은 현재 구현을 참고하되 각 책임 폴더의 `_template`에서 생성합니다.
```text
현재 구현 확인 ──▶ 경계 결정 ──▶ _template 복사 ──▶ live/clusters 확장
```
## 1. 프로젝트 선택 기록
구현을 추가하기 전에 다음 항목을 결정하고 `docs/decisions`에 ADR을 작성합니다.
- cloud/on-prem provider와 account/project 구조
- Terraform, OpenTofu, Pulumi 등 IaC 엔진
- Kustomize 중심 또는 Helm 사용 범위
- Flux 또는 Argo CD 등 GitOps 컨트롤러
- External Secrets 또는 SOPS 등 비밀 관리 방식
- admission policy와 observability 운영 범위
선택하지 않은 도구의 빈 폴더를 모두 만들 필요는 없습니다.
Terraform/OpenTofu를 선택했다면 IaC 파일을 추가하기 전에 다음 선택 파일을
만들고 한 값만 활성화합니다.
```bash
cp infrastructure/.iac-engine.example infrastructure/.iac-engine
```
`.iac-engine`에는 주석을 제외하고 `tofu` 또는 `terraform` 한 줄만 남깁니다.
선택한 도구와 version을 CI에도 설치·고정하고 project-specific
`init -backend=false`/`validate` 검사를 추가합니다.
## 2. 프로젝트 메타데이터 설정
1. `README.md`의 제목과 프로젝트 범위를 바꿉니다.
2. `.github/CODEOWNERS.example`을 실제 소유자로 수정한 뒤 `CODEOWNERS`로
이름을 바꿉니다.
3. `SECURITY.md`에 조직의 보안 연락처와 SLA를 추가합니다.
4. 선택한 도구 버전을 프로젝트의 버전 관리 방식으로 고정합니다.
5. branch protection과 required check를 설정합니다.
## 3. Foundation bootstrap
`bootstrap/foundation` 아래에 remote state, locking, 초기 CI identity 등
다른 인프라가 의존하는 최소 구성을 작성합니다.
Foundation은 일반 infrastructure state와 분리하고 변경 권한을 좁게 유지합니다.
이미 조직 공통 foundation이 있다면 이 폴더에는 외부 의존 계약과 초기화 방법만
문서화해도 됩니다.
## 4. Infrastructure 작성
작은 프로젝트는 component와 live root만으로 시작합니다.
```bash
cp -R infrastructure/components/_template infrastructure/components/kubernetes-cluster
mkdir -p infrastructure/live/dev
cp -R infrastructure/live/_template infrastructure/live/dev/cluster
```
동일한 조합이 여러 환경에서 반복될 때만 stack을 추가합니다.
```bash
cp -R infrastructure/stacks/_template infrastructure/stacks/cluster
```
`live` root마다 backend/state를 분리하고, provider credential은 파일에 저장하지
않습니다.
## 5. Desired state 조립
필요한 catalog 템플릿을 복사합니다.
```bash
cp -R gitops/platform/_template gitops/platform/core
cp -R gitops/apps/_template gitops/apps/example-api
mkdir -p gitops/clusters/dev
cp -R gitops/clusters/_template gitops/clusters/dev/main
```
component의 `base`에 공통값을 두고, 환경 차이가 있을 때만 overlay를 추가합니다.
마지막으로 cluster `kustomization.yaml`이 사용할 component를 참조하게 합니다.
복사된 README의 `__REPLACE_ME_*__` 값을 모두 실제 메타데이터로 바꿉니다.
controller에 연결하기 전에 실제 cluster root를 로컬에서 렌더해 확인합니다.
## 6. GitOps bootstrap
desired-state root가 준비되고 클러스터가 생성되면 `bootstrap/gitops`에서 GitOps
컨트롤러 하나를 선택해 설치합니다. 이 단계에는 다음만 포함합니다.
- controller 설치 또는 설치 선언
- repository/OCI source 연결
- 검증된 `gitops/clusters/<...>` root reconcile 연결
- controller가 secret manager에 접근하는 최소 identity
일반 platform addon과 application은 이 단계에 넣지 않습니다.
## 7. 검증
```bash
make doctor
make check
kubectl kustomize gitops/clusters/lab/main/namespaces
kubectl kustomize gitops/clusters/lab/main/all >/dev/null
```
프로젝트에서 실제 IaC, Helm, policy 파일을 추가하면 필요한 validator를
`scripts/validate.sh`에 명시적으로 추가하고 CI에서도 같은 `make check`를
호출합니다. 도구가 없을 때 조용히 성공하도록 만들지 않습니다.
첫 환경은 다음 조건을 모두 만족하면 완료된 것으로 봅니다.
- 실제 `live` root의 대상, state, owner와 실행 절차가 작성되어 있다.
- 실제 cluster root가 필요한 catalog base/overlay를 참조하고 비어 있지 않다.
- cluster root 렌더 결과와 IaC plan이 리뷰 가능하다.
- GitOps bootstrap root가 `_template`이 아닌 실제 cluster root를 가리킨다.
- 비밀 관리, rollback과 담당자 연락 경로가 문서화되어 있다.
## 8. 운영 준비
- production apply 승인 및 concurrency lock
- backup/restore와 disaster recovery runbook
- cluster와 addon upgrade 정책
- secret rotation과 접근 감사
- alert routing과 담당자
- 비용, 용량, SLO 기준
운영 준비가 끝나기 전에는 템플릿 placeholder 값을 production에 재사용하지
않습니다.
+43 -228
View File
@@ -1,248 +1,63 @@
# Ingress / Traefik 운영
# Lab ingress and Traefik
dev 환경은 K3s packaged Traefik 을 그대로 유지한다. 단 **`/var/lib/rancher/k3s/server/manifests/traefik.yaml` 는 수정하지 않는다.** 운영 설정은 `k8s/overlays/dev/platform/traefik/``HelmChartConfig` 로만 오버라이드한다.
K3s packaged Traefik은 유지하며
`gitops/platform/traefik/overlays/lab/helmchartconfig.yaml`
`HelmChartConfig`로만 설정합니다.
## 현재 구성
금지:
| 위치 | 역할 |
|---|---|
| `k8s/overlays/dev/platform/traefik/helmchartconfig.yaml` | Traefik replica, 기본 ingressClass, HTTP→HTTPS redirect, metrics, 기본 TLS option 연결 |
| `k8s/overlays/dev/platform/traefik/middleware.yaml` | 공용 `security-headers` Middleware + `modern-tls` TLSOption |
| `k8s/overlays/dev/auth/ingress.yaml` | `project.com``auth-server` |
| `k8s/overlays/dev/keycloak/ingress-public.yaml` | `keycloak.dev.example.com` → Keycloak 공개 path (`/realms/`, `/resources/`, `/.well-known/`, `/js/`) |
| `k8s/overlays/dev/platform/cert-manager/` | cert-manager `v1.20.2` CRD/controller 설치 overlay |
| `k8s/overlays/dev/platform/cert-manager-issuers/` | `letsencrypt-staging` / `letsencrypt-prod` ClusterIssuer |
| `k8s/overlays/dev/platform/keycloak-operator/` | Keycloak Operator `26.6.1`. dev 제약상 `mnt` 에 설치해 `mnt` 의 Keycloak CR 을 watch. K8s API egress NetworkPolicy 포함 |
| `k8s/overlays/dev/tls/*.yaml` | cert-manager 설치 후 발급할 `Certificate` 리소스 |
| `k8s/components/forward-auth/` | oauth2-proxy + Traefik ForwardAuth 재사용 component |
| `k8s/overlays/dev/` | 기본 dev overlay. 현재 forward-auth component 를 직접 포함 |
| `k8s/overlays/dev/keycloak-realm/` | `KeycloakRealmImport` 로 realm/client 를 Git 관리 |
- `/var/lib/rancher/k3s/server/manifests/traefik.yaml` 직접 수정
- health/metrics/admin endpoint 공개
- 인증 없는 management ingress
- lab HTTP/self-signed 구성을 staging/prod로 승격
## 설계 원칙
- 앱은 `Ingress` 만 선언하고, 공통 보안 정책은 Traefik Middleware / TLSOption 으로 재사용
- Keycloak 은 외부 전체 공개가 아니라 **최소 공개 path** 만 연다. `/admin`, `/metrics`, `/health` 는 비공개
- TLS 리소스는 cert-manager + ClusterIssuer 적용 후 `tls/` overlay 에서 발급
- north-south ingress 는 `kube-system` 의 Traefik Pod 에서만 시작 → app NetworkPolicy 도 그에 맞춰 작성
## ForwardAuth variant
`k8s/components/forward-auth/` 는 oauth2-proxy + ForwardAuth Middleware 를 담은 Kustomize component 다. 현재 `k8s/overlays/dev/` 가 이 component 를 직접 포함한다.
구성:
- `oauth2-proxy` Deployment / Service / ConfigMap / VaultStaticSecret
- `project.com/oauth2/*` 경로용 Ingress
- `oauth2-proxy-auth` Traefik Middleware
- `auth-server` Ingress patch — `project.com/` 요청은 oauth2-proxy 를 거친 인증된 사용자만 통과
흐름: `Traefik ForwardAuth → oauth2-proxy → Keycloak`.
### 적용 전제
- `k8s/overlays/dev/keycloak-realm/` 또는 동등한 방법으로 `platform` realm + `auth-server-ingress` client 가 준비됨
- redirect URI: `https://project.com/oauth2/callback`
- Vault path `secret/oauth2-proxy/forward-auth``client_secret`, `cookie_secret` 저장
- `project.com`, `keycloak.dev.example.com` 이 실제 Traefik 진입점으로 해석됨
### 브라우저 접속 전제
curl 검증은 `--resolve project.com:443:<ingress-ip>``-k` 로 DNS/TLS 문제를 우회할 수 있다. 브라우저는 이 옵션이 없으므로 dev 환경에서 직접 접속하려면 운영자가 아래를 별도로 맞춰야 한다.
## Request path
```text
<ingress-ip> project.com
<ingress-ip> keycloak.dev.example.com
client
-> Traefik Ingress
-> ForwardAuth Middleware
-> oauth2-proxy
-> Keycloak authorization endpoint
-> oauth2-proxy callback/session
-> auth-server
```
예: Traefik `LoadBalancer` IP 중 하나가 `10.208.141.123` 이면 로컬 `/etc/hosts` 에 두 host 를 추가한다. dev overlay 는 현재 외부 ACME 발급 대신 `dev-selfsigned` ClusterIssuer 를 사용하므로 브라우저에서는 인증서 경고를 허용하거나 해당 인증서를 로컬 trust store 에 등록해야 한다. 공인 DNS 가 Traefik 진입점으로 향하고 ACME 인증서가 Ready 가 되면 이 임시 조치는 제거한다.
auth-server는 ForwardAuth 결과만 신뢰하지 않고 Keycloak JWT를 다시 검증합니다.
Ingress 인증과 API authorization은 서로 다른 경계입니다.
Chrome 에서 계속 실패하면 먼저 boundary 를 나눈다.
## Source paths
| Boundary | 확인 |
|---|---|
| 로컬 DNS | `getent hosts project.com keycloak.dev.example.com` 이 Traefik IP 를 반환해야 한다. |
| 브라우저 DNS cache | `/etc/hosts` 수정 후 Chrome 재시작 또는 `chrome://net-internals/#dns` 에서 cache clear. |
| TLS trust | `ERR_CERT_*` 가 나오면 dev self-signed 인증서를 허용하거나 trust store 에 등록한다. |
| 인증 redirect | `curl -k -D - --resolve project.com:443:<ingress-ip> https://project.com/swagger-ui.html``302 Location: https://keycloak...` 를 반환해야 한다. |
| 로그인 후 app route | 인증 후 `404 PRES-005` 는 ForwardAuth 실패가 아니라 auth-server 에 해당 route 가 없다는 뜻이다. |
| Path | Role |
| --- | --- |
| `gitops/platform/traefik/overlays/lab` | K3s HelmChartConfig와 공통 security headers |
| `gitops/platform/forward-auth/component` | oauth2-proxy, Middleware, protected ingress patch |
| `gitops/platform/forward-auth/overlays/lab` | component의 lab composition |
| `gitops/apps/auth-server/overlays/lab` | auth-server ingress와 issuer/JWK setting |
| `gitops/platform/keycloak/overlays/lab` | Keycloak public/admin ingress split |
| `gitops/apps/keycloak-realm-import/overlays/lab` | versioned realm/client import |
### 브라우저 검증 순서
## Apply boundary
dev ForwardAuth 를 브라우저에서 직접 확인할 때는 아래 순서로 진행한다. 중간 단계를 건너뛰면 "Chrome 이 안 된다" 만 보이고 어느 boundary 가 깨졌는지 알기 어렵다.
#### 1. Traefik 진입 IP 확인
개별 ingress 디렉터리를 임의 순서로 적용하지 않습니다.
```bash
kubectl -n kube-system get svc traefik \
-o jsonpath='{.status.loadBalancer.ingress[*].ip}{"\n"}'
export KUBE_CONTEXT_LAB='<expected-context>'
bash scripts/bin/bootstrap.sh lab
```
예상 예시:
bootstrap은 operator와 Keycloak 준비, realm import 완료 후 auth-server를
적용합니다.
```text
10.208.141.123 10.208.141.14
```
## Lab DNS/TLS
이 문서의 예시는 `10.208.141.123` 을 사용한다. 실제 클러스터에서 나온 IP 중 하나를 선택한다.
현재 host 값은 lab 검증용이며 공개 서비스 계약이 아닙니다. 브라우저 E2E를
하려면 운영자가 DNS/hosts와 인증서 trust를 별도로 맞춰야 합니다.
#### 2. curl 로 클러스터 경로 먼저 확인
staging/prod는 다음이 준비된 뒤 별도 overlay로 만듭니다.
브라우저를 열기 전에 curl 로 Traefik / oauth2-proxy / Keycloak boundary 가 살아있는지 확인한다.
```bash
curl -k -sS -L \
-D /tmp/project-infra-login.headers \
-o /tmp/project-infra-login.body \
--resolve project.com:443:10.208.141.123 \
--resolve keycloak.dev.example.com:443:10.208.141.123 \
https://project.com/swagger-ui.html
```
정상 신호:
```bash
sed -n '1,80p' /tmp/project-infra-login.headers
grep -o '<title>[^<]*' /tmp/project-infra-login.body
```
정상이라면 헤더에는 첫 응답 `HTTP/2 302``location: https://keycloak.dev.example.com/.../auth` 가 보이고, body title 은 아래처럼 나온다.
```text
<title>Sign in to platform
```
이 단계가 실패하면 브라우저를 볼 필요가 없다. 먼저 `docs/troubleshooting.md``ForwardAuth 로그인 E2E 검증 실패` 사건에서 해당 boundary 를 찾는다.
#### 3. 빠른 Chrome 임시 프로필로 확인
로컬 `/etc/hosts` 와 인증서 trust 를 건드리기 전에, Chrome 실행 옵션으로 DNS/TLS 를 임시 우회해 본다.
```bash
google-chrome \
--user-data-dir=/tmp/project-infra-chrome \
--ignore-certificate-errors \
--host-resolver-rules="MAP project.com 10.208.141.123, MAP keycloak.dev.example.com 10.208.141.123" \
https://project.com/swagger-ui.html
```
정상 흐름:
1. `https://project.com/swagger-ui.html` 접속
2. Traefik ForwardAuth 가 미인증 요청을 감지
3. `302` 로 Keycloak 로그인 화면 이동
4. `Sign in to platform` 화면 표시
5. 로그인 성공 후 `project.com` 으로 callback
이 방식으로 성공하면 Kubernetes / Traefik / oauth2-proxy / Keycloak 경로는 정상이다. 평소 Chrome 에서 안 되는 원인은 로컬 DNS cache, `/etc/hosts`, 인증서 trust, 기존 쿠키 중 하나다.
#### 4. 일반 Chrome 으로 볼 수 있게 hosts 등록
임시 Chrome 이 성공하면 로컬 OS resolver 를 맞춘다.
```bash
sudo tee -a /etc/hosts >/dev/null <<'EOF'
# Project-Infra dev ingress
10.208.141.123 project.com
10.208.141.123 keycloak.dev.example.com
EOF
```
확인:
```bash
getent hosts project.com keycloak.dev.example.com
```
두 host 가 선택한 Traefik IP 를 반환해야 한다.
#### 5. Chrome DNS cache / 기존 세션 정리
hosts 를 바꾼 뒤에도 Chrome 이 이전 DNS / 쿠키를 들고 있을 수 있다.
권장 순서:
1. `chrome://net-internals/#dns` 에서 DNS cache clear
2. `chrome://net-internals/#sockets` 에서 socket pools flush
3. `project.com`, `keycloak.dev.example.com` 사이트 데이터 삭제
4. Chrome 완전 종료 후 재시작
그래도 헷갈리면 아래처럼 새 임시 프로필을 쓰는 게 가장 빠르다.
```bash
google-chrome --user-data-dir=/tmp/project-infra-normal https://project.com/swagger-ui.html
```
#### 6. 인증서 경고 처리
dev overlay 는 현재 `dev-selfsigned` ClusterIssuer 로 TLS Secret 을 만든다. 따라서 일반 Chrome 에서는 인증서 경고가 뜰 수 있다.
검증 목적이면 고급 옵션에서 예외를 허용한다. 장기적으로 반복 검증할 예정이면 `project-com-tls`, `keycloak-dev-example-com-tls` 인증서를 로컬 trust store 에 등록한다.
이 경고는 dev self-signed 인증서 때문에 생기는 것으로, ForwardAuth 실패와는 다른 boundary 다.
#### 7. 로그인 후 결과 해석
로그인 후 `swagger-ui.html` 이 열리면 브라우저 검증은 성공이다.
로그인 후 `/api/me` 를 열어 `404 PRES-005` 가 나오면 이것도 ForwardAuth 실패가 아니다. 인증은 통과했고 auth-server 애플리케이션에 `/api/me` route 가 없다는 뜻이다.
판단 기준:
| 결과 | 의미 |
|---|---|
| Keycloak 로그인 화면이 뜸 | 미인증 redirect 정상 |
| 로그인 후 `project.com` 으로 돌아옴 | callback / token exchange / session cookie 정상 |
| `/oauth2/auth``202` | oauth2-proxy 세션 인증 정상 |
| auth-server 가 `404 PRES-005` 반환 | 인증 통과 후 application route 없음 |
| auth-server 가 `401` 반환 | Authorization header 또는 JWT validation boundary 문제 |
### 인증 실패 처리
Traefik ForwardAuth 는 `/oauth2/auth` 를 호출한다. oauth2-proxy 가 `401` 또는 `403` 을 반환하면 `oauth2-proxy-errors` Middleware 가 `/oauth2/start?rd={url}` 로 넘겨 로그인 흐름을 시작한다.
중요: Traefik errors middleware 는 기본적으로 원래 status code 를 유지할 수 있다. 그러면 oauth2-proxy 가 `Location` 을 내려도 브라우저는 `401` 응답을 자동 redirect 로 처리하지 않는다. dev 구성은 `statusRewrites``401`/`403``302` 로 바꿔 브라우저가 바로 Keycloak 로그인 화면으로 이동하게 한다.
## cert-manager / ClusterIssuer
repo 에 `k8s/overlays/dev/platform/cert-manager/``k8s/overlays/dev/platform/cert-manager-issuers/` 가 추가되어 있다.
- 설치 overlay: 공식 static install `v1.20.2`
- issuer overlay: ACME HTTP-01 용 `letsencrypt-staging` / `letsencrypt-prod`
source-of-truth 관점에서 cert-manager 도 이 repo 의 선언형 관리 대상. 단, **실제 인증서 발급은 DNS 가 Traefik 외부 진입점을 가리키고 80/443 도달이 가능해야** 완료된다.
```bash
kubectl apply -k k8s/overlays/dev/platform/cert-manager
kubectl apply -k k8s/overlays/dev/platform/cert-manager-issuers
kubectl apply -k k8s/overlays/dev/tls
```
운영 보정 필요: `admin@project.com` 은 실제 운영 수신 가능한 메일로 교체.
## Keycloak realm / client Git 관리
`k8s/overlays/dev/keycloak-realm/``KeycloakRealmImport``platform` realm 과 `auth-server-ingress` client 를 선언한다. `k8s/overlays/dev/keycloak/` 도 수제 `Deployment` 가 아니라 `Keycloak` CR 기반으로 전환되어 있다.
`KeycloakRealmImport` 는 같은 `mnt` namespace 의 `Keycloak/keycloak` 을 대상으로 동작한다.
### 적용 순서
```bash
kubectl apply -k k8s/overlays/dev/platform/keycloak-operator
# 기존 수제 Deployment/Service/ConfigMap/ServiceAccount keycloak* 정리
kubectl apply -k k8s/overlays/dev
kubectl apply -k k8s/overlays/dev/keycloak-realm
```
## 적용 범위
repo 가 커버하는 것:
- Traefik 운영 정책의 Git 관리
- app ingress host / path / policy 정의
- Traefik → app 방향 ingress allow NetworkPolicy
- TLS `Certificate` 선언 준비
- ForwardAuth variant 와 KeycloakRealmImport 선언
> 미완 항목(DNS / ACME 발급 / end-to-end 테스트)은 README 의 [Limitations](../README.md#limitations-honest-scope) 섹션을 참고.
- 실제 DNS ownership
- cert-manager Issuer/Certificate
- TLS redirect와 HSTS
- 신뢰 가능한 oauth2-proxy OIDC issuer
- registry ingress의 인증·TLS
+25 -25
View File
@@ -1,31 +1,31 @@
# NetworkPolicy 매트릭스
# Lab networking
단일 namespace(`mnt`) 내부에서도 서비스 간 트래픽을 **최소권한** 으로 제한한다. baseline 은 모든 Pod 의 ingress/egress 차단하고, 컴포넌트별로 필요한 경로만 명시적으로 연다.
Namespace `mnt``gitops/policies/baseline`에서 ingressegress를 모두 default-deny로
시작하고 kube-dns UDP/TCP 53만 공통 허용합니다.
## 정책 목록
| Owner | Allowed traffic |
| --- | --- |
| `gitops/apps/auth-server` | Traefik -> auth-server:8080; auth-server -> PostgreSQL:5432, Keycloak:8080 |
| `gitops/apps/auth-migration` | migration Job -> PostgreSQL:5432 |
| `gitops/apps/identity-postgres` | Keycloak, auth-server, migration Job -> PostgreSQL:5432 |
| `gitops/platform/minio` | MinIO peer; MinIO Operator; approved in-namespace clients |
| `gitops/platform/keycloak` | Traefik/auth-server ingress; PostgreSQL egress; Keycloak peer |
| `gitops/platform/registry` | Traefik/in-namespace ingress; MinIO egress |
| `gitops/platform/vault` | VSO controller ingress; kube-apiserver TokenReview egress |
| `gitops/platform/keycloak-operator` | kube-apiserver egress |
| 정책 파일 | 역할 |
|---|---|
| `overlays/dev/networkpolicy-baseline.yaml` | `default-deny-all` (전 Pod ingress/egress 기본 차단) + `allow-dns-egress` (kube-system/kube-dns 53) |
| `overlays/dev/database/networkpolicy.yaml` | identity-postgres ingress ← keycloak / auth-server / migration-flyway (5432) |
| `overlays/dev/auth/networkpolicy.yaml` | auth-server ingress ← `kube-system/traefik`(8080); egress → postgres(5432) + keycloak(8080); flyway egress → postgres(5432) |
| `overlays/dev/keycloak/networkpolicy.yaml` | keycloak ingress ← `kube-system/traefik`(8080) + auth-server(8080); egress → postgres(5432); Keycloak Pod 간 peer 통신 (Infinispan/JGroups) |
| `overlays/dev/storage/networkpolicy.yaml` | minio ingress ← `part-of=auth-platform`(9000); 자체 peer(9000/9001) |
| `overlays/dev/test/networkpolicy.yaml` | test-server 3 대 내부 상호 통신만 허용 |
| `overlays/dev/vault/networkpolicy.yaml` | vault ingress ← VSO Operator Pod(8200) |
| `overlays/dev/registry/networkpolicy.yaml` | docker-registry ingress ← namespace 내 전 Pod(5000) + `kube-system/traefik`(5000); egress → minio(9000) |
NetworkPolicy CIDR `10.43.0.0/16``10.208.141.0/24`는 lab K3s의 service
CIDR/control-plane subnet 계약입니다. 클러스터 설정이 다르면 overlay에서
명시적으로 변경해야 합니다. staging/prod로 복사하지 않습니다.
## 작성 규칙
정책 추가 순서:
- cross-namespace 참조가 필요한 항목(예: `kube-system/traefik`)은 `namespaceSelector` + `podSelector` 를 한 블록에 조합해 **AND 시맨틱** 으로 작성한다. 두 selector 를 별도 블록에 두면 OR 가 되어 정책이 헐거워진다.
- north-south ingress 는 `kube-system` 의 Traefik Pod 에서만 시작되므로, app 측 NetworkPolicy 도 실제 클러스터 기준으로 `kube-system` 을 허용해야 한다 (`ingressClassName=traefik` 만으로는 부족).
- baseline default-deny 가 켜져 있는 한, 새 워크로드를 올릴 때마다 ingress / egress **명시적으로** 추가해야 한다. 이게 의도된 마찰이다 (실수로 wide-open 으로 시작하지 않도록).
1. source/destination workload label과 namespace를 확인합니다.
2. 실제 named port와 protocol을 확인합니다.
3. 가장 좁은 pod/namespace selector로 ingress egress 양쪽을 작성합니다.
4. 해당 stage를 렌더하고 정적 검증합니다.
5. server-side dry-run과 diff 후 적용합니다.
6. 허용 요청과 차단되어야 할 요청을 모두 확인합니다.
## Keycloak Operator 추가 고려사항
Keycloak Operator 가 Keycloak Pod 를 만들고 watch 하기 때문에 default-deny 환경에서는 다음 두 가지를 NetworkPolicy 로 명시한다:
- Keycloak Operator 의 Kubernetes API egress (CR reconcile)
- Keycloak Pod 간 Infinispan/JGroups peer 통신 (cluster mode)
해당 정책은 `overlays/dev/keycloak/networkpolicy.yaml` 에 함께 들어 있다.
`ipBlock`은 Kubernetes API처럼 selector로 표현할 수 없는 lab 경계에서만
사용합니다. 넓은 RFC1918 CIDR 전체 허용을 기본값으로 만들지 않습니다.
+99 -62
View File
@@ -1,83 +1,120 @@
# 운영 / 검증
# Operations and validation
bootstrap, teardown, validate.sh, 환경별 차등 계획의 **설계 의도** 를 정리한 문서. 단계별 실제 실행 절차는 [guide.md](../guide.md) 에 있다.
## bootstrap 단계
`VaultConnection` / `VaultAuth` / `VaultStaticSecret` 은 VSO Helm 설치로 CRD 가 등록된 뒤에만 apply 할 수 있다. 그래서 `overlays/dev/vso/` 는 dev kustomization 집계에 포함되지 않으며, `bin/bootstrap.sh` 마지막 단계에서 별도로 `kubectl apply -k overlays/dev/vso/` 한다.
| Phase | 작업 | 의존하는 직전 상태 | 멱등 안전? |
|:---:|---|---|---|
| 0 | MinIO Operator Helm install (`tasks/minio-operator-install.sh`) | helm 가능한 클러스터 | ✅ `helm upgrade --install` |
| 1 | `kubectl apply -k base/managing/namespace/` (PSS restricted 라벨 선행) | — | ✅ `kubectl apply` |
| 2 | VSO-managed Secret 점검 | namespace 존재 | ⚠️ `RESET_STALE_SECRETS=yes` 옵션 시 파괴적 |
| 3 | `kubectl apply -k overlays/dev/` (vault + registry + 앱) | namespace + PSS 라벨 | ✅ `kubectl apply` |
| 4 | `vault-0` Pod Running 대기 | Phase 3 의 Vault StatefulSet | ✅ wait 만 |
| 5 | `tasks/vault-init.sh` (init / unseal / auth / policy×2 / role×2) | `vault-0` Running | ✅ 상태 체크 후 차이만 적용 |
| 6 | `tasks/vso-install.sh` (helm upgrade --install) | Vault auth/role 준비 | ✅ `helm upgrade --install` |
| 7 | `kubectl apply -k overlays/dev/vso/` (VaultConnection / VaultAuth / VaultStaticSecret) | Phase 6 의 VSO CRD 등록 | ✅ `kubectl apply` |
Phase 5 의 1 회성 셋업 흐름은 [secret-pipeline-bootstrap 시퀀스](diagrams/sequence/secret-pipeline-bootstrap.md), Phase 7 이후의 정상 reconcile 은 [secret-pipeline-runtime 시퀀스](diagrams/sequence/secret-pipeline-runtime.md) 참고.
## Bootstrap
```bash
# dev — 비밀번호를 프롬프트에서 무음 입력 (bash history 에 안 남음)
bash k8s/scripts/bin/bootstrap.sh dev
# teardown — 대화형 y/N
bash k8s/scripts/bin/teardown.sh dev
export KUBE_CONTEXT_LAB='<expected-context>'
bash scripts/bin/bootstrap.sh lab
```
## 스크립트 구조
각 stage는 `render -> server-side dry-run -> diff -> confirm -> apply` 순서로
실행됩니다. `CONFIRM=yes`는 비대화 환경에서만 사용하고 context 확인을
우회하지 않습니다.
`k8s/scripts/``bin / ci / lib / tasks` 4 축:
| Order | Entrypoint / task | Completion boundary |
| ---: | --- | --- |
| 1 | `namespaces` | Namespace Active |
| 2 | `00-platform` | cert-manager와 Keycloak Operator Available |
| 3 | MinIO/VSO Helm task | controller Ready, CRD registered |
| 4 | `10-vault` | `vault-0` Running |
| 5 | Vault init/unseal/policy/seed | KV와 Kubernetes auth 준비 |
| 6 | `20-secrets` | data 선행 destination Secret 생성 |
| 7 | `30-data` | PostgreSQL/MinIO/Keycloak Ready |
| 8 | MinIO registry provision task | bucket/access key 생성 후 Vault 기록 |
| 9 | `35-registry` | registry Secret 생성과 Deployment rollout 완료 |
| 10 | `40-operations` | Flyway Complete, RealmImport Done |
| 11 | `50-apps` | auth-server/oauth2-proxy rollout 완료 |
| 디렉토리 | 역할 |
|---|---|
| `bin/` | 사용자 진입점. `bootstrap.sh` / `teardown.sh` |
| `ci/` | CI / 로컬 검증. `validate.sh` (kustomize + kubeconform + kube-linter) |
| `lib/` | 공통 Bash 라이브러리. `common.sh` (strict mode / trap / log / confirm / retry / mask_secret) + `vault.sh` |
| `tasks/` | 재사용 작업. `vault-init.sh` / `vault-seed-apps.sh` / `vso-install.sh` |
`gitops/clusters/lab/main/all`은 절대 apply하지 않습니다.
모든 쉘 스크립트는 `set -Eeuo pipefail` + `IFS=$'\n\t'` + `trap_cleanup` 으로 공통 에러 처리. root token / registry BasicAuth 같은 민감 값은 **stdin 파이프** 로만 전달하고 stdout 에 찍지 않는다.
## Vault init material
## 검증 (validate.sh)
기본 lab 경로는 repo root의 ignored `vault-init-keys.json`입니다. 스크립트는
`0600`으로 쓰지만 암호화 파일은 아닙니다. `VAULT_KEYS_FILE`로 repo 밖의
안전한 위치를 지정하는 방식을 권장하며 prod에서는 필수입니다.
키, root token, password, MinIO secret key를 argv로 전달하지 않습니다.
unseal/login, JSON 조립, Vault 기록은 stdin 경로를 사용합니다.
`docker-registry/minio`는 일반 seed 대상이 아닙니다. `30-data`에서 MinIO가
Ready가 된 뒤 MinIO가 bucket-scoped access key를 생성하고, bootstrap task가
그 결과를 Vault에 기록합니다. `35-registry`는 그 이후에만 VSO CR과 registry
Deployment를 적용하므로 missing Secret 상태의 Pod를 만들지 않습니다.
## Secret rotation
VSO destination은 `overwrite: true`로 선언되어 Vault 변경을 Kubernetes
Secret에 반영합니다. auth-server와 oauth2-proxy는 지원되는 Secret 변경 시
rollout target을 사용합니다.
다음 credential은 외부 시스템 상태와 함께 회전해야 하므로 Vault 값만 바꾸면
안 됩니다.
- PostgreSQL role password
- Keycloak DB password
- MinIO access key credential
- registry basic-auth/pull credential
각 소비자와 backend credential을 순서대로 갱신하고 stage health를 확인하는
별도 rotation runbook이 필요합니다.
## Teardown
기본 teardown은 앱과 one-shot operation만 삭제합니다.
```bash
bash k8s/scripts/ci/validate.sh
bash scripts/bin/teardown.sh lab
```
3 단계:
데이터, Vault, namespace까지 삭제하려면 명시적으로 opt-in합니다.
1. 각 overlay 에 대해 `kustomize build` (환경 중립성 / patch 유효성)
2. 렌더 결과에 `kubeconform -strict -ignore-missing-schemas` (Kubernetes OpenAPI + Datree CRD catalog)
3. 렌더 결과에 `kube-linter lint --config .kube-linter.yaml` (securityContext / resources / PSS / image tag 등)
`.kube-linter.yaml`**블록 단위 분석으로 생기는 컨텍스트 오탐 4 종**(`dangling-service`, `non-existent-service-account`, `mismatching-selector`, `no-anti-affinity`) 만 제외한다. 나머지는 모두 활성.
목표 상태:
```
k8s/overlays/dev build=ok schema=ok lint=ok
k8s/overlays/dev/vso build=ok schema=ok lint=ok
```bash
DELETE_DATA=yes bash scripts/bin/teardown.sh lab
```
## 환경별 배포
공유 operator와 cluster-scoped 리소스까지 삭제하는 것은 전용 lab cluster에서만
사용합니다.
현재 `dev` overlay 만 완성. `staging` / `prod` 는 의도적으로 비어 있고 추후 확장 예정. validate.sh 는 `kustomization.yaml` 이 없는 환경을 자동 스킵한다 — 빈 overlay 가 CI 를 빨갛게 만들지 않기 위함.
```bash
DELETE_DATA=yes TEARDOWN_PLATFORM=yes \
bash scripts/bin/teardown.sh lab
```
### 계획된 환경별 차등
`FORCE_FINALIZERS=yes`는 정상 삭제가 반복해서 실패한 namespace 복구의 최후
수단입니다. orphaned volume과 controller state를 만들 수 있습니다.
| 리소스 | dev | staging | prod |
|---|---|---|---|
| Vault replicas / storage | 1 / 1Gi | 1 / 5Gi | 3 (HA Raft) / 20Gi |
| Registry replicas / storage | 1 / 5Gi | 1 / 10Gi | 2 / 50Gi |
| PostgreSQL retention policy | Delete | Retain | Retain |
| 이미지 tag 정책 | semver tag | semver tag | `@sha256:` digest pin |
| TLS | 비활성화 | cert-manager | cert-manager + HSTS |
## Validation
prod 승격 시 필수 작업:
```bash
make check
```
- Vault storage `file``raft` + KMS auto-unseal
- Postgres backup CronJob (Velero / pgBackRest)
- cert-manager ClusterIssuer 로 TLS 전환
- 이미지 tag → digest pin
로컬 profile은 render를 항상 수행하고 설치되지 않은 부가 도구는 알려준 뒤
건너뜁니다. full profile은 다음 도구가 모두 없으면 실패합니다.
- kustomize 또는 kubectl
- kubeconform
- kube-linter
- shellcheck
- shfmt
- gitleaks
```bash
mise install
VALIDATION_PROFILE=full make check
```
Gitea workflow는 full profile을 실행합니다. 검증 entrypoint의 source of truth는
`tests/kustomize-entrypoints.txt`입니다.
## External incident actions
현재 tree에서 민감 파일을 untrack/ignore하는 것만으로 과거 노출은 해결되지
않습니다. 다음 작업은 live Vault와 모든 협업자에게 영향을 주므로 repository
refactor와 분리합니다.
1. root token과 unseal/recovery material 회전
2. 영향 credential 전체 회전
3. 백업과 감사 로그에서 노출 범위 확인
4. 협업자에게 force-fetch/reclone 절차 공지
5. 승인된 maintenance window에서 원격 Git history 정리
+16
View File
@@ -0,0 +1,16 @@
# Runbooks
운영자가 긴급 상황에서도 그대로 실행할 수 있는 절차를 둡니다. 프로젝트를
운영하기 전에 최소한 다음 runbook을 준비합니다.
- foundation/state 접근 복구
- 실패한 plan/apply 복구와 state lock 처리
- GitOps controller 복구와 reconciliation 중지/재개
- cluster 및 핵심 addon upgrade/rollback
- secret rotation과 credential 노출 대응
- backup restore와 disaster recovery
- 인증서, DNS, ingress 장애 대응
- 관측성 또는 alert pipeline 장애 대응
각 문서는 `목적`, `사전 조건`, `영향`, `절차`, `검증`, `롤백`,
`에스컬레이션` 섹션을 포함해야 합니다.
+15
View File
@@ -0,0 +1,15 @@
# Runbook 제목
## 목적
## 사전 조건
## 영향
## 절차
## 검증
## 롤백
## 에스컬레이션
+4 -4
View File
@@ -127,7 +127,7 @@ root token 은 **비상시 (rekey / generate-root / 전체 복구) 전용**으
REPO_ROOT="$(pwd)" \
VAULT_ADMIN_USERNAME='alice' \
VAULT_ADMIN_PASSWORD='<초기 비밀번호>' \
bash k8s/scripts/tasks/vault-setup-admin.sh
bash scripts/tasks/vault-setup-admin.sh
```
이 스크립트가 수행:
@@ -185,7 +185,7 @@ vault token revoke <old-root-token>
REPO_ROOT="$(pwd)" \
VAULT_ADMIN_USERNAME='bob' \
VAULT_ADMIN_PASSWORD='<임시 pw>' \
bash k8s/scripts/tasks/vault-setup-admin.sh
bash scripts/tasks/vault-setup-admin.sh
# 제거
vault delete auth/userpass/users/bob
@@ -227,7 +227,7 @@ vault audit enable socket address=loki-syslog.monitoring.svc:514 socket_type=tcp
### 권장 — 대화형 입력
```bash
bash k8s/scripts/bin/bootstrap.sh dev
bash scripts/bin/bootstrap.sh lab
# 프롬프트에서 무음 입력 (echo 안 됨)
```
@@ -245,7 +245,7 @@ KEYCLOAK_DB_PASSWORD='...' \
AUTH_SERVER_DB_PASSWORD='...' \
KEYCLOAK_ADMIN_PASSWORD='...' \
MINIO_ROOT_PASSWORD='...' \
bash k8s/scripts/bin/bootstrap.sh dev
bash scripts/bin/bootstrap.sh lab
# 또는 세션 전체 history 비활성화
set +o history
+3 -3
View File
@@ -25,7 +25,7 @@
| 키 | 허용 값 |
| --- | --- |
| `example.com/environment` | `dev` \| `staging` \| `prod` |
| `example.com/environment` | `lab` \| `dev` \| `staging` \| `prod` |
| `example.com/owner-team` | 팀 slug (e.g., `auth-platform`, `sre`) |
| `example.com/cost-center` | 회계 코스트 센터 ID |
| `example.com/data-classification` | `public` \| `internal` \| `confidential` \| `restricted` |
@@ -149,7 +149,7 @@ spec:
1. **prod 환경**: `<registry>/<path>@sha256:<digest>` 형태 digest pin **필수**. 뮤터블 태그(`:1`, `:latest`, `:main`) 금지.
2. **staging**: digest 권장, 최소 semver tag(`:1.24.0`) 허용. 절대 `:latest` 금지.
3. **dev**: semver tag 허용, `:latest` 지양 (로컬 / 노드 cache invalidation 이슈).
3. **lab / dev**: semver tag 허용, `:latest` 지양 (로컬 / 노드 cache invalidation 이슈). `lab` 은 폐기 가능한 단일 클러스터 실험 환경에만 사용한다.
4. `imagePullPolicy`:
- digest 사용 시 `IfNotPresent` (이미지 콘텐츠는 immutable)
- 뮤터블 태그 사용 시 `Always`
@@ -265,7 +265,7 @@ spec:
이 문서의 규약은 CI 에서 기계 검증된다.
- 실행: `k8s/scripts/ci/validate-docs.sh`
- 실행: `scripts/ci/validate-docs.sh`
- Lint 설정 위치: `.kube-linter.yaml` (repo root)
- 목표 스코어:
- syntax 에러: **0**
@@ -12,7 +12,7 @@
이 문서의 목표:
- dev / staging / prod 환경 분리를 **label·namespace·selector 레벨에서** 일관되게 만든다
- lab / dev / staging / prod 환경 분리를 **label·namespace·selector 레벨에서** 일관되게 만든다
- 서비스별 리소스 소유권(팀·도메인·컴포넌트)을 label로 쿼리 가능하게 한다
- K3s packaged component와 사용자 AddOn을 혼동하지 않는다
- 멀티 서버에서 `manifests/` 디렉터리를 source-of-truth로 쓰는 사고를 원천 차단한다
@@ -35,11 +35,13 @@
기본 환경:
- `lab` — 폐기 가능한 실험 환경
- `dev`
- `staging`
- `prod`
필요 시 `sandbox` / `canary` / `dr`을 추가할 수 있으나 dev/staging/prod 의미를 흐리지 않는다.
`lab`은 운영 승격 단계가 아니다. 필요 시 `canary` / `dr`을 추가할 수 있으나
dev/staging/prod 의미를 흐리지 않는다.
각 리소스는 두 곳에 동시에 환경이 드러나야 한다.
@@ -95,7 +97,7 @@
well-known label 6종으로 표현되지 않는 축은 다음 키로 고정한다.
- `example.com/environment``dev|staging|prod|canary|dr`
- `example.com/environment``lab|dev|staging|prod|canary|dr`
- `example.com/team` — 소유 팀 (예: `identity-sre`)
- `example.com/tier``frontend|backend|data|platform`
- `example.com/data-classification``public|internal|confidential|restricted`
@@ -107,7 +109,7 @@ well-known label 6종으로 표현되지 않는 축은 다음 키로 고정한
- `app.kubernetes.io/environment` 사용 (well-known set에 없음)
- 도메인 없는 커스텀 키 (`environment: prod` 같은 top-level key)
### 6. selector에 들어가는 label은 **불변 3종만**
### 6. selector에 들어가는 label은 **불변 2종만**
Deployment / StatefulSet의 `selector.matchLabels`는 일단 apply 후 수정 불가다. 여기에는 운영 중 **절대 바뀌지 않는** 값만 넣는다.
@@ -115,11 +117,11 @@ Deployment / StatefulSet의 `selector.matchLabels`는 일단 apply 후 수정
- `app.kubernetes.io/name`
- `app.kubernetes.io/instance`
- `app.kubernetes.io/component`
금지 (selector에 넣지 말 것):
- `app.kubernetes.io/version` (배포 때마다 바뀜)
- `app.kubernetes.io/component` (역할 재분류 시 immutable selector 충돌)
- `app.kubernetes.io/managed-by` (툴 교체 시 drift)
- `example.com/environment` (overlay에서 주입되면 selector immutable 위반)
@@ -146,10 +148,11 @@ Deployment / StatefulSet의 `selector.matchLabels`는 일단 apply 후 수정
기본:
- Git repo의 `k8s/` 디렉터리가 SoT
- Git repo의 `gitops/` 디렉터리가 Kubernetes desired state의 SoT
- CI/ArgoCD/Flux가 `kubectl apply --server-side`로 push
- 서버별 scp / vim 절대 금지
- 멀티 서버 bootstrap AddOn도 Git 관리(예: `k8s/bootstrap/*`를 첫 서버에만 배치)
- 멀티 서버 bootstrap AddOn도 Git 관리(`bootstrap/`에서 최소 설치 후
`gitops/clusters/` root로 인계)
### 9. GitOps apply는 Server-Side Apply가 기본
@@ -168,20 +171,25 @@ kubectl diff --server-side -k <overlay>
이후 `kustomize.md`에서 상세히 다룬다. 이 문서에서는 원칙만 박는다.
- `k8s/base/` — 공통 shape, 환경-agnostic
- `k8s/overlays/{dev,staging,prod}/` — patches / images / replicas / resources / labels
- `gitops/{apps,platform,policies,tenants}/<unit>/base` — 공통 shape, 환경-agnostic
- 각 unit의 `overlays/{lab,dev,staging,prod}` — patches / images / replicas / resources / labels
- `gitops/clusters/<env>/<cluster>` — catalog를 선택하는 실제 rollout entrypoint
overlay는 base를 재작성하지 않는다. overlay diff가 100줄을 넘으면 base 설계 실패 신호다.
### 11. `app/managing/plugins` 책임 분리
### 11. catalog와 rollout 책임 분리
`k8s/base/` 하위는 다음 3축으로 고정한다.
`gitops/` 하위 소유권은 다음 축으로 고정한다.
- `app/units/<domain>/<service>/` — 애플리케이션 유닛 (auth, keycloak, test-server)
- `managing/` — Job/CronJob 운영 작업 (flyway-migrate, backup, restore, bootstrap admin)
- `plugins/`플랫폼 (ingress-controller, cert-manager, external-secrets, observability, policy)
- `apps/` — application-facing workload와 그 app이 독점 소유하는 data/operation
- `platform/` — 여러 app이 공유하는 platform service와 operator
- `policies/`admission, security와 governance policy
- `tenants/` — namespace, RBAC, quota와 tenant boundary
- `clusters/` — 환경/클러스터별 최종 조립과 rollout entrypoint
이 축은 **소유 팀이 다르다**는 가정 위에 있다. 각 축은 독립된 Git owner (CODEOWNERS)를 가진다.
stateful 여부보다 실제 lifecycle owner를 우선합니다. 예를 들어 app 전용
PostgreSQL과 Flyway는 해당 app catalog가, 공용 MinIO와 Vault는 platform이
소유합니다. 각 축은 독립된 Git owner(CODEOWNERS)를 가질 수 있습니다.
### 12. 상태 저장 / 외부 공개 범위를 architecture 단계에서 분류
@@ -247,50 +255,41 @@ K3s multi-server에서는 아래가 모든 서버에서 동일해야 한다(불
## 추천 디렉터리 구조
```text
k8s/
base/
app/
shared/
units/
identity/
auth/
kustomization.yaml
keycloak/
kustomization.yaml
data/
postgres-identity/
kustomization.yaml
managing/
flyway-migrate-identity/
backup-postgres/
plugins/
ingress-nginx/
cert-manager/
external-secrets/
kube-prometheus-stack/
overlays/
dev/
kustomization.yaml
staging/
kustomization.yaml
prod/
kustomization.yaml
region-kr-main/
region-kr-dr/
bootstrap/
k3s-addons-disabled/
scripts/
render.sh
diff.sh
apply.sh
bootstrap/
foundation/
gitops/
gitops/
apps/
auth-server/
base/
overlays/{lab,staging,prod}/
identity-postgres/
auth-migration/
platform/
ingress-nginx/
cert-manager/
secret-delivery/
policies/
baseline/
tenants/
identity/
clusters/
lab/main/stages/
staging/main/stages/
prod/kr-main/stages/
prod/kr-dr/stages/
scripts/
bin/
ci/
tasks/
```
## 프로젝트 기준 요약
- 환경 3종(`dev`/`staging`/`prod`) + namespace prefix 고정
- 환경 4종(`lab`/`dev`/`staging`/`prod`) + namespace prefix 고정
- well-known `app.kubernetes.io/*` 6개 + 자체 도메인 운영 label 필수
- `app.kubernetes.io/environment` 사용 금지, `example.com/environment`로 대체
- selector에는 불변 3종
- selector에는 불변 2종(`name`/`instance`)
- K3s packaged component는 초기에 disable 여부 결정, 직접 수정 금지
- `manifests/`는 apply sink, Git이 SoT
- `kubectl apply --server-side` GitOps 기본
+5 -2
View File
@@ -90,7 +90,8 @@ Flyway migration은 **독립 실행 단계**다.
- CI/CD 명시 단계
- 운영자 명시 실행 절차
장기 실행 Deployment에 넣지 않는다. Flyway 예시는 `examples/infra/flyway.md` 참조.
장기 실행 Deployment에 넣지 않는다. Flyway 예시는
`docs/examples/infra/flyway.md` 참조.
### 5. migration Job은 배포 흐름 안에서 app보다 먼저 실행
migration을 app보다 **선행**시키는 것은 manifest 메타데이터로 선언한다.
@@ -139,7 +140,9 @@ metadata:
4. `flyway info` (결과 확인)
5. app rollout
`validateOnMigrate=true` 기본값이 있더라도, 운영 runbook에서는 validate 단계를 **분리 Job** 또는 **initContainer**로 분리한다. `examples/infra/flyway.md` 참조.
`validateOnMigrate=true` 기본값이 있더라도, 운영 runbook에서는 validate 단계를
**분리 Job** 또는 **initContainer**로 분리한다.
`docs/examples/infra/flyway.md` 참조.
### 8. migration source는 Git이 source of truth
중요한 것은 아래다.
+1 -1
View File
@@ -75,7 +75,7 @@ Pod 내부에서 TLS 재암호화가 필요 없으면 Pod는 HTTP로 수신한
기본:
- `KC_DB=postgres`
- `KC_DB_URL=jdbc:postgresql://keycloak-db-rw:5432/keycloak` (CloudNativePG `-rw` RW endpoint 권장)
- `KC_DB_URL=jdbc:postgresql://keycloak-db-rw:5432/keycloak` (CloudNativePG `-rw` RW endpoint 권장) <!-- gitleaks:allow -->
- `KC_DB_USERNAME`, `KC_DB_PASSWORD` → Secret `secretKeyRef`
- Keycloak schema와 auth-server schema는 **다른 DB 또는 다른 database**로 분리
+64 -76
View File
@@ -57,10 +57,10 @@ Kustomize는 Kubernetes 리소스를 **template-free**로 조합하고 환경별
overlay 한 디렉터리의 `kustomization.yaml`은 짧아야 한다. diff가 몇 백 줄을 넘으면 base 설계 실패 신호.
권장 구조:
권장 unit overlay 구조:
```
overlays/prod/
gitops/apps/auth-server/overlays/prod/
kustomization.yaml
patches/
auth-replicas.yaml
@@ -70,21 +70,26 @@ overlays/prod/
postgres-storage.yaml
```
### 4. 디렉터리 구조는 base / components / overlays 3축
### 4. catalog unit과 cluster entrypoint를 분리
```
k8s/
base/
app/units/<domain>/<service>/
managing/<job>/
plugins/<platform>/
components/
<reusable-cross-cutting>/
overlays/
<env>/[region/]
gitops/
apps/<unit>/
base/
components/
overlays/<env>/
platform/<unit>/
base/
components/
overlays/<env>/
policies/<unit>/
tenants/<unit>/
clusters/<env>/<region-or-cluster>/
```
`components/`"Kustomize Components"로, 여러 overlay에서 재사용.
`components/`소유 unit 내부의 Kustomize Component입니다. 여러 catalog를
조립하는 최종 경계는 `clusters/`이며 catalog 디렉터리를 controller root로
직접 사용하지 않습니다.
### 5. `commonLabels` 금지, `labels:` 사용
@@ -108,15 +113,14 @@ labels:
기존 `commonLabels` 사용 코드는 migration plan을 세워 교체. selector에 이미 들어간 label이 있다면 해당 리소스를 **재배포** (delete + recreate) 없이는 변경 불가.
### 6. selector에는 불변 3종만
### 6. selector에는 불변 2종만
overlay에서 selector를 건드리지 않는다. selector에 허용되는 label은:
- `app.kubernetes.io/name`
- `app.kubernetes.io/instance`
- `app.kubernetes.io/component`
3종은 base에서 고정. overlay가 `labels:`로 추가하는 label은 반드시 `includeSelectors: false`.
2종은 base에서 고정. overlay가 `labels:`로 추가하는 label은 반드시 `includeSelectors: false`.
### 7. `patches:` (v5 스타일) 사용, `patchesStrategicMerge` / `patchesJson6902` 금지
@@ -146,7 +150,7 @@ patches:
multiple overlay에서 공통으로 끼워야 하는 변경(예: mTLS 활성화, sidecar 주입, monitoring label 추가)은 component로.
```
components/
gitops/apps/auth-server/components/
with-istio-sidecar/
kustomization.yaml # kind: Component
patches/
@@ -215,11 +219,15 @@ field manager 이름을 환경별로 통일해야 `managedFields` 충돌이 예
CI가 아래를 순서대로 실행:
```bash
kubectl kustomize overlays/prod > /tmp/rendered.yaml
kubectl kustomize gitops/clusters/prod/kr-main/all > /tmp/rendered.yaml
kubeconform -strict -summary -schema-location default -schema-location 'https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json' /tmp/rendered.yaml
kubectl diff --server-side --field-manager=ci -k overlays/prod
kubectl diff --server-side --field-manager=ci -k gitops/clusters/prod/kr-main/stages/50-apps
```
`all`은 schema/policy 감사용이고 diff/apply는 의존성이 준비된 개별 stage를
대상으로 합니다. 여러 stage 변경은 승인된 orchestrator/controller가 순서와
health gate를 보장해야 합니다.
- kubeconform / kubeval: schema validation
- kyverno / OPA Gatekeeper: policy validation (post-render)
- conftest: opa policy bundle 실행
@@ -254,62 +262,41 @@ v2.1에서 `bases:`가 `resources:`로 통합됨. 신규 파일에서 `bases:`
## 추천 폴더 구조
```text
k8s/
base/
app/
kustomization.yaml
units/
identity/
auth/
kustomization.yaml
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
keycloak/
kustomization.yaml
data/
postgres-identity/
kustomization.yaml
statefulset.yaml
service-headless.yaml
service.yaml
managing/
flyway-migrate-identity/
gitops/
apps/
auth/
base/
kustomization.yaml
job.yaml
backup-postgres/
kustomization.yaml
cronjob.yaml
plugins/
ingress-nginx/
cert-manager/
external-secrets/
kube-prometheus-stack/
fluent-bit/
components/
with-service-monitor/
with-pdb-tier1/
with-topology-spread-zone/
with-network-policy-deny-default/
overlays/
dev/
kustomization.yaml
staging/
kustomization.yaml
prod/
kr-main/
kustomization.yaml
patches/
kr-dr/
kustomization.yaml
patches/
scripts/
render.sh
diff.sh
apply.sh
validate.sh
deployment.yaml
service.yaml
servicemonitor.yaml
pdb.yaml
hpa.yaml
components/
with-service-monitor/
with-pdb-tier1/
with-topology-spread-zone/
overlays/{lab,staging,prod}/
postgres-identity/
base/
overlays/{lab,staging,prod}/
flyway-migrate-identity/
base/
overlays/{lab,staging,prod}/
platform/
ingress-nginx/
cert-manager/
secret-delivery/
policies/
network-policy-deny-default/
clusters/
lab/main/stages/
staging/main/stages/
prod/kr-main/stages/
prod/kr-dr/stages/
scripts/
bin/
ci/
```
## 프로젝트 기준 요약
@@ -317,9 +304,10 @@ k8s/
- Kustomize v5 문법 기준, `commonLabels` 금지, `labels:` 사용
- `patches:` 단일 키, `target:` + `path:` 또는 `patch:` inline
- `components:`로 cross-cutting 재사용
- selector에는 불변 3종만 (name / instance / component)
- selector에는 불변 2종만 (name / instance)
- generator는 configMap만 기본, secret은 External Secrets
- `kubectl apply --server-side --field-manager=<id>` 전제
- render + schema + policy 검증을 CI에서 강제
- base / components / overlays 3축 디렉터
- catalog unit의 base/components/overlays와 cluster entrypoint를 분
- `stages/`만 apply하고 `all/`은 render/schema/policy audit에만 사용
- overlay diff는 짧아야 한다 (base 재작성 금지)
+4 -1
View File
@@ -88,7 +88,10 @@
기본:
- `spec.configuration.name`에 root credential Secret (MINIO_ROOT_USER, MINIO_ROOT_PASSWORD)
- Vault KV에 root credential 저장, VSO로 Secret 동기화
- 앱용 access는 `mc admin user svcacct add` 로 service account 발급
- 앱용 access는 `mc admin accesskey create`로 서버 생성 credential을 발급하고
inline policy로 권한을 축소한다
- 생성 결과의 secret key는 한 번만 수신해 stdin으로 Vault에 기록하며,
`--secret-key` 또는 `mc admin user add`로 secret을 argv에 전달하지 않는다
- service account는 최소 권한 policy 바인딩
### 7. TLS는 기본 활성화
@@ -126,18 +126,17 @@ Flux 플랫폼에서는 `Kustomization.spec.dependsOn` 으로 순서를 명시
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: auth-server
name: prod-50-apps
namespace: flux-system
spec:
interval: 5m
path: ./k8s/overlays/prod
path: ./gitops/clusters/prod/main/stages/50-apps
prune: true
sourceRef:
kind: GitRepository
name: platform
dependsOn:
- name: cert-manager
- name: postgres-operator
- name: prod-40-operations
```
### 11. node 작업 (drain / cordon) 은 PDB 존중 흐름
+40 -36
View File
@@ -1,19 +1,23 @@
# 운영 중 만난 함정 9건 — 사건 카탈로그
K3s 기반 로컬 클러스터에서 Project-Infra 를 부트스트랩 / 운영하면서 실제로 만났던 사건들의 narrative 정리.
운영자 절차서 톤은 [`guide.md`](../guide.md) 의 15장 (`15.1` ~ `15.11`) 에 있고, 이 문서는 사건 단위로 "무엇을 보고 / 왜 그랬고 / 어떻게 풀었는지" 를 짧게 쓰기 위한 자료다.
> 이 문서는 과거 `dev` 단일-aggregate 시기의 incident 기록이다. node 이름,
> host, 인증서, 수동 apply 명령은 당시 상태를 설명하며 현재 실행 runbook이
> 아니다. 현재 배포와 복구는 `docs/operations.md`와 staged bootstrap을 따른다.
| # | 사건 | guide.md cross-ref |
|---|---|---|
| 1 | Registry image pull 실패 (`ImagePullBackOff`) | (별건 — guide.md 15.1 은 ContainerCreating 사건) |
| 2 | `vault-0``0/1 Running` 에서 멈춤 | [15.10](../guide.md#1510-vault-0-이-01-running-에서-멈춤) |
| 3 | `helm upgrade``has no deployed releases` 로 실패 | [15.11](../guide.md#1511-helm-upgrade-가-has-no-deployed-releases-로-실패) |
| 4 | VSO 가 기존 K8s Secret 을 덮어쓰지 않음 | [15.8](../guide.md#158-기존-k8s-secret-이-남아있을-때) |
| 5 | ForwardAuth 로그인 E2E 검증 실패 | (현장 검증 사건) |
| 6 | namespace 가 `Terminating` 에 걸림 | [15.9](../guide.md#159-namespace-가-terminating-에-걸림) |
| 7 | PodSecurity 위반 경고 (admission) | [15.7](../guide.md#157-podsecurity-위반-경고) |
| 8 | VSO 가 Vault 로그인 실패 | [15.2](../guide.md#152-vso-가-vault-에-로그인-실패) |
| 9 | Registry 는 살아났지만 auth-server 새 이미지 pull 이 끝나지 않음 | (해결됨 — registries.yaml 제거 + hosts.toml 직접 작성) |
K3s 기반 로컬 클러스터에서 Project-Infra 를 부트스트랩 / 운영하면서 실제로
만났던 사건을 "무엇을 보고 / 왜 그랬고 / 어떻게 풀었는지" 순서로 기록한다.
| # | 사건 |
| --- | --- |
| 1 | Registry image pull 실패 (`ImagePullBackOff`) |
| 2 | `vault-0``0/1 Running` 에서 멈춤 |
| 3 | `helm upgrade``has no deployed releases` 로 실패 |
| 4 | VSO 가 기존 K8s Secret 을 덮어쓰지 않음 |
| 5 | ForwardAuth 로그인 E2E 검증 실패 |
| 6 | namespace 가 `Terminating` 에 걸림 |
| 7 | PodSecurity 위반 경고 (admission) |
| 8 | VSO 가 Vault 로그인 실패 |
| 9 | Registry 는 살아났지만 auth-server 새 이미지 pull 이 끝나지 않음 |
---
@@ -133,8 +137,8 @@ Git/Kustomize 원천 파일을 수정했다.
변경 파일:
- `k8s/base/plugins/docker-registry/configmap.yaml`
- `k8s/overlays/dev/registry/networkpolicy.yaml`
- `gitops/platform/registry/base/configmap.yaml`
- `gitops/platform/registry/overlays/lab/networkpolicy.yaml`
변경 내용:
@@ -156,8 +160,8 @@ ports:
적용:
```bash
kubectl diff -k k8s/overlays/dev/registry
kubectl apply -k k8s/overlays/dev/registry
kubectl diff -k gitops/platform/registry/overlays/lab
kubectl apply -k gitops/platform/registry/overlays/lab
kubectl -n mnt rollout restart deployment/docker-registry
kubectl -n mnt rollout status deployment/docker-registry --timeout=180s
```
@@ -340,8 +344,8 @@ server = "http://registry.project.com"
skip_verify = true
[host."http://registry.project.com".auth]
username = "testuser"
password = "abcd6845"
username = "<registry-username>"
password = "<registry-password>"
EOF
# 3) 적용
@@ -360,7 +364,7 @@ sudo systemctl restart k3s # control-plane
```bash
# 노드의 registry 연결 직접 검증
sudo /usr/local/bin/k3s ctr -a /run/k3s/containerd/containerd.sock \
images pull --plain-http --user 'testuser:abcd6845' \
images pull --plain-http --user '<registry-username>:<registry-password>' \
registry.project.com/auth-platform/auth-server:manual-20260512071751
# Pod 가 새 이미지로 정상 Running 되는지
@@ -425,7 +429,7 @@ vault-0 0/1 Running 0 5m
### 해결
```bash
REPO_ROOT="$(pwd)" ENV_NAME=dev bash k8s/scripts/tasks/vault-init.sh
REPO_ROOT="$(pwd)" ENV_NAME=lab bash scripts/tasks/vault-init.sh
```
이 스크립트는 idempotent — 다음을 차례로 처리한다.
@@ -526,7 +530,7 @@ Vault KV 에 새 값을 넣었는데 K8s Secret 은 옛날 값을 유지. `Vault
bootstrap 스크립트에 명시적 opt-in 환경변수를 둠.
```bash
RESET_STALE_SECRETS=yes bash k8s/scripts/bin/bootstrap.sh dev
RESET_STALE_SECRETS=yes bash scripts/bin/bootstrap.sh lab
```
이 옵션이 있을 때만 VSO-managed K8s Secret 후보들을 선제 삭제 → VSO 가 새로 생성.
@@ -640,7 +644,7 @@ unknown TLS options: kube-system-modern-tls@kubernetescrd
Traefik packaged manifest 를 직접 수정하지 않고, Git source-of-truth 인 overlay 를 적용했다.
```bash
kubectl apply -k k8s/overlays/dev/platform/traefik
kubectl apply -k gitops/platform/traefik/overlays/lab
```
적용된 리소스:
@@ -680,17 +684,17 @@ dev 전용 `ClusterIssuer/dev-selfsigned` 를 추가하고, dev TLS `Certificate
변경 파일:
- `k8s/overlays/dev/platform/cert-manager-issuers/dev-selfsigned-clusterissuer.yaml`
- `k8s/overlays/dev/platform/cert-manager-issuers/kustomization.yaml`
- `k8s/overlays/dev/tls/project-com-certificate.yaml`
- `k8s/overlays/dev/tls/keycloak-dev-certificate.yaml`
- `k8s/overlays/dev/tls/registry-project-com-certificate.yaml`
- `gitops/platform/cert-manager/overlays/lab/issuers/dev-selfsigned-clusterissuer.yaml`
- `gitops/platform/cert-manager/overlays/lab/issuers/kustomization.yaml`
- `gitops/platform/cert-manager/overlays/lab/certificates/project-com-certificate.yaml`
- `gitops/platform/cert-manager/overlays/lab/certificates/keycloak-dev-certificate.yaml`
- `gitops/platform/cert-manager/overlays/lab/certificates/registry-project-com-certificate.yaml`
적용:
```bash
kubectl apply -k k8s/overlays/dev/platform/cert-manager-issuers
kubectl apply -k k8s/overlays/dev/tls
kubectl apply -k gitops/platform/cert-manager/overlays/lab/issuers
kubectl apply -k gitops/platform/cert-manager/overlays/lab/certificates
kubectl -n mnt wait --for=condition=Ready certificate/project-com --timeout=120s
kubectl -n mnt wait --for=condition=Ready certificate/keycloak-dev-example-com --timeout=120s
```
@@ -846,7 +850,7 @@ set_authorization_header = true
변경 파일:
- `k8s/components/forward-auth/oauth2-proxy-config.yaml`
- `gitops/platform/forward-auth/component/oauth2-proxy-config.yaml`
반영 후 oauth2-proxy 를 재시작했다.
@@ -1071,15 +1075,15 @@ spec:
type: RuntimeDefault
```
이 패턴은 [`docs/security-hardening.md`](./security-hardening.md) 의 체크리스트에 정리되어 있고, [`k8s/scripts/ci/validate.sh`](../k8s/scripts/ci/validate.sh) 가 `kube-linter` 로 회귀를 막는다.
이 패턴은 [`docs/security-hardening.md`](./security-hardening.md) 의 체크리스트에 정리되어 있고, [`scripts/ci/validate.sh`](../scripts/ci/validate.sh) 가 `kube-linter` 로 회귀를 막는다.
### 검증
```bash
bash k8s/scripts/ci/validate.sh
bash scripts/ci/validate.sh
# build=ok schema=ok lint=ok
kubectl apply -k k8s/overlays/dev
bash scripts/bin/bootstrap.sh lab
# Warning 없음
```
@@ -1123,10 +1127,10 @@ K8s 인증의 의존 그래프는 두 단:
```bash
# 1. ClusterRoleBinding 적용
kubectl apply -k k8s/overlays/<env>/vault/
kubectl apply -k gitops/platform/vault/overlays/<env>/
# 2. vault auth/kubernetes/config 설정 (idempotent)
REPO_ROOT="$(pwd)" ENV_NAME=<env> bash k8s/scripts/tasks/vault-init.sh
REPO_ROOT="$(pwd)" ENV_NAME=<env> bash scripts/tasks/vault-init.sh
```
`vault-init.sh` 는 다음을 자동으로 한다:
+4
View File
@@ -1,5 +1,9 @@
# Docs validation report
> Historical report for the documentation examples at the timestamp below.
> It is not the current repository gate. Use `make check`; current deployable
> entrypoints are listed in `tests/kustomize-entrypoints.txt`.
Generated: 2026-04-20T08:51:44Z
Tools: kubeconform v0.6.7, kube-linter v0.7.4, yq v4.44.3
CRD schemas: datreeio/CRDs-catalog (remote fetch)
+57 -84
View File
@@ -1,98 +1,71 @@
# Vault / VSO 상세
# Vault and Vault Secrets Operator
README 의 Vault·VSO 핵심 섹션을 보충한다. Kubernetes auth 초기화 명령, policy/role 매핑, VaultStaticSecret 카탈로그, dockerconfigjson `.auth` 이슈를 모은다.
## Ownership
## Vault 기본 정보
- Vault workload: `gitops/platform/vault`
- VSO Helm values: `gitops/platform/secret-delivery/base/helm/values.yaml`
- VaultConnection/VaultAuth: `gitops/platform/secret-delivery/base`
- lab secret declarations: `gitops/platform/secret-delivery/overlays/lab`
| 항목 | 값 |
|---|---|
| 이미지 | `hashicorp/vault:1.17.2` |
| 배포 | StatefulSet (replicas 1, file backend) |
| 실행 | `vault server -config=/vault/config/vault.hcl` |
| 포트 | 8200 (http) / 8201 (cluster) — 내부 ClusterIP, NodePort 없음 |
| 저장 | PVC 5Gi (dev overlay 1Gi patch) |
| UI 접근 | `kubectl -n mnt port-forward svc/vault 8200:8200` (외부 노출 금지) |
Vault KV-v2가 secret source of truth이며, workload는 VSO가 만든 Kubernetes
Secret만 소비합니다.
## 설계 결정
## Bootstrap order
- **file backend (학습 환경 전용)**: 단일 노드 + 학습 목적으로 `storage "file"`. HA 불가. prod 승격 시 `storage "raft"` + KMS 기반 auto-unseal 로 전환.
- **`disable_mlock = true`**: 컨테이너에 `IPC_LOCK` capability 를 부여하지 않고 PSS Restricted 프로필을 유지하기 위함. 대신 swap 이 꺼진 노드에서 실행해야 한다.
- **`tls_disable = 1`**: 단일 namespace 내부 통신만 발생하고 cert-manager 전에 부트스트랩이 끝나야 해서 현재는 비활성화. 클러스터 밖 노출 시 cert-manager 발급 인증서로 TLS 활성화 필수.
- **`api_addr: http://vault:8200` + `cluster_addr: http://vault:8201`**: 짧은 Service 이름. 모든 소비자가 같은 `mnt` namespace 에 있어 FQDN 불필요.
```text
VSO controller/CRD install
Vault workload Running
Vault init + unseal
Kubernetes auth + policy/role
KV seed
VaultConnection/VaultAuth/pre-data VaultStaticSecret apply
pre-data destination Secret wait
PostgreSQL/MinIO/Keycloak apply
MinIO registry access key generate -> Vault
registry VaultStaticSecret/apply
consumer workloads apply
```
## RBAC
이 순서 때문에 `all/` aggregate를 직접 apply할 수 없습니다.
ClusterRoleBinding `vault-tokenreview-binding``system:auth-delegator`. Vault 의 Kubernetes auth method 는 클라이언트(VSO 등)가 제출한 ServiceAccount JWT 를 `TokenReview` + `SubjectAccessReview` API 로 검증한다. 이 ClusterRoleBinding 이 없으면 VSO 로그인이 `permission denied` 로 실패한다.
## Access split
Vault Pod 의 ServiceAccount 는 `automountServiceAccountToken: true` (기본). Vault 는 `/var/run/secrets/kubernetes.io/serviceaccount/{token,ca.crt}` 를 읽어 `auth/kubernetes/config``token_reviewer_jwt` / `kubernetes_ca_cert` 를 채운다.
| VaultAuth | Policy | Read paths |
| --- | --- | --- |
| `vault-auth-auth-platform` | `vso-auth-platform` | `identity-postgres/*`, `auth-server/*`, `keycloak/*`, `oauth2-proxy/*` |
| `vault-auth-storage` | `vso-storage` | `minio/*`, `docker-registry/*` |
## Kubernetes auth 초기 설정
현재 lab은 두 auth boundary가 같은 namespace와 ServiceAccount를 공유합니다.
실무 namespace 분리 시 workload/domain별 ServiceAccount, VaultAuth, policy,
bound namespace를 함께 분리해야 합니다.
`tasks/vault-init.sh` 가 수행:
## Secret catalog
| Domain | Kubernetes Secret |
| --- | --- |
| PostgreSQL | `identity-postgres-superuser`, `keycloak-db`, `auth-server-db` |
| Keycloak | `keycloak-db-operator`, `keycloak-bootstrap-admin-operator`, `keycloak-client-auth-server-ingress` |
| oauth2-proxy | `oauth2-proxy-secrets` |
| MinIO | `minio-tenant-env` |
| registry | `docker-registry-minio`, `docker-registry-basic-auth`, `docker-registry-pull-credentials` |
VSO destinations use `overwrite: true`. auth-server와 oauth2-proxy는 지원되는
Secret 변경에 rollout target을 선언합니다. DB/MinIO/registry credential은
backend 상태를 먼저 바꾸는 조정된 rotation이 필요합니다.
`docker-registry-minio`는 MinIO가 생성한 access key를 Vault에 기록한 뒤
`35-registry`에서 동기화합니다. 다른 정적 seed와 같은 시점에 미리 만들지
않습니다.
## Key material
`vault-init-keys.json`에는 root token과 unseal material이 들어가므로 Git에
들어가면 안 됩니다. 기본 lab 파일도 평문이며 단지 ignored/0600일 뿐입니다.
```bash
vault auth enable kubernetes # idempotent 체크
vault write auth/kubernetes/config \
kubernetes_host="https://kubernetes.default.svc.cluster.local:443" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
token_reviewer_jwt=@/var/run/secrets/kubernetes.io/serviceaccount/token
vault secrets enable -path=secret kv-v2 # idempotent 체크
# policy 2개 (역할별 least-privilege)
vault policy write vso-auth-platform - \
# identity-postgres/* + auth-server/* + keycloak/* read
vault policy write vso-storage - \
# minio/* read
# role 2개 (같은 SA, 다른 policy)
vault write auth/kubernetes/role/vso-auth-platform \
policies=vso-auth-platform bound_sa=vault-secrets-operator/mnt ttl=1h
vault write auth/kubernetes/role/vso-storage \
policies=vso-storage bound_sa=vault-secrets-operator/mnt ttl=1h
VAULT_KEYS_FILE=/secure/path/lab-vault-init.json \
bash scripts/bin/bootstrap.sh lab
```
## VaultAuth / VaultStaticSecret 매핑
policy 분리에 따라 VaultAuth CR 도 2 개. 각 VaultStaticSecret 은 자기 도메인의 VaultAuth 를 참조한다:
| VaultAuth CR | Vault role | 참조 VaultStaticSecret |
|---|---|---|
| `vault-auth-auth-platform` | `vso-auth-platform` | `identity-postgres-superuser`, `keycloak-db-creds`, `auth-server-db-creds`, `keycloak-bootstrap-admin` |
| `vault-auth-storage` | `vso-storage` | `minio-tenant-env` |
VSO Operator SA(`vault-secrets-operator`) 는 한 개이지만 Vault 쪽에서 role 별 policy 가 분리되어 있다. auth-platform 토큰이 유출돼도 MinIO secret 은 보호된다.
## VSO 가 관리하는 Secret 카탈로그
Registry 는 auth 없이 운영(NetworkPolicy 로 `mnt` 내부 전용 보호)이라 base 에는 VaultStaticSecret 이 없다. dev overlay 에서 BasicAuth / pull credential 두 개를 추가한다.
| VaultStaticSecret (dev overlay) | Vault 경로 | K8s Secret | 소비 방식 |
|---|---|---|---|
| `identity-postgres-superuser` | `secret/identity-postgres/superuser` | `identity-postgres-superuser` | file mount (`/run/secrets/superuser/`) → `POSTGRES_USER_FILE`, `POSTGRES_PASSWORD_FILE` |
| `keycloak-db-creds` | `secret/keycloak/db` | `keycloak-db` | file mount — postgres initdb + keycloak `KC_DB_PASSWORD_FILE` |
| `auth-server-db-creds` | `secret/auth-server/db` | `auth-server-db` | file mount — `SPRING_CONFIG_IMPORT=configtree:/etc/secrets/` + Flyway sh wrapper |
| `keycloak-bootstrap-admin` | `secret/keycloak/bootstrap-admin` | `keycloak-bootstrap-admin` | file mount — `KC_BOOTSTRAP_ADMIN_{USERNAME,PASSWORD}_FILE` |
| `minio-tenant-env` | `secret/minio/tenant-env` | `minio-tenant-env` | MinIO Operator `spec.configuration.name` (env file) |
| `docker-registry-basic-auth` (dev) | `secret/docker-registry/basic-auth` | `docker-registry-basic-auth` | Traefik Middleware basicAuth |
| `docker-registry-pull-credentials` (dev) | `secret/docker-registry/pull-cred` | `docker-registry-pull-credentials` | imagePullSecret (`.dockerconfigjson`) |
모든 VaultStaticSecret 은 `destination.overwrite` 기본값(`false`) 사용. 기존 Secret 이 수동으로 존재하면 VSO 가 덮어쓰지 않는다 (소유권 경합 방지).
> **Vault 값 교체 후 즉시 반영**: `kubectl -n mnt delete secret <name>` 으로 기존 Secret 을 지우면 VSO 가 다음 reconcile 에 새 값으로 재생성한다.
`refreshAfter: 1h` — Vault 값 변경 시 1 시간 내 K8s Secret 에 자동 반영.
## dockerconfigjson `.auth` 필드
Docker 공식 config 스키마는 `.auth = base64("<username>:<password>")` 형태다. 기존에는 password 만 base64 하던 버그가 있었고 현재는 다음으로 수정되어 있다:
```
{{ printf "%s:%s" username password | b64enc }}
```
Docker daemon 이 Registry 에 로그인할 때 이 필드를 디코드하므로 정확한 포맷이 필수.
## VaultConnection address
base 는 `http://vault:8200` (짧은 이름) 만 둔다. VSO Operator Pod 가 같은 `mnt` namespace 에 있으면 Kubernetes DNS 가 짧은 이름을 해결한다. 다른 namespace 에서 운영할 때는 overlay 에서 FQDN(`http://vault.mnt.svc.cluster.local:8200`) 으로 patch.
과거 Git 이력에 포함된 값은 삭제만으로 복구되지 않습니다. live credential
회전과 원격 history 정리는 별도 incident response로 수행합니다.