init: 폴더구조 설계 및 인프라 설계

This commit is contained in:
DongHyeonka
2026-07-24 14:31:53 +09:00
parent 34ad612281
commit f9c463f87a
1839 changed files with 323096 additions and 1 deletions
+105
View File
@@ -0,0 +1,105 @@
# AGENTS.md
Read order:
1. `/AGENTS.md`
2. nearest nested `AGENTS.md`
3. `/docs/standards/infra/STYLE.md` (normative labels/naming/ports/images/resources — always read before any manifest work)
4. relevant `/docs/standards/infra/**`
5. relevant `/docs/examples/infra/**`
6. current request
> **WARNING TO AI AGENTS**: You MUST READ the files in `/docs/standards/infra/` and `/docs/examples/infra/` using your file reading tools BEFORE proposing or writing any K8s manifests. DO NOT rely on generic Kubernetes knowledge. The standard documents contain mandatory strict rules (e.g., probe settings, security contexts, non-root constraints, resource limits, namespace strategies) that MUST be hardcoded into your output. Generating plain boilerplate YAML without strict standards compliance is a critical failure.
Repo role:
- this repository owns Kubernetes/K3s infrastructure source
- source of truth is Git + Kustomize directories under `k8s/`
- scripts are helper tools, not the source of truth
- runtime cluster state or server-local manifest files are not authoritative
Primary directories:
- `k8s/base/`: environment-neutral Kustomize bases
- `k8s/base/app/`: application-facing workload units
- `k8s/base/managing/`: management/operations units such as bootstrap, migration, backup, restore, and admin jobs
- `k8s/base/plugins/`: platform/plugin-style base resources
- `k8s/overlays/<env>/`: environment overlays such as `dev`, `staging`, and `prod`
- `k8s/scripts/`: render, diff, apply, validate, backup, and restore helpers
- `docs/standards/infra/`: infra standards
- `docs/examples/infra/`: approved examples
Structure principle:
- `k8s/base` is organized by operational ownership and workload role, not by environment.
- `k8s/overlays` is organized by environment first, so large fleets can be rendered, diffed, applied, and audited environment-by-environment.
- This repo intentionally does not use the small-project “each service owns its own base and overlays” pattern as the primary layout.
- Service ownership still exists under `k8s/base/app/<domain>/<workload>/**`; environment rollout ownership lives under `k8s/overlays/<env>`.
Hard bans:
- do not treat `/var/lib/rancher/k3s/server/manifests` as source of truth
- do not edit K3s packaged component manifests directly
- do not put production secrets in Git/plain manifests
- do not bypass Kustomize with ad-hoc generated YAML as the primary path
- do not mix app rollout, DB migration, and control-plane upgrade in one opaque step
- do not expose health, metrics, admin, or management endpoints publicly by default
- do not use `hostPath` as an operating default
- do not use `start-dev`/dev-mode style configs for production components
- do not rely on default namespace for production workloads
Global routing:
- environment / namespace / source-of-truth / K3s packaged components
-> `/docs/standards/infra/architecture-environments.md`
- config / secret / Vault delivery strategy
-> `/docs/standards/infra/config-and-secrets.md`
- workload kind selection
-> `/docs/standards/infra/workload-selection.md`
- storage / PVC / storage class / retention
-> `/docs/standards/infra/storage-pvc.md`
- network / service / ingress / TLS
-> `/docs/standards/infra/network-ingress-tls.md`
- resources / probes / quota / PDB / HPA
-> `/docs/standards/infra/resources-probes-availability.md`
- backup / restore
-> `/docs/standards/infra/backup-restore.md`
- security hardening / RBAC / network policy / pod security
-> `/docs/standards/infra/security-hardening.md`
- operational procedure / upgrade / rollback
-> `/docs/standards/infra/operations-runbook-upgrade-rollback.md`
- observability / health / metrics / logs
-> `/docs/standards/infra/observability-health.md`
- database / PostgreSQL / migration ownership / Flyway flow
-> `/docs/standards/infra/db-and-migration.md`
- K3s-only rules
-> `/docs/standards/infra/k3s-specific.md`
- scripts structure and shell rules
-> `/docs/standards/infra/scripts.md`
- Kustomize structure and overlay rules
-> `/docs/standards/infra/kustomize.md`
Component routing:
- Keycloak
-> `/docs/standards/infra/keycloak.md`
- Vault
-> `/docs/standards/infra/vault.md`
- MinIO
-> `/docs/standards/infra/minio.md`
- Flyway
-> `/docs/standards/infra/flyway.md`
Before editing:
- identify target environment: dev / staging / prod
- identify target unit: app / managing / plugin / script / docs
- identify whether the change belongs in `k8s/base`, `k8s/overlays/<env>`, `k8s/scripts`, or docs
- identify workload type: Deployment / StatefulSet / Job / CronJob
- identify whether storage, secret delivery, ingress, migration, or rollback path changes
- identify whether the change touches a K3s-specific rule
Default execution flow:
- read the owning standard first
- prefer changing Kustomize source under `k8s/` over live cluster state
- prefer render -> validate -> diff -> apply thinking
- prefer explicit rollback/restore path before risky changes
If the request touches multiple areas, use this priority:
1. `k3s-specific.md`
2. `architecture-environments.md`
3. `db-and-migration.md`
4. the directly relevant component standard
5. supporting standards such as storage / security / operations / observability
+165 -1
View File
@@ -1,2 +1,166 @@
# project-infra
# Project-Infra
K3s 기반 백엔드 실행 환경을 Git으로 관리하는 개인 프로젝트입니다.
[Project-Auth-Server](https://github.com/donghyeon-ka/project-auth-server)가 실제로 실행될 때 필요한 인증 게이트, secret 전달, DB 마이그레이션, 네트워크 정책을 Kubernetes 리소스로 구성했습니다.
이 프로젝트에서 확인하고 싶었던 질문은 네 가지입니다.
- 로그인 / 세션 처리를 애플리케이션 밖으로 빼면 ingress와 backend의 책임은 어떻게 나뉘는가?
- secret을 Vault에 두면서도 Pod는 Kubernetes-native하게 실행할 수 있는가?
- default-deny NetworkPolicy 환경에서 백엔드 워크로드가 어떤 통신을 명시해야 하는가?
- dev에 집중하되, staging / prod 승격 시 달라질 정책 지점을 overlay 구조로 남길 수 있는가?
| | |
|---|---|
| **Runtime** | K3s 1.30, Kustomize, Helm, Bash |
| **Ingress / Auth** | Traefik, oauth2-proxy, Keycloak Operator |
| **Secrets** | HashiCorp Vault, Vault Secrets Operator |
| **Data** | PostgreSQL, Flyway, MinIO, Docker Registry |
| **Policy** | NetworkPolicy default-deny, PSS Restricted |
| **Validation** | `kustomize build`, `kubeconform -strict`, `kube-linter` |
| **Scope** | dev overlay 중심. staging / prod는 의도적으로 미구현 |
---
## Architecture
![System Overview](docs/diagrams/architecture/01-overall.png)
큰 흐름은 두 가지입니다.
- 요청 흐름: `Traefik → oauth2-proxy → auth-server → PostgreSQL / MinIO`
- secret 흐름: `Vault → VSO → Kubernetes Secret → Pod envFrom`
상세 다이어그램:
- [ForwardAuth cold path](docs/diagrams/sequence/forward-auth-cold.md) / [warm path](docs/diagrams/sequence/forward-auth-warm.md)
- [Secret pipeline bootstrap](docs/diagrams/sequence/secret-pipeline-bootstrap.md) / [runtime reconcile](docs/diagrams/sequence/secret-pipeline-runtime.md)
- [전체 아키텍처 설명](docs/architecture.md)
---
## Engineering Decisions
### 1. 인증은 Ingress에서 먼저 막고, 백엔드는 JWT를 다시 검증
Traefik `ForwardAuth → oauth2-proxy → Keycloak` 조합으로 미인증 요청을 ingress 계층에서 먼저 차단합니다.
그 뒤 auth-server는 Keycloak JWT를 Spring Security Resource Server로 다시 검증합니다.
이 구조는 로그인 / 세션 처리와 API 권한 검증을 분리하기 위한 선택입니다. 인증 정책 변경은 애플리케이션 코드보다 Kubernetes manifest 변경으로 다룰 수 있습니다.
![인증·인가 흐름](docs/diagrams/architecture/02-auth-flow.png)
### 2. Vault는 source of truth, Pod는 Kubernetes Secret만 소비
Pod마다 Vault Agent sidecar를 붙이지 않고, Vault Secrets Operator가 Vault KV 값을 Kubernetes Secret으로 동기화합니다.
```text
Vault KV-v2 → VSO reconcile → Kubernetes Secret → Pod envFrom
```
Pod는 Vault endpoint, token, template rendering을 직접 알지 않습니다. 대신 secret이 Kubernetes Secret으로 존재하므로, etcd encryption-at-rest가 다음 검증 항목으로 남습니다.
### 3. Vault role / policy는 도메인별로 분리
VSO Operator의 ServiceAccount는 하나지만, Vault 쪽 role과 policy는 `vso-auth-platform` / `vso-storage`로 나눴습니다.
`VaultStaticSecret`은 자기 도메인의 `VaultAuth`만 참조합니다.
auth-platform 권한으로 storage secret 경로에 접근하지 못하게 하려는 결정입니다.
상세 매핑은 [docs/vault-vso.md](docs/vault-vso.md#vaultauth--vaultstaticsecret-매핑)를 참고합니다.
### 4. NetworkPolicy는 default-deny에서 시작
`mnt` namespace 안에서도 모든 Pod 간 ingress / egress를 기본 차단합니다.
새 워크로드를 추가하려면 필요한 통신을 NetworkPolicy로 명시해야 합니다.
이 마찰은 의도한 것입니다. wide-open으로 시작하는 실수를 줄이고, 워크로드 간 통신 관계를 코드로 남기기 위함입니다.
상세 매트릭스는 [docs/networking.md](docs/networking.md)를 참고합니다.
### 5. K3s packaged Traefik manifest는 직접 수정하지 않음
K3s가 관리하는 Traefik manifest를 직접 수정하면 재부팅 / 재적용 시 덮어써질 수 있습니다.
그래서 `HelmChartConfig` overlay와 Middleware / TLSOption 리소스로 운영 정책을 관리합니다.
![Traefik 파이프라인](docs/diagrams/architecture/07-traefik-pipeline.png)
---
## Repository Layout
Kustomize `base / components / overlays` 구조입니다.
| Path | Role |
|---|---|
| `k8s/base/` | 환경 중립 매니페스트 |
| `k8s/components/` | 재사용 component. 현재 ForwardAuth component |
| `k8s/overlays/dev/` | 기본 dev 환경. 현재 ForwardAuth component 포함 |
| `k8s/overlays/{staging,prod}/` | 의도적으로 비워둔 승격 지점 |
| `k8s/scripts/` | bootstrap / validation / reusable tasks |
| `terraform/` | contracts만 존재. 추후 구현 |
| `docs/` | 상세 설계와 운영 문서 |
---
## Validation
```bash
bash k8s/scripts/ci/validate.sh
```
`validate.sh`는 주요 overlay에 대해 세 단계를 수행합니다.
1. `kustomize build` — overlay 조립과 patch 유효성 확인
2. `kubeconform -strict` — Kubernetes / CRD schema 확인
3. `kube-linter` — securityContext, resources, image tag 등 정적 점검
목표 상태:
```text
k8s/overlays/dev build=ok schema=ok lint=ok
k8s/overlays/dev/vso build=ok schema=ok lint=ok
```
운영 절차는 [docs/operations.md](docs/operations.md), 실행 매뉴얼은 [guide.md](guide.md)를 참고합니다.
---
## Current Status
| Area | Status |
|---|---|
| dev overlay 매니페스트 | 구성됨 (kustomize / kubeconform / kube-linter 검증 가능) |
| Vault + VSO 부트스트랩 | idempotent script 로 구성 |
| Traefik HelmChartConfig + Middleware | 구성됨 |
| NetworkPolicy default-deny | 구성됨 |
| ForwardAuth | dev overlay에 포함됨 |
| KeycloakRealmImport | overlay 준비됨. 실제 적용 결과 확인 필요 |
| cert-manager + ClusterIssuer | manifest 준비됨. 외부 DNS 필요 |
| staging / prod overlay | 의도적으로 비워둠 |
| Terraform | 디렉토리 contracts만 존재 |
---
## Limitations
운영 완료 상태가 아니라 dev 환경에서 실행 구조를 검증한 프로젝트입니다.
- Vault는 file backend 단일 노드입니다. dev에서는 secret 전달 경로와 VSO reconcile을 검증하는 데 충분하다고 보고 선택했습니다. prod에서는 `raft` storage와 KMS auto-unseal로 전환해야 합니다.
- cert-manager / ClusterIssuer manifest는 있지만, 실제 ACME 인증서 발급은 외부 DNS가 Traefik 진입점을 가리켜야 완료됩니다.
- ForwardAuth 로그인 / 로그아웃 / deny-allow E2E 검증은 인증서와 DNS 정리 후 진행할 항목입니다.
- Postgres 백업, Terraform 실제 모듈, staging/prod overlay는 아직 구현하지 않았습니다.
---
## Documentation
- [docs/architecture.md](docs/architecture.md) — 전체 구조, 워크로드 표, 이미지 정책
- [docs/networking.md](docs/networking.md) — NetworkPolicy 매트릭스 / 작성 규칙
- [docs/ingress-traefik.md](docs/ingress-traefik.md) — Traefik, ForwardAuth, cert-manager, KeycloakRealmImport
- [docs/vault-vso.md](docs/vault-vso.md) — Vault auth, policy/role, VSO Secret catalog
- [docs/operations.md](docs/operations.md) — bootstrap 단계, validate.sh, 환경별 차등
- [docs/troubleshooting.md](docs/troubleshooting.md) — 운영 중 만난 함정 7건 (사건 카탈로그)
- [docs/diagrams/architecture/](docs/diagrams/architecture/) — draw.io 아키텍처 그림
- [docs/diagrams/sequence/](docs/diagrams/sequence/) — Mermaid sequence diagrams
- [guide.md](guide.md) — 실제 실행 매뉴얼
@@ -0,0 +1,41 @@
# ADR-0001: Keycloak admin host 는 공개 Ingress 로 노출하지 않는다
- Status: Accepted
- Date: 2026-04-24
- Scope: dev / staging / prod 전 환경
## 컨텍스트
Keycloak CR 의 `spec.hostname.admin``https://keycloak-admin.dev.example.com` 로 선언되어 있다. Keycloak 26 Hostname v2 는 이 값을 admin console 의 베이스 URL 및 redirect 기준으로 사용한다 (공식 문서: https://www.keycloak.org/server/hostname). 그러나 해당 FQDN 에 대응하는 Kubernetes `Ingress` 리소스는 **의도적으로 생성하지 않는다**.
## 결정
1. `keycloak-admin.<env>.example.com` 에 대한 공개 Ingress 는 repo 내에 두지 않는다.
2. admin console 접근은 다음 중 하나를 요구한다:
- 사내 VPN + `kubectl port-forward -n mnt svc/keycloak 9000:9000` (관리 포트 직접 접근)
- bastion 에서 `kubectl exec``kcadm.sh` 호출
- 향후 별도로 도입할 zero-trust proxy (Pomerium / cloudflared tunnel / Teleport) 경로
3. `spec.hostname.admin` 선언 자체는 유지한다 — Keycloak 이 admin UI 링크를 올바른 FQDN 으로 발행해야 외부 OIDC/SAML 메타데이터와 충돌이 없기 때문이다. "FQDN 은 있으나 외부 공개 라우터는 없다" 가 정식 상태다.
## 근거
- `docs/standards/infra/keycloak.md` §11 "Admin Console 은 별도 host 로 분리" + "사내 IP 화이트리스트 / VPN / OIDC forward-auth" 요구와 정합.
- `docs/standards/infra/network-ingress-tls.md` §7 "내부 도구(Argo CD, Grafana, Kibana)는 VPN/zero-trust proxy 로만 노출" 및 §16 "`/admin/*` 는 Ingress path 에 포함하지 않는다" 기준.
- Keycloak 공식 권장: admin console 의 인터넷 공개는 공격면 확대. `sslRequired=external` 만으로는 brute-force / credential stuffing / SSRF 경로를 차단하지 못한다.
- CWE-284 (Improper Access Control) 예방을 위해 관리 평면을 데이터 평면과 물리적으로 분리한다.
## 결과
- admin console 접근은 SRE / platform 팀에만 부여되며, 접근 경로는 Runbook (`docs/standards/infra/operations-runbook-upgrade-rollback.md`) 의 "Keycloak admin 접근" 섹션(TODO)을 따른다.
- 인증 실패에 대한 alerting 은 `/admin/*` 공개 환경보다 훨씬 낮은 임계치로 설정 가능 (외부 스캐너 노이즈가 없기 때문).
- 향후 공개가 필요해지면 이 ADR 의 status 를 `Superseded by ADR-XXXX` 로 바꾸고 신규 ADR 에서:
1. `keycloak-admin.<env>.example.com` 용 Certificate (ECDSA, dev 는 letsencrypt-staging)
2. 전용 Ingress + Traefik `CIDRAllowList` middleware (사내 CIDR 만 허용)
3. oauth2-proxy forward-auth 또는 상호 TLS 인증 중 하나
4. admin-specific NetworkPolicy (egress-from-keycloak 에 대한 회귀 방지)
를 함께 도입한다.
## 대안과 기각 사유
- **대안 A — public Ingress + IP allowlist**: Traefik `CIDRAllowList` middleware 로 사내 CIDR 만 허용. 기각 사유: 사내 CIDR 이 변하거나 원격 근무자 VPN 미사용 시 실수로 통과시키는 위험. 관리 평면은 데이터 평면과 동일 ingress controller 를 공유하지 않는 것이 수비적으로 낫다.
- **대안 B — Keycloak 내장 IP allowlist**: Keycloak 자체 인증 흐름에는 path-level IP 필터 기능이 없다. 기각.
+147
View File
@@ -0,0 +1,147 @@
# Architecture 상세
README 의 Architecture / Key Components 를 보충하는 문서. 폴더 구조 전체와 워크로드, 이미지 정책을 모은다.
## 폴더 구조
```
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 만 존재, 추후 구현
```
## 네임스페이스 전략
`mnt` 단일 namespace. 학습 단계의 단순성 우선. 실무에서는 역할별 namespace(`auth`, `storage`, `security`, `registry`) 분리가 원칙이며, base 는 환경 중립이라 overlay 재구성으로 분리 가능하다.
- Pod Security Standards: `pod-security.kubernetes.io/enforce=restricted` (audit + warn 동시).
- 모든 리소스는 overlay 의 `namespace: mnt` 로 일괄 주입.
## 워크로드 목록
| 워크로드 | 종류 | 위치 | 참조 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` |
auth-server / keycloak 모두 `jdbc:postgresql://identity-postgres:5432/<db>` 로 short name 접속 (같은 namespace).
### Flyway 실행 순서
dev overlay 가 migration-flyway Job 에 ArgoCD annotation 을 patch:
```
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: PreSync
```
ArgoCD 배포 시 Job 이 앱보다 먼저 돌고 스키마 마이그레이션을 마친 뒤 `auth-server` 가 뜬다.
### Keycloak hostname patch
dev overlay JSON patch 가 Keycloak ConfigMap 에 다음을 주입:
- `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 양쪽이 안전하다.
Binary file not shown.

After

Width:  |  Height:  |  Size: 715 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 643 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 632 KiB

@@ -0,0 +1,53 @@
# ForwardAuth · Cold Path (최초 로그인)
세션 쿠키가 없는 첫 요청. OIDC Authorization Code Flow + PKCE 전 구간이 1 회 일어난다. **사용자당 세션 만료 주기마다 한 번** 만 발생 — 일상 운영 트래픽의 99% 는 [warm path](forward-auth-warm.md) 다.
> Traefik 의 path-based Ingress 라우팅이 전제: `project.com/oauth2/*` 는 oauth2-proxy 로, 그 외 path 는 ForwardAuth Middleware 를 거쳐 백엔드로 간다. 이 라우팅 결정이 그림의 모든 분기의 기반이다.
```mermaid
sequenceDiagram
autonumber
participant User
participant Traefik
participant OAuth as oauth2-proxy
participant KC as Keycloak
User->>Traefik: GET project.com/api/me
Traefik->>OAuth: ForwardAuth GET /oauth2/auth (no cookie)
OAuth-->>Traefik: 401 Unauthorized
Traefik-->>User: 302 to /oauth2/start
User->>Traefik: GET /oauth2/start
Note over Traefik: Ingress 가 /oauth2/* 를 oauth2-proxy 로 라우팅
Traefik->>OAuth: forward
Note over OAuth: PKCE code_verifier 생성, code_challenge 산출
OAuth-->>User: 302 to Keycloak authorize with code_challenge
User->>KC: GET /realms/platform/protocol/openid-connect/auth
KC-->>User: 로그인 폼
User->>KC: POST 자격증명
KC-->>User: 302 to /oauth2/callback with auth code
User->>Traefik: GET /oauth2/callback with code
Traefik->>OAuth: forward
OAuth->>KC: POST /token (code, code_verifier)
KC-->>OAuth: id_token, access_token, refresh_token
OAuth->>KC: GET /realms/platform/protocol/openid-connect/certs
KC-->>OAuth: JWKS 공개키
Note over OAuth: id_token 서명 검증 with JWKS, nonce 일치 확인
OAuth-->>User: Set-Cookie _oauth2_proxy + 302 to /api/me
Note over User: 이후 요청은 warm path
```
## 핵심 인사이트
- **Ingress 라우팅이 분기의 뿌리**: 그림의 Note 가 가리키듯 `/oauth2/*` 와 그 외 path 가 *Ingress 단에서* 갈린다. 이 라우팅이 없으면 cold path 가 시작 자체를 못 한다.
- **PKCE 가 핵심 보안 장치**: `code_verifier` 는 메시지 7~8 에서 oauth2-proxy 가 생성해 자기 세션에 저장하고, 메시지 16 에서 token exchange 시 함께 보낸다. Keycloak 은 `code_challenge` 와 매칭 검증. **authorization code 가 중간에 가로채지더라도 verifier 없이는 token 으로 교환 불가**. oauth2-proxy v7.5+ 는 PKCE 가 기본 활성.
- **JWKS 검증의 위치**: 메시지 18~19 (`GET .../certs`) 가 별개의 호출이다. oauth2-proxy 는 JWKS 를 *처음 1 회 fetch 후 캐시* 하고, Keycloak 의 JWKS endpoint 가 회전 가능 (`kid` 헤더로 식별). **id_token 서명 검증 (메시지 20 의 Note) 이 끝나야 쿠키가 발급되므로**, 이후 warm path 에서 백엔드가 받는 `X-Forwarded-User`*이미 검증된 사용자* 다.
- **TLS 검증 전제**: 현재 oauth2-proxy 설정은 `ssl_insecure_skip_verify=false` 이다. 따라서 Keycloak 공개 호스트(`keycloak.dev.example.com`) 인증서 체인이 정상이어야 token 교환과 JWKS 검증 흐름이 끝까지 진행된다.
## 평소 요청 흐름은?
→ [forward-auth-warm.md](forward-auth-warm.md)
@@ -0,0 +1,47 @@
# ForwardAuth · Warm Path (세션 쿠키 보유)
쿠키 검증으로 끝나는 평소 요청 경로. **운영 트래픽의 99% 가 이 흐름** 이다. cold path 의 OIDC handshake 는 세션 만료 시에만 다시 일어난다.
3 가지 결과가 있다: (1) 쿠키 정상 → 즉시 통과, (2) access_token 만료 → silent refresh 후 통과, (3) 쿠키 위조 또는 refresh 실패 → cold path 재진입.
```mermaid
sequenceDiagram
autonumber
participant User
participant Traefik
participant OAuth as oauth2-proxy
participant KC as Keycloak
participant App as auth-server
User->>Traefik: GET project.com/api/me with cookie
Traefik->>OAuth: ForwardAuth GET /oauth2/auth
alt 쿠키 HMAC 유효 + access_token 미만료
OAuth-->>Traefik: 202 Accepted with X-Auth-Request-User
else access_token 만료, refresh_token 유효
Note over OAuth: silent refresh
OAuth->>KC: POST /token with refresh_token
KC-->>OAuth: 새 access_token
OAuth-->>Traefik: 202 Accepted with X-Auth-Request-User
else 쿠키 위조 또는 refresh 실패
OAuth-->>Traefik: 401 Unauthorized
Traefik-->>User: 302 to /oauth2/start
Note over User: cold path 재진입
end
Note over Traefik: 클라이언트 X-Forwarded 헤더 strip 후 oauth2-proxy 응답 헤더만 주입
Traefik->>App: GET /api/me with X-Forwarded-User alice
App-->>User: 200 OK
```
## 핵심 인사이트
- **백엔드가 헤더만 신뢰해도 안전한 이유**: Traefik 의 ForwardAuth Middleware 가 *클라이언트로부터 들어온* `X-Forwarded-*` 헤더를 strip 하고, *oauth2-proxy 응답에 담긴* 헤더만 백엔드로 전달한다. 클라이언트가 위조한 `X-Forwarded-User: admin` 은 도달하지 못한다. **이 strip 동작이 무너지면 권한 우회 취약점**이 되므로 Traefik Middleware 의 `authResponseHeaders` 와 (Traefik global) `forwardedHeaders` 설정이 핵심.
- **백엔드 코드 단순화의 실체**: `auth-server` 의 컨트롤러는 `request.getHeader("X-Forwarded-User")` 한 줄만 본다. JWT 라이브러리, JWKS 캐시, 쿠키 파서, 세션 스토어가 모두 사라진다. 단위 테스트도 헤더 1 개 주입으로 인증된 사용자 시나리오가 만들어진다.
- **silent refresh 는 사용자에게 보이지 않음**: alt 의 두 번째 분기가 그 경우. 사용자 브라우저는 redirect 를 안 본다 — Traefik ForwardAuth 호출 안에서 refresh 가 끝나고 같은 응답이 202 로 돌아온다.
- **위조 시 회귀 경로**: 세 번째 분기. 쿠키 HMAC 가 안 맞거나 refresh 가 실패하면 oauth2-proxy 가 401 을 반환하고, Traefik 이 cold path 의 시작점인 `/oauth2/start` 로 돌려보낸다. 즉 **공격자가 쿠키를 위조해 봤자 결과는 로그인 페이지로의 redirect 일 뿐**이다.
## 처음 로그인 시 흐름은?
→ [forward-auth-cold.md](forward-auth-cold.md)
@@ -0,0 +1,44 @@
# Secret Pipeline · Bootstrap (1 회)
`tasks/vault-init.sh` 가 클러스터 최초 셋업 시 한 번만 수행하는 흐름. Vault 의 Kubernetes auth method 와 두 개의 role/policy, 그리고 VSO 가 사용할 VaultAuth CR 까지 준비한다. 모든 단계는 멱등 체크 후 차이만 적용된다 — Note 에 명시된 read 호출이 그 체크 지점.
```mermaid
sequenceDiagram
autonumber
participant Op as Operator
participant Vault
participant K8s as K8s API
Op->>Vault: operator init (5 unseal keys)
Op->>Vault: operator unseal (3 keys)
Vault-->>Op: Unsealed
Op->>K8s: apply ClusterRoleBinding (system auth-delegator)
Note over K8s: Vault Pod SA 에 TokenReview 권한 위임
Op->>Vault: auth enable kubernetes + write config
Note over Vault: vault auth list 후 미존재 시에만 enable
Op->>Vault: secrets enable kv-v2 at path secret
Note over Vault: vault secrets list 후 미존재 시에만 enable
Op->>Vault: policy write x2 + role write x2 (auth-platform, storage)
Note over Vault: 각 policy/role read 후 차이만 적용
Op->>Vault: kv put auth-server-db, keycloak-db, minio-tenant-env
Op->>K8s: apply VaultAuth x2
Op->>K8s: apply VaultStaticSecret x7
Note over K8s: VaultAuth 가 먼저, VaultStaticSecret 나중 - 그래야 reconcile 성공
```
## 핵심 인사이트
- **두 role 의 의도**: VSO 의 ServiceAccount 는 `vault-secrets-operator/mnt` 한 개뿐이다. 그러나 Vault 에 role 두 개를 두고 각각 다른 policy 를 묶었다. **VaultStaticSecret 마다 자기 도메인의 VaultAuth CR 을 참조**하므로, auth-platform role 의 토큰이 유출돼도 minio secret 은 못 읽는다.
- **`system:auth-delegator` 의 위치**: 이 ClusterRoleBinding 은 *Vault Pod 의 SA* 에 부여된다. Vault 가 VSO 의 SA JWT 를 검증하기 위해 K8s 의 `TokenReview` API 를 호출할 권한이 필요하기 때문. VSO 측이 아니라 Vault 측에 붙는다는 점이 자주 헷갈리는 지점.
- **멱등성의 위치**: 각 enable / write 호출 직전에 `vault auth list`, `vault secrets list`, `vault policy read`, `vault read auth/kubernetes/role/<name>` 으로 현재 상태를 체크하고 차이만 적용한다. 따라서 이 다이어그램의 모든 단계는 *재실행 안전*.
- **마지막 두 단계의 순서**: VaultAuth 가 먼저, VaultStaticSecret 이 나중. 그래야 VSO 가 첫 reconcile 에서 `vaultAuthRef` 를 정상 해석한다.
## 정상 운영 시 reconcile 흐름은?
→ [secret-pipeline-runtime.md](secret-pipeline-runtime.md)
@@ -0,0 +1,54 @@
# Secret Pipeline · Steady-State Reconcile
VSO 가 VaultStaticSecret CR 을 reconcile 할 때마다 일어나는 흐름. **이 다이어그램은 한 reconcile 사이클** 만 다룬다 — 부트스트랩(정책/role/CR 적용) 은 [secret-pipeline-bootstrap.md](secret-pipeline-bootstrap.md) 에서 이미 끝난 상태를 전제한다.
VSO 는 controller-runtime 기반이라 informer 가 *시작 시 1 회 watch 등록* 하고, 이후 K8s API 가 push 하는 이벤트로 reconcile 이 트리거된다. 즉 매 cycle 마다 watch 호출이 새로 일어나는 게 아니다.
```mermaid
sequenceDiagram
autonumber
participant VSO as VSO Operator
participant K8s as K8s API
participant Vault
participant Sec as K8s Secret
Note over VSO,K8s: informer 가 시작 시 1 회 watch 등록 후 이벤트 수신 대기
K8s-->>VSO: event for VaultStaticSecret auth-server-db-creds
VSO->>K8s: read VaultStaticSecret spec
K8s-->>VSO: vaultAuthRef, path
VSO->>K8s: read VaultAuth vault-auth-auth-platform
K8s-->>VSO: role vso-auth-platform, mount kubernetes
VSO->>Vault: POST auth/kubernetes/login (role, jwt)
Vault->>K8s: TokenReview (VSO SA JWT)
Note over Vault,K8s: system auth-delegator 권한 사용
K8s-->>Vault: ok, sa vault-secrets-operator
Vault-->>VSO: Vault token (policy vso-auth-platform, ttl 1h)
VSO->>Vault: GET secret/data/auth-server/db
Vault-->>VSO: username, password, jdbc-url
Note over VSO: destination overwrite false 면 기존 Secret 유지
VSO->>K8s: create or update Secret auth-server-db
K8s-->>Sec: stored
Note over Sec: kubelet 이 Pod 시작 시 envFrom 으로 마운트 (시퀀스 외)
loop every refreshAfter (1h)
VSO->>Vault: GET secret/data/auth-server/db
Vault-->>VSO: 최신 값
opt 값이 변경된 경우
VSO->>K8s: update Secret auth-server-db
end
end
```
## 핵심 인사이트
- **이 다이어그램의 시작점은 K8s 가 던지는 event**: VSO 가 매 cycle 마다 watch API 를 새로 호출하는 게 아니다. controller-runtime 의 informer 가 startup 에 watch 를 establish 하고, K8s API 가 변경 사항을 push 하면 reconcile loop 이 깨어난다. 그래서 메시지 1 의 화살표 방향이 K8s → VSO.
- **TokenReview 는 VSO 가 부르는 게 아니라 Vault 가 부른다**: 메시지 7 (`Vault to K8s API: TokenReview`) 가 그 호출. Vault 가 *받은* SA JWT 가 진짜 VSO 의 것인지 확인하기 위해 K8s 에 위임 검증한다.
- **role 결정은 VaultAuth CR 이 한다**: 메시지 4~5 에서 VSO 는 *VaultStaticSecret 이 가리키는 VaultAuth* 를 읽고, 거기에 박힌 `role: vso-auth-platform` 으로 Vault login 한다. 같은 SA 라도 어느 VaultAuth 를 거쳤느냐에 따라 받는 policy 가 달라진다.
- **`overwrite=false` 의 책임 위치**: 이건 K8s API 의 동작이 아니라 *VSO reconciler 가 update 호출 전에 자기 로직으로 결정* 한다. 그래서 Note 가 VSO 위에 붙는다.
- **즉시 반영**: Vault 값 변경 직후 반영하려면 `kubectl -n mnt delete secret auth-server-db`. 다음 reconcile 에서 VSO 가 위 흐름을 다시 돌아 새 값으로 재생성한다 — Pod 는 envFrom 으로 받은 값이 바뀌었음을 자동으로 알 수 없으므로 rollout 도 함께.
@@ -0,0 +1,417 @@
# architecture / environments 예시
이 파일의 모든 YAML은 `kubectl apply --server-side --dry-run=server` 에 통과해야 한다.
모든 예시는 1000+ 서비스 운영 기준으로 작성되었고, 단독으로 복붙해서 바로 apply 할 수 있도록 self-contained 하다.
---
## 좋은 예시 1: namespace에 환경 · 도메인 · PodSecurity · 운영 label 전부 박기
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/version: "1.24.3"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/team: identity-sre
example.com/tier: backend
example.com/slo-tier: tier-1
example.com/data-classification: confidential
example.com/cost-center: cc-1042
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/warn: restricted
annotations:
example.com/owner-email: identity-sre@example.com
example.com/runbook: https://runbooks.example.com/identity/auth
example.com/slo-doc: https://slo.example.com/identity/auth
---
apiVersion: v1
kind: ResourceQuota
metadata:
name: default-quota
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: quota
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
spec:
hard:
requests.cpu: "20"
requests.memory: 40Gi
limits.cpu: "40"
limits.memory: 80Gi
pods: "200"
persistentvolumeclaims: "20"
---
apiVersion: v1
kind: LimitRange
metadata:
name: default-limits
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: limits
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
spec:
limits:
- type: Container
default:
cpu: "500m"
memory: 512Mi
defaultRequest:
cpu: "100m"
memory: 128Mi
max:
cpu: "4"
memory: 8Gi
min:
cpu: "10m"
memory: 32Mi
```
**왜 좋은가:**
- `app.kubernetes.io/*` well-known 6종이 모두 있고, 운영 축은 `example.com/*`로 분리되어 selector immutability를 깨지 않는다
- PodSecurity admission이 namespace 레벨에서 `restricted`로 강제 → 이후 Pod spec이 noncompliant면 창조 시점에 거부
- ResourceQuota + LimitRange가 namespace 단위로 고정되어 하나의 서비스가 클러스터를 삼킬 수 없다
- 환경(prod)·도메인(identity)·서비스(auth)가 namespace 이름과 label 양쪽에 드러남
---
## 좋은 예시 2: multi-region prod overlay 디렉터리 (kr-main + kr-dr)
```text
k8s/
base/
app/
units/
identity/
auth/
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
```
**왜 좋은가:**
- 1000+ 서비스 스케일에서 단일 overlay/prod로는 region 차이를 표현할 수 없다. region이 overlay 하위 계층이 되어야 한다
- base는 region·환경을 모른다 (원칙 충족)
- DR region은 base의 image pull spec만 mirror로 패치하고 나머지는 공유
---
## 좋은 예시 3: SLO tier 별 기본 default per namespace
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: slo-defaults
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: slo-config
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/slo-tier: tier-1
data:
availability-slo: "99.95"
rpo-minutes: "5"
rto-minutes: "15"
backup-interval-minutes: "15"
multi-az-required: "true"
pdb-min-available-percent: "50"
```
**왜 좋은가:**
- SLO/RPO/RTO 숫자가 YAML로 문서화되어 audit 가능
- 같은 tier 정의가 팀마다 제각각 drift 되는 일을 막는다
- `example.com/slo-tier` label이 cluster-wide 쿼리 축 제공 (`kubectl get ns -l example.com/slo-tier=tier-1`)
---
## 좋은 예시 4: K3s packaged component disable을 bootstrap 레벨에서 선언
```yaml
# /etc/rancher/k3s/config.yaml (Git-managed, applied identically to every server node)
write-kubeconfig-mode: "0640"
cluster-cidr: "10.42.0.0/16"
service-cidr: "10.43.0.0/16"
cluster-dns: "10.43.0.10"
cluster-domain: "cluster.local"
disable:
- traefik
- servicelb
- local-storage
disable-network-policy: false
tls-san:
- "k3s.prod.example.internal"
- "10.0.0.10"
kube-apiserver-arg:
- "audit-log-path=/var/log/k3s/audit.log"
- "audit-log-maxage=30"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=100"
- "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml"
kubelet-arg:
- "config=/etc/rancher/k3s/kubelet.yaml"
```
**왜 좋은가:**
- prod 스케일에서 traefik / servicelb / local-storage는 전부 외부 컴포넌트로 대체되므로 disable이 기본
- critical config (`cluster-cidr`, `service-cidr`, `cluster-dns`, `cluster-domain`)가 Git 하나의 파일에 고정 → 서버 간 mismatch 불가능
- audit log와 kubelet config가 선언형으로 박힘 → 신규 서버 조인 시 drift 없음
---
## 좋은 예시 5: 도메인 분리 + public/internal/operator ingress host 패턴
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-public
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/version: "1.24.3"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/exposure: public
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/proxy-body-size: "8m"
spec:
ingressClassName: nginx-public
tls:
- hosts:
- auth.example.com
secretName: auth-public-tls
rules:
- host: auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: auth
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-admin
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: admin
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/exposure: operator-only
annotations:
cert-manager.io/cluster-issuer: internal-ca
nginx.ingress.kubernetes.io/auth-url: "https://sso.ops.example.com/oauth2/auth"
nginx.ingress.kubernetes.io/auth-signin: "https://sso.ops.example.com/oauth2/sign_in?rd=$escaped_request_uri"
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8"
spec:
ingressClassName: nginx-internal
tls:
- hosts:
- auth.ops.example.com
secretName: auth-admin-tls
rules:
- host: auth.ops.example.com
http:
paths:
- path: /actuator
pathType: Prefix
backend:
service:
name: auth
port:
number: 8081
```
**왜 좋은가:**
- 한 서비스(auth)가 public API와 operator-only admin 포트를 별도 ingress + 별도 ingressClass + 별도 TLS issuer로 분리
- CIDR whitelist + OAuth2 sso forward-auth가 admin endpoint에 강제
- `example.com/exposure` label로 cluster-wide audit 쿼리 가능
---
## 나쁜 예시 1: `default` namespace에 prod workload
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: default
spec:
replicas: 3
selector:
matchLabels:
app: auth-server
template:
metadata:
labels:
app: auth-server
spec:
containers:
- name: auth
image: registry.example.com/auth:1.24.3
```
**문제:** `default` namespace는 PodSecurity / Quota / NetworkPolicy를 걸기 위한 격리 단위가 될 수 없고, 다른 팀 리소스와 섞인다. 1000-서비스 환경에서 `default`는 영구적으로 비워두는 것이 운영 원칙.
---
## 나쁜 예시 2: `app.kubernetes.io/environment` 사용 (well-known label에 없음)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/environment: prod # invalid well-known key
```
**문제:** Kubernetes 공식 well-known label set은 `{name,instance,version,component,part-of,managed-by}` 6종뿐. `environment`는 여기 없으므로 **자체 도메인**(`example.com/environment`)을 써야 한다. 다른 팀이 `app.kubernetes.io/env` 같은 변종을 만들어 drift가 퍼진다.
---
## 나쁜 예시 3: `manifests/` 디렉터리에 운영 리소스 직접 배치
```text
/var/lib/rancher/k3s/server/manifests/auth-prod.yaml
/var/lib/rancher/k3s/server/manifests/keycloak-prod.yaml
/var/lib/rancher/k3s/server/manifests/ingress-nginx.yaml
```
**문제:** 멀티 서버 K3s는 이 디렉터리를 서버 간 동기화하지 **않는다**. 서버 A에만 있는 파일은 서버 B 리더가 되면 사라진 것처럼 보인다. source of truth는 Git + Kustomize여야 한다.
---
## 나쁜 예시 4: selector에 버전 / 환경 label 포함
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
namespace: prod-identity-auth
spec:
selector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/version: "1.24.3" # changes on every release
example.com/environment: prod # injected by overlay
template:
metadata:
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/version: "1.24.3"
example.com/environment: prod
spec:
containers:
- name: auth
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`)만.
---
## 나쁜 예시 5: 같은 hostname을 dev와 prod가 공유
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth
namespace: dev-identity-auth
spec:
ingressClassName: nginx-public
rules:
- host: auth.example.com # same as prod
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: auth
port:
number: 8080
```
**문제:** 환경 간 host 공유는 TLS cert race, 동일 hostname의 두 ingress 간 routing 불확실성, 외부 모니터링이 어느 환경을 보는지 혼동을 유발한다. dev는 반드시 `auth.dev.example.com` 같이 별도 hostname을 쓴다.
---
## 나쁜 예시 6: K3s traefik manifest 직접 수정으로 prod ingress 커스터마이즈
```bash
vim /var/lib/rancher/k3s/server/manifests/traefik.yaml
# added custom middleware config inline
systemctl restart k3s
```
**문제:** K3s는 재시작 시 이 파일을 packaged 원본으로 overwrite한다. 운영 커스터마이징이 조용히 사라진다. prod 1000-서비스 스케일에서는 `--disable=traefik` 후 ingress-nginx를 별도 컴포넌트로 관리하는 것이 유일한 정답. 유지한다면 **반드시** `HelmChartConfig` 사용.
+564
View File
@@ -0,0 +1,564 @@
# backup / restore 예시
모든 예시는 실제 매니페스트로 `kubectl apply -f` 가능하다.
---
## 좋은 예시 1: Velero 설치 후 BackupStorageLocation / VolumeSnapshotLocation
```yaml
---
apiVersion: velero.io/v1
kind: BackupStorageLocation
metadata:
name: default
namespace: velero
labels:
app.kubernetes.io/part-of: platform-backup
spec:
provider: aws
objectStorage:
bucket: acme-prod-velero-backups
prefix: k3s-prod
config:
region: us-east-1
s3ForcePathStyle: "false"
s3Url: https://s3.us-east-1.amazonaws.com
default: true
accessMode: ReadWrite
credential:
name: velero-s3-credentials
key: cloud
---
apiVersion: velero.io/v1
kind: VolumeSnapshotLocation
metadata:
name: csi-default
namespace: velero
spec:
provider: csi
```
왜 좋은가:
- 백업 저장소가 **클러스터 외부** S3 (같은 cluster MinIO에 넣지 않음)
- credential은 별도 Secret
- CSI snapshot location이 명시됨
❌ 나쁜 예시 1: 같은 cluster 안 MinIO를 백업 저장소로 사용
```yaml
spec:
provider: aws
objectStorage:
bucket: backups
config:
s3Url: http://minio.object-prod.svc.cluster.local:9000 # 같은 cluster!
```
문제:
- cluster 장애 = 백업 동시 소실
- MinIO 자체를 복구하려면 외부 백업이 또 필요 — 순환 의존
---
## 좋은 예시 2: Velero Schedule (tier별 분리, 30일 retention)
```yaml
---
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: gold-daily
namespace: velero
labels:
backup.platform.io/tier: gold
spec:
schedule: "0 2 * * *" # 매일 02:00 UTC
useOwnerReferencesInBackup: true
template:
ttl: 720h0m0s # 30일 retention
includedNamespaces:
- auth-prod
- data-prod
- object-prod
includedResources:
- persistentvolumeclaims
- persistentvolumes
- secrets
- configmaps
- deployments
- statefulsets
- services
- ingresses
- networkpolicies
labelSelector:
matchLabels:
backup.platform.io/tier: gold
snapshotVolumes: true
defaultVolumesToFsBackup: false
csiSnapshotTimeout: 30m
storageLocation: default
volumeSnapshotLocations:
- csi-default
hooks:
resources:
- name: postgres-consistent
includedNamespaces: [data-prod]
labelSelector:
matchLabels:
app.kubernetes.io/name: postgres
pre:
- exec:
container: postgres
command: ["/bin/sh", "-c", "psql -U postgres -c CHECKPOINT"]
onError: Fail
timeout: 2m
---
apiVersion: velero.io/v1
kind: Schedule
metadata:
name: bronze-weekly-fsb
namespace: velero
labels:
backup.platform.io/tier: bronze
spec:
schedule: "0 3 * * 0" # 매주 일요일 03:00 UTC
template:
ttl: 2160h0m0s # 90일
includedNamespaces: ["archive-prod"]
labelSelector:
matchLabels:
backup.platform.io/tier: bronze
snapshotVolumes: false
defaultVolumesToFsBackup: true # kopia/restic FSB
storageLocation: default
```
왜 좋은가:
- Schedule이 tier별로 분리되어 RPO/retention/도구를 구분
- CSI snapshot(gold)과 FSB(bronze)를 목적에 맞게 선택
- Postgres는 pre-hook으로 `CHECKPOINT`를 수행해 crash-consistent에 가까운 스냅샷 확보
- `labelSelector`가 PVC의 `backup.platform.io/tier`와 매칭
---
## 좋은 예시 3: Velero Restore
```yaml
---
apiVersion: velero.io/v1
kind: Restore
metadata:
name: auth-prod-restore-2026-04-16
namespace: velero
spec:
backupName: gold-daily-20260415020000
includedNamespaces: ["auth-prod"]
restorePVs: true
existingResourcePolicy: none # 기존 리소스 보존, 누락된 것만 복원
namespaceMapping:
auth-prod: auth-prod-restore # 검증용 별도 네임스페이스로 복원
labelSelector:
matchLabels:
backup.platform.io/tier: gold
```
왜 좋은가:
- 복원 대상이 `auth-prod-restore`로 분리되어 운영 영향 없이 검증 가능
- `existingResourcePolicy: none`으로 실수 덮어쓰기 방지
- `restorePVs: true`로 PVC/PV까지 함께 복원
---
## 좋은 예시 4: CloudNativePG Cluster + ScheduledBackup + Backup
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: data-prod
labels:
pod-security.kubernetes.io/enforce: restricted
---
apiVersion: v1
kind: Secret
metadata:
name: cnpg-s3-credentials
namespace: data-prod
type: Opaque
stringData:
ACCESS_KEY_ID: REPLACE_VIA_VSO
ACCESS_SECRET_KEY: REPLACE_VIA_VSO
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: auth-pg
namespace: data-prod
labels:
app.kubernetes.io/name: auth-pg
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
backup.platform.io/tier: gold
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8
primaryUpdateStrategy: unsupervised
postgresql:
parameters:
shared_buffers: "512MB"
max_connections: "200"
wal_compression: "on"
archive_timeout: "60s"
bootstrap:
initdb:
database: auth
owner: auth_app
secret:
name: auth-pg-app
storage:
size: 50Gi
storageClass: fast-ssd-retain
walStorage:
size: 20Gi
storageClass: fast-ssd-retain
monitoring:
enablePodMonitor: true
resources:
requests: {cpu: "500m", memory: "2Gi"}
limits: {cpu: "2", memory: "4Gi"}
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: s3://acme-prod-pg-backups/auth-pg
endpointURL: https://s3.us-east-1.amazonaws.com
s3Credentials:
accessKeyId:
name: cnpg-s3-credentials
key: ACCESS_KEY_ID
secretAccessKey:
name: cnpg-s3-credentials
key: ACCESS_SECRET_KEY
wal:
compression: gzip
maxParallel: 8
data:
compression: gzip
immediateCheckpoint: true
jobs: 4
affinity:
podAntiAffinityType: required
topologyKey: kubernetes.io/hostname
---
apiVersion: postgresql.cnpg.io/v1
kind: ScheduledBackup
metadata:
name: auth-pg-daily
namespace: data-prod
spec:
schedule: "0 0 2 * * *" # 매일 02:00 (CNPG는 6-field cron)
backupOwnerReference: self
cluster:
name: auth-pg
method: barmanObjectStore
---
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: auth-pg-premigration-2026-04-16
namespace: data-prod
spec:
cluster:
name: auth-pg
method: barmanObjectStore
```
왜 좋은가:
- Postgres 16, 3 instances, 자동 failover
- WAL continuous archiving + daily base backup으로 RPO 5분 / PITR 가능
- `ScheduledBackup`이 cron 기반 정기 백업, `Backup`이 on-demand (마이그레이션 직전 등)
- `enablePodMonitor`로 Prometheus 연동
- `podAntiAffinity`로 노드 분산
- `backup.retentionPolicy: 30d`
❌ 나쁜 예시 2: StatefulSet + cron으로 `pg_dump` 하나만
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: pg-dump-nightly
spec:
schedule: "0 3 * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: dump
image: postgres:16
command: ["sh", "-c", "pg_dumpall -U postgres > /backup/dump.sql"]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: backup, mountPath: /backup }
volumes:
- name: tmp
emptyDir: {}
- name: backup
emptyDir: {}
```
문제:
- PITR 불가 (base backup + WAL 아님)
- single file → 대규모에서 restore 시간 폭증
- logical dump는 replication slot / extension / large object 처리에 구멍
- 같은 cluster의 PVC에 저장 시 장애 시 동시 소실
---
## 좋은 예시 5: CNPG PITR restore (bootstrap.recovery)
```yaml
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: auth-pg-restore
namespace: data-prod
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8
storage:
size: 50Gi
storageClass: fast-ssd-retain
walStorage:
size: 20Gi
storageClass: fast-ssd-retain
bootstrap:
recovery:
source: auth-pg-source
recoveryTarget:
targetTime: "2026-04-16 09:45:00.00+00"
externalClusters:
- name: auth-pg-source
barmanObjectStore:
destinationPath: s3://acme-prod-pg-backups/auth-pg
endpointURL: https://s3.us-east-1.amazonaws.com
s3Credentials:
accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID}
secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY}
wal:
maxParallel: 8
```
왜 좋은가:
- PITR을 declarative CRD로 표현
- 원본 cluster를 건드리지 않고 별도 `auth-pg-restore` 클러스터로 복원
- 특정 시점(`targetTime`)까지 WAL replay
---
## 좋은 예시 6: K3s etcd snapshot + S3 업로드
```ini
# /etc/rancher/k3s/config.yaml (control-plane nodes)
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-retention: 28
etcd-s3: true
etcd-s3-endpoint: "s3.us-east-1.amazonaws.com"
etcd-s3-bucket: "acme-prod-k3s-etcd"
etcd-s3-folder: "prod-cluster-1"
etcd-s3-region: "us-east-1"
etcd-s3-access-key-file: /var/lib/rancher/k3s/server/etcd-s3-access
etcd-s3-secret-key-file: /var/lib/rancher/k3s/server/etcd-s3-secret
secrets-encryption: true
```
token 별도 보관 (예: 운영자 금고 / 외부 Vault):
```
/var/lib/rancher/k3s/server/token → offline backup, 접근 로그 남김
```
왜 좋은가:
- 6시간마다 etcd snapshot + S3 자동 업로드 + 28개 보관
- secrets encryption 활성화로 snapshot 유출 시 노출 감소
- server token을 snapshot과 같은 위치에 두지 않음
**주의: 이 snapshot은 PVC 데이터를 포함하지 않는다. 반드시 Velero + CNPG backup과 병행.**
---
## 좋은 예시 7: Vault raft snapshot (CronJob)
```yaml
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-raft-snapshot
namespace: vault
spec:
schedule: "0 */6 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 0
template:
spec:
restartPolicy: Never
serviceAccountName: vault-snapshot
securityContext:
runAsNonRoot: true
runAsUser: 100
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: snapshot
image: hashicorp/vault@sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef
env:
- name: VAULT_ADDR
value: https://vault.vault.svc:8200
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: vault-snapshot-token
key: token
command:
- sh
- -c
- |
set -eu
TS=$(date -u +%Y%m%dT%H%M%SZ)
vault operator raft snapshot save /snap/vault-${TS}.snap
aws s3 cp /snap/vault-${TS}.snap s3://acme-prod-vault-snap/ --sse aws:kms
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: ["ALL"]}
volumeMounts:
- {name: snap, mountPath: /snap}
volumes:
- name: snap
emptyDir: {}
```
왜 좋은가:
- 6시간마다 raft snapshot + S3 (SSE-KMS) 업로드
- snapshot용 scoped token 사용 (최소권한)
- `concurrencyPolicy: Forbid`로 snapshot 중복 실행 방지
---
## 좋은 예시 8: MinIO bucket replication (DR)
```bash
# 소스 클러스터 MinIO에서
mc alias set src https://minio.prod-a.acme.io $SRC_KEY $SRC_SECRET
mc alias set dst https://minio.prod-b.acme.io $DST_KEY $DST_SECRET
mc admin replicate add src dst
mc version enable src/assets
mc version enable dst/assets
mc replicate add src/assets --remote-bucket dst/assets --replicate "delete,delete-marker,existing-objects,metadata-sync"
# DR 발생 시 (소스 완전 장애 후 복구)
mc replicate resync start src/assets --remote-bucket dst/assets
```
왜 좋은가:
- bucket versioning이 replication 전제
- `resync`로 DR 복구 경로 확보
- `mc mirror`를 단독 DR 수단으로 사용하지 않음
❌ 나쁜 예시 3: `mc mirror`만 단독 사용
```bash
mc mirror --overwrite src/assets dst/assets # 현재 객체만 동기화, 버전 이력 없음
```
문제:
- 버전 이력 / 삭제 marker / metadata 누락
- 랜섬웨어 / 실수 삭제 시 복구 불가
---
## 좋은 예시 9: Restore drill 기록 양식
```yaml
# /runbooks/restore-drills/2026-Q1-auth-pg.yaml
drill:
id: drill-2026-q1-auth-pg
component: cloudnativepg:auth-pg
tier: gold
target_rpo: 5m
target_rto: 30m
executed_at: 2026-03-18T14:00:00Z
executor: sre@acme.io
source_backup: barman:auth-pg/base/20260318T020000
restore_target_time: "2026-03-17 23:59:00+00"
restore_cluster: auth-pg-drill
result:
status: success
observed_rpo: 3m
observed_rto: 22m
verification_query: "select count(*) from users where created_at < '2026-03-17 23:59:00'"
verification_result: 1842317
issues:
- description: "WAL fetch parallelism bumped from 4 to 8 for better RTO"
action: "updated Cluster.spec.externalClusters[0].barmanObjectStore.wal.maxParallel to 8"
next_drill_due: 2026-06-18
```
왜 좋은가:
- RPO/RTO 목표 vs 실측을 같이 기록
- 검증 query 결과까지 남김
- 다음 drill 예정일이 명시 → 90일 초과 시 경보
---
## 나쁜 예시 4: "Git에 manifest 있으니 복구 완료"
```
✗ manifests are in Git
✗ so restore is solved
```
문제:
- DB state, Vault state, MinIO objects, K3s cluster state 모두 복구 안 됨
- Argo CD sync만으로는 runtime data가 돌아오지 않음
---
## 나쁜 예시 5: K3s etcd snapshot만 있으면 PVC도 복구된다고 오해
```
✗ k3s etcd-snapshot restore → all data back
```
문제:
- etcd snapshot은 API object 선언만 복구. PVC 안의 파일은 복구 안 됨
- 반드시 Velero + DB-level backup과 병행
+642
View File
@@ -0,0 +1,642 @@
# config / secrets 예시
모든 YAML은 `kubectl apply --dry-run=server -f -` 기준 clean. VSO는 Helm chart `hashicorp/vault-secrets-operator``vault-secrets-operator` namespace에 설치되어 있고, Vault는 `vault` namespace(`https://vault.vault.svc:8200`)에서 기동 중이며, Kubernetes auth method(`auth/kubernetes`)가 활성화되어 있다고 가정한다.
---
## 좋은 예시 1: 비기밀 ConfigMap (hash-suffixed by Kustomize)
```yaml
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-server-config
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/part-of: identity-platform
data:
application.yaml: |
server:
port: 8080
shutdown: graceful
management:
endpoints:
web:
base-path: /actuator
exposure:
include: health,info,prometheus
server:
port: 9090
spring:
main:
banner-mode: off
datasource:
hikari:
maximum-pool-size: 20
connection-timeout: 5000
logging:
level:
root: INFO
com.example.auth: INFO
```
**왜 좋은가:**
- 비밀값은 하나도 없다(username/password/url 제외). Hikari pool size, log level, actuator 경로 같은 operational config만.
- Kustomize `configMapGenerator`로 hash suffix를 붙이면 Deployment가 자동 rollout.
---
## 좋은 예시 2: VSO 전체 스택 (VaultConnection + VaultAuth + VaultStaticSecret + VaultDynamicSecret + VaultPKISecret)
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: auth-prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.29
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server
namespace: auth-prod
automountServiceAccountToken: true
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: vault
namespace: auth-prod
spec:
address: https://vault.vault.svc:8200
skipTLSVerify: false
caCertSecretRef: vault-ca-bundle
headers: {}
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: auth-server
namespace: auth-prod
spec:
vaultConnectionRef: vault
method: kubernetes
mount: kubernetes
kubernetes:
role: auth-server
serviceAccount: auth-server
audiences:
- vault
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: auth-server-oidc-client
namespace: auth-prod
spec:
vaultAuthRef: auth-server
mount: kv
type: kv-v2
path: identity/auth-server/prod/oidc
refreshAfter: 1h
destination:
name: auth-server-oidc-client
create: true
type: Opaque
rolloutRestartTargets:
- kind: Deployment
name: auth-server
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: auth-server-db
namespace: auth-prod
spec:
vaultAuthRef: auth-server
mount: database
path: creds/auth-server-role
destination:
name: auth-server-db
create: true
type: Opaque
transformation:
templates:
DB_URL:
text: 'jdbc:postgresql://identity-postgres.data-prod.svc:5432/auth?user={{ .Secrets.username }}&password={{ .Secrets.password }}&sslmode=require'
DB_USERNAME:
text: '{{ .Secrets.username }}'
DB_PASSWORD:
text: '{{ .Secrets.password }}'
rolloutRestartTargets:
- kind: Deployment
name: auth-server
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: auth-server-internal-tls
namespace: auth-prod
spec:
vaultAuthRef: auth-server
mount: pki_int
role: auth-server
commonName: auth-server.auth-prod.svc
altNames:
- auth-server.auth-prod.svc.cluster.local
- auth-server
ttl: 24h
destination:
name: auth-server-internal-tls
create: true
type: kubernetes.io/tls
rolloutRestartTargets:
- kind: Deployment
name: auth-server
```
**왜 좋은가:**
- Vault가 source of truth. 모든 비밀이 `kv/identity/auth-server/prod/*` 또는 database/PKI engine에서 발급.
- VSO가 결과물을 표준 Kubernetes Secret(`Opaque`, `kubernetes.io/tls`)으로 materialize.
- Dynamic DB credential은 Postgres role에서 TTL 기반 자동 발급/폐기. Rotation 시 `rolloutRestartTargets`로 Deployment rolling restart.
- PKI Secret은 `kubernetes.io/tls` 타입 → Traefik/앱 TLS에 그대로 소비 가능.
---
## 좋은 예시 3: VSO Secret을 소비하는 Deployment (envFrom + volume 혼합)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/version: 1.42.0
app.kubernetes.io/managed-by: argocd
spec:
replicas: 6
revisionHistoryLimit: 5
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/version: 1.42.0
spec:
serviceAccountName: auth-server
automountServiceAccountToken: true
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/identity/auth-server@sha256:8f3c0a8c6b3a2a7a0f1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
- name: metrics
containerPort: 9090
envFrom:
- configMapRef:
name: auth-server-config
- secretRef:
name: auth-server-db
- secretRef:
name: auth-server-oidc-client
volumeMounts:
- name: internal-tls
mountPath: /var/run/secrets/tls
readOnly: true
- name: appconfig
mountPath: /workspace/config
readOnly: true
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
- name: metrics-exporter
image: registry.example.com/platform/jmx-exporter@sha256:1111111111111111111111111111111111111111111111111111111111111111
ports:
- name: jmx-metrics
containerPort: 9091
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 200m
memory: 128Mi
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumes:
- name: internal-tls
secret:
secretName: auth-server-internal-tls
defaultMode: 0400
- name: appconfig
configMap:
name: auth-server-config
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 64Mi
imagePullSecrets:
- name: registry-example-com
```
**왜 좋은가:**
- VSO가 생성한 `auth-server-db`, `auth-server-oidc-client`를 envFrom으로 소비. 앱 코드는 `DB_USERNAME`, `DB_PASSWORD`, `OIDC_CLIENT_SECRET` 환경변수를 읽기만 함.
- TLS private key는 volume(`/var/run/secrets/tls`, mode 0400)으로만 마운트. env 노출 없음.
- `metrics-exporter` sidecar에는 **어떤 secret도 envFrom/volumeMount로 전달하지 않는다**. Scope 최소화.
- image는 digest pin, `imagePullPolicy: IfNotPresent`.
---
## 나쁜 예시 1: plain Secret manifest + ConfigMap에 비밀 혼재
```yaml
apiVersion: v1
kind: Secret
metadata:
name: auth-server-db
namespace: auth-prod
type: Opaque
stringData:
username: prod-admin
password: S3cur3P@ssw0rd!
---
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-server-config
namespace: auth-prod
data:
application.yaml: |
spring:
datasource:
url: jdbc:postgresql://prod-db:5432/auth
username: prod-admin
password: S3cur3P@ssw0rd!
```
**문제:**
- 운영 비밀이 Git에 평문으로 커밋된다. base64/stringData 여부와 무관.
- ConfigMap에 password가 들어가 있음 → RBAC `configmaps:get` 권한을 가진 모든 SA가 읽을 수 있음.
- secret source가 두 곳에 있어 회전 불가능.
- VSO/ESO/SealedSecrets 어느 경로에도 부합하지 않음.
---
## 좋은 예시 4: ImagePullSecret을 VSO로 Vault에서 sync
```yaml
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: registry-example-com
namespace: auth-prod
spec:
vaultAuthRef: auth-server
mount: kv
type: kv-v2
path: platform/registry/example-com
refreshAfter: 24h
destination:
name: registry-example-com
create: true
type: kubernetes.io/dockerconfigjson
transformation:
templates:
.dockerconfigjson:
text: |
{
"auths": {
"registry.example.com": {
"username": "{{ .Secrets.username }}",
"password": "{{ .Secrets.password }}",
"auth": "{{ printf "%s:%s" .Secrets.username .Secrets.password | b64enc }}"
}
}
}
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server
namespace: auth-prod
automountServiceAccountToken: true
imagePullSecrets:
- name: registry-example-com
```
**왜 좋은가:**
- registry credential도 Vault가 SoT. 하드코딩 없음.
- VSO가 `kubernetes.io/dockerconfigjson` 타입 Secret을 생성. kubelet이 바로 인식.
- SA에 묶여 있어 Deployment마다 imagePullSecrets 반복 선언 불필요.
---
## 좋은 예시 5: cert-manager + VSO 비교 — Ingress TLS는 cert-manager, internal mTLS는 VSO PKI
cert-manager가 외부 공인 도메인용 `kubernetes.io/tls` Secret을 발급하고, VSO `VaultPKISecret`은 internal service mesh mTLS용 단기 인증서를 발급한다. 두 경로 모두 최종 형태는 `kubernetes.io/tls` Secret으로 동일하므로 앱은 secret name만 구분한다.
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: auth-example-com
namespace: auth-prod
spec:
secretName: auth-example-com-tls
issuerRef:
kind: ClusterIssuer
name: letsencrypt-prod
dnsNames:
- auth.example.com
duration: 2160h
renewBefore: 360h
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: auth-server-internal-tls
namespace: auth-prod
spec:
vaultAuthRef: auth-server
mount: pki_int
role: auth-server
commonName: auth-server.auth-prod.svc
ttl: 24h
destination:
name: auth-server-internal-tls
create: true
type: kubernetes.io/tls
rolloutRestartTargets:
- kind: Deployment
name: auth-server
```
**왜 좋은가:**
- 외부 ACME 인증서는 공인 CA(Let's Encrypt), 내부는 조직 CA(Vault PKI)로 분리.
- 둘 다 같은 Secret 타입이라 Traefik/앱이 동일하게 소비 가능.
- VSO PKI는 24h TTL로 짧게 회전 → lateral movement window 최소화.
---
## 좋은 예시 6: Vault Agent Injector가 K8s Secret 없이 파일로 템플릿 렌더링
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-report-generator
namespace: reports-prod
spec:
replicas: 1
selector:
matchLabels:
app.kubernetes.io/name: report-generator
template:
metadata:
labels:
app.kubernetes.io/name: report-generator
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "report-generator"
vault.hashicorp.com/agent-inject-secret-report.conf: "kv/data/reports/smtp"
vault.hashicorp.com/agent-inject-template-report.conf: |
{{- with secret "kv/data/reports/smtp" -}}
[smtp]
host = {{ .Data.data.host }}
port = {{ .Data.data.port }}
username = {{ .Data.data.username }}
password = {{ .Data.data.password }}
{{- end }}
vault.hashicorp.com/secret-volume-path-report.conf: "/vault/secrets"
vault.hashicorp.com/agent-inject-containers: "report-generator"
vault.hashicorp.com/agent-run-as-user: "10001"
vault.hashicorp.com/agent-run-as-group: "10001"
spec:
serviceAccountName: report-generator
automountServiceAccountToken: true
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: report-generator
image: registry.example.com/reports/generator@sha256:2222222222222222222222222222222222222222222222222222222222222222
ports:
- name: http
containerPort: 8080
volumeMounts:
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumes:
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 32Mi
```
**왜 좋은가:**
- K8s Secret object가 **생성되지 않는다**. RBAC audit이 Secret API 호출 없이 Vault audit log로 대체된다.
- Vault Agent sidecar가 tmpfs에 템플릿 렌더링 → 앱은 파일만 읽음.
- legacy 앱이 INI/TOML 포맷 설정 파일을 요구할 때 적합.
**VSO vs Vault Agent Injector:**
| 항목 | VSO | Vault Agent Injector |
|---|---|---|
| 결과 | K8s Secret | Pod tmpfs 파일 |
| K8s API 노출 | Secret object 존재 | 없음 |
| 소비 방식 | envFrom/volume | file read |
| 회전 시 | `rolloutRestartTargets` | Agent re-render(인메모리) |
| 복잡도 | 낮음(CRD만) | 높음(sidecar/init) |
| 권장 | **운영 기본** | 템플릿/legacy 앱 |
---
## 나쁜 예시 2: Vault Injector annotation을 모든 컨테이너에 적용 + env 렌더링
```yaml
metadata:
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "auth-server"
vault.hashicorp.com/agent-inject-secret-db: "kv/data/auth-server/db"
vault.hashicorp.com/agent-inject-template-db: |
{{- with secret "kv/data/auth-server/db" -}}
export DB_USERNAME={{ .Data.data.username }}
export DB_PASSWORD={{ .Data.data.password }}
{{- end }}
```
**문제:**
- `agent-inject-containers` 미지정 → sidecar(metrics, proxy) 포함 모든 컨테이너의 `/vault/secrets`가 보임.
- `export DB_PASSWORD=...``source`로 읽는 launcher 스크립트 → process env로 비밀이 흘러 `/proc/<pid>/environ` 노출.
- dynamic lease renew를 활용하지 못하고, 회전 시 rollout trigger 없음.
---
## 좋은 예시 7: SealedSecret (VSO 미도입 환경/bootstrap)
```yaml
apiVersion: bitnami.com/v1alpha1
kind: SealedSecret
metadata:
name: vault-bootstrap-token
namespace: vault
spec:
encryptedData:
token: AgCd9sK... (public key로 암호화된 blob)
template:
metadata:
name: vault-bootstrap-token
namespace: vault
type: Opaque
```
**왜 좋은가:**
- Git 커밋 가능(public key로 암호화, cluster controller만 복호화).
- VSO 자체를 기동하기 위한 bootstrap credential(Vault root token, unseal key 대신 KMS auto-unseal 권장)에 적합.
- SealedSecrets controller가 `Secret`을 namespace에 materialize.
**주의:**
- 운영에서 **VSO가 기동되면 SealedSecrets 경로는 최소화**. 이중 source of truth 방지.
- Key 회전은 controller의 sealing key rotation 절차 준수.
---
## 좋은 예시 8: EncryptionConfiguration for Secret at-rest (API Server 레벨)
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources:
- secrets
providers:
- kms:
apiVersion: v2
name: platform-kms-v2
endpoint: unix:///var/run/kmsplugin/socket.sock
timeout: 3s
- aescbc:
keys:
- name: fallback-2026-q1
secret: c2VjcmV0LTMyLWJ5dGUtZmFsbGJhY2sta2V5LTIwMjZxMS1leGFtcGxl
- identity: {}
```
**K3s 활성화:**
```yaml
# /etc/rancher/k3s/config.yaml
secrets-encryption: true
kube-apiserver-arg:
- "encryption-provider-config=/etc/rancher/k3s/encryption-config.yaml"
- "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml"
- "audit-log-path=/var/log/k3s-audit.log"
```
**왜 좋은가:**
- KMS v2 provider가 primary → envelope encryption, 키는 KMS 외부에 존재.
- `aescbc`는 fallback. `identity`는 마지막(평문), 기존 Secret을 재암호화하기 전 decryption용.
- K3s config.yaml에 `secrets-encryption: true`로 선언. 서버 재시작 후 `kubectl get secrets -A -o json | kubectl replace -f -`로 기존 Secret 재암호화.
---
## 나쁜 예시 3: Kustomize secretGenerator로 운영 비밀 literal
```yaml
# overlays/prod/kustomization.yaml
secretGenerator:
- name: auth-server-db
literals:
- username=prod-admin
- password=S3cur3P@ssw0rd!
```
**문제:**
- 운영 비밀이 Git에 literal 평문 저장.
- Kustomize hash suffix는 비밀 보호가 아님.
- 회전 시 매번 Git 커밋 필요(감사/리뷰 시 비밀 노출).
- 운영은 VSO/ESO/SealedSecrets 경로로만 비밀을 배포해야 한다.
+499
View File
@@ -0,0 +1,499 @@
# db / migration 예시
모든 YAML은 `kubectl apply` 가능하다. 상세 Flyway Job 예시는 `examples/infra/flyway.md` 참조.
---
## 좋은 예시 1: auth-server와 keycloak DB 경계 분리 (CNPG 2 cluster)
```yaml
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: auth-pg
namespace: data-prod
labels:
app.kubernetes.io/name: auth-pg
app.kubernetes.io/part-of: auth-platform
backup.platform.io/tier: gold
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8
bootstrap:
initdb:
database: auth
owner: auth_app
secret: {name: auth-pg-app}
storage: {size: 50Gi, storageClass: fast-ssd-retain}
walStorage: {size: 20Gi, storageClass: fast-ssd-retain}
monitoring: {enablePodMonitor: true}
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: s3://acme-prod-pg-backups/auth-pg
s3Credentials:
accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID}
secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY}
wal: {compression: gzip, maxParallel: 8}
data: {compression: gzip, immediateCheckpoint: true, jobs: 4}
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: keycloak-pg
namespace: data-prod
labels:
app.kubernetes.io/name: keycloak-pg
app.kubernetes.io/part-of: identity-platform
backup.platform.io/tier: gold
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8
bootstrap:
initdb:
database: keycloak
owner: keycloak
secret: {name: keycloak-pg-app}
storage: {size: 30Gi, storageClass: fast-ssd-retain}
walStorage: {size: 10Gi, storageClass: fast-ssd-retain}
monitoring: {enablePodMonitor: true}
backup:
retentionPolicy: "30d"
barmanObjectStore:
destinationPath: s3://acme-prod-pg-backups/keycloak-pg
s3Credentials:
accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID}
secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY}
wal: {compression: gzip, maxParallel: 8}
data: {compression: gzip, jobs: 4}
```
왜 좋은가:
- auth와 keycloak이 별도 CNPG cluster → 장애 / 업그레이드 영향 분리
- 각각 schema ownership이 분리되어 migration 파이프라인도 분리 가능
- 백업 destination path도 분리 → retention / 암호화 정책 독립
❌ 나쁜 예시 1: 하나의 cluster의 하나의 database에 두 서비스 schema
```yaml
# single CNPG cluster, database=shared
# auth-server uses schema "auth"
# keycloak uses schema "keycloak"
# one Flyway project manages both
```
문제:
- 서비스별 업그레이드 / restore 영향 격리 불가
- Flyway history가 서로 섞임
- 한 서비스가 lock을 오래 잡으면 다른 서비스가 멈춤
---
## 좋은 예시 2: migration을 Helm hook으로 app보다 먼저 실행
```yaml
---
apiVersion: batch/v1
kind: Job
metadata:
name: auth-flyway-migrate
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
app.kubernetes.io/managed-by: Helm
annotations:
"helm.sh/hook": "pre-upgrade,pre-install"
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
spec:
parallelism: 1
completions: 1
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile: {type: RuntimeDefault}
containers:
- name: flyway
image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
args: ["-X", "migrate"]
env:
- {name: FLYWAY_URL, value: "jdbc:postgresql://auth-pg-rw.data-prod.svc:5432/auth"}
- {name: FLYWAY_USER, value: "auth_app"}
- {name: FLYWAY_LOCATIONS, value: "filesystem:/flyway/sql"}
- {name: FLYWAY_SCHEMAS, value: "auth_server"}
- {name: FLYWAY_DEFAULT_SCHEMA, value: "auth_server"}
- {name: FLYWAY_TABLE, value: "flyway_schema_history"}
- {name: FLYWAY_VALIDATE_ON_MIGRATE, value: "true"}
- {name: FLYWAY_BASELINE_ON_MIGRATE, value: "false"}
- {name: FLYWAY_CLEAN_DISABLED, value: "true"}
- name: FLYWAY_PASSWORD
valueFrom: {secretKeyRef: {name: auth-pg-app, key: password}}
resources:
requests: {cpu: "100m", memory: "256Mi"}
limits: {cpu: "1", memory: "1Gi"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: ["ALL"]}
volumeMounts:
- {name: sql, mountPath: /flyway/sql, readOnly: true}
- {name: tmp, mountPath: /tmp}
volumes:
- name: sql
configMap: {name: auth-flyway-sql}
- name: tmp
emptyDir: {}
```
왜 좋은가:
- Helm hook으로 app install/upgrade보다 **먼저** 실행 (`-10` weight)
- `before-hook-creation,hook-succeeded` 삭제 정책으로 이전 Job 깨끗이 정리
- `cleanDisabled=true` 명시 (실수로 `flyway clean` 방지)
- `parallelism: 1`, `backoffLimit: 0`, `activeDeadlineSeconds: 1800`
- digest pinning, restricted PSA
---
## 좋은 예시 3: Argo CD sync wave로 순서 지정
```yaml
---
apiVersion: batch/v1
kind: Job
metadata:
name: auth-flyway-migrate
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
annotations:
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
parallelism: 1
completions: 1
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: flyway
image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
args: ["-X", "migrate"]
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 1Gi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
annotations:
argocd.argoproj.io/sync-wave: "0"
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.24.0
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { memory: 1536Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
왜 좋은가:
- Argo CD는 sync-wave가 낮은 것부터 실행
- Helm hook과 혼용하지 않음 (한쪽만 사용)
❌ 나쁜 예시 2: Helm hook + Argo CD hook 혼용
```yaml
annotations:
"helm.sh/hook": "pre-upgrade"
"argocd.argoproj.io/sync-wave": "-1"
"argocd.argoproj.io/hook": Sync
```
문제:
- Argo CD가 Helm chart를 렌더링할 때 Helm hook을 일반 리소스로 취급해 sync 순서가 꼬임
- 실행이 중복되거나 누락됨
- 한 방식으로 통일할 것
---
## 좋은 예시 4: Expand → Migrate → Contract 3단계 릴리즈
### 배경
`users` 테이블의 `email` 컬럼 (NULL 허용)을 NOT NULL + 정규화된 `email_canonical` 컬럼으로 바꾸고 싶다.
### Release 1 — Expand
`V120__add_email_canonical_nullable.sql`:
```sql
-- flyway:executeInTransaction=false
ALTER TABLE users ADD COLUMN email_canonical text;
CREATE INDEX CONCURRENTLY idx_users_email_canonical ON users(email_canonical);
```
`V121__backfill_email_canonical.sql` (같은 릴리즈 또는 별도 배치 Job):
```sql
UPDATE users
SET email_canonical = lower(trim(email))
WHERE email_canonical IS NULL
AND email IS NOT NULL;
```
앱은 쓰기: `email` + `email_canonical` 둘 다 채움. 읽기: 여전히 `email`.
### Release 2 — Migrate
앱 읽기 경로를 `email_canonical`로 전환. 새 가입/수정은 `email_canonical`만 보장.
`V122__add_email_canonical_not_null.sql`:
```sql
-- 이 시점에는 모든 row에 email_canonical이 채워져 있어야 함
ALTER TABLE users ALTER COLUMN email_canonical SET NOT NULL;
ALTER TABLE users ADD CONSTRAINT users_email_canonical_unique UNIQUE (email_canonical);
```
### Release 3 — Contract
앱이 `email` 컬럼을 더 이상 읽지/쓰지 않는 버전으로 완전히 롤아웃된 뒤.
`V130__drop_legacy_email_column.sql`:
```sql
ALTER TABLE users DROP COLUMN email;
```
왜 좋은가:
- 각 릴리즈가 N-1 ↔ N 동시 운영 가능
- `CREATE INDEX CONCURRENTLY``-- flyway:executeInTransaction=false`로 분리
- Contract는 backfill + 앱 전환이 모두 끝난 뒤 별도 릴리즈
❌ 나쁜 예시 3: 한 릴리즈에 expand + contract
```sql
-- V100__rename_email.sql
ALTER TABLE users RENAME COLUMN email TO email_old;
ALTER TABLE users ADD COLUMN email text NOT NULL DEFAULT '';
-- 앱이 어느 버전이든 장애 발생 가능
```
문제:
- rolling deploy 중간에 앱이 N-1 / N 모두 실행 → 컬럼 없음 / 이름 다름으로 에러
- rollback 시 DB 상태가 앞서가 있어 N-1 앱이 기동 안 됨
---
## 좋은 예시 5: PITR 복구 계획 (CNPG)
```yaml
---
apiVersion: postgresql.cnpg.io/v1
kind: Cluster
metadata:
name: auth-pg-restore
namespace: data-prod
spec:
instances: 3
imageName: ghcr.io/cloudnative-pg/postgresql:16.4-8
storage: {size: 50Gi, storageClass: fast-ssd-retain}
walStorage: {size: 20Gi, storageClass: fast-ssd-retain}
bootstrap:
recovery:
source: auth-pg-source
recoveryTarget:
targetTime: "2026-04-16 09:45:00+00" # 잘못된 migration 직전
externalClusters:
- name: auth-pg-source
barmanObjectStore:
destinationPath: s3://acme-prod-pg-backups/auth-pg
s3Credentials:
accessKeyId: {name: cnpg-s3-credentials, key: ACCESS_KEY_ID}
secretAccessKey: {name: cnpg-s3-credentials, key: ACCESS_SECRET_KEY}
wal: {maxParallel: 8}
```
왜 좋은가:
- 운영 cluster는 건드리지 않고 `auth-pg-restore`로 복원
- `recoveryTarget.targetTime`을 분단위로 지정
- 복원 후 검증 → 운영 전환은 별도 runbook
---
## 좋은 예시 6: non-transactional DDL을 별도 migration 파일로
`V200__create_idx_users_last_login.sql`:
```sql
-- flyway:executeInTransaction=false
-- Long-running DDL. Run in low-traffic window.
-- Runtime estimate: ~15min on 50M rows.
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_last_login
ON users(last_login_at);
```
왜 좋은가:
- `CREATE INDEX CONCURRENTLY`는 Postgres에서 트랜잭션 내 실행 불가
- Flyway 8.2+ `executeInTransaction=false` directive로 파일 단위 제어
- 주석에 runtime 추정치 / 영향 명시
❌ 나쁜 예시 4: 트랜잭션 내 CREATE INDEX CONCURRENTLY
```sql
-- V200__.sql (기본 트랜잭션 모드)
CREATE INDEX CONCURRENTLY idx_users_last_login ON users(last_login_at);
-- → ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block
```
문제:
- Flyway가 자동으로 트랜잭션을 열기 때문에 실패
- `-- flyway:executeInTransaction=false`가 필수
---
## 좋은 예시 7: 운영 절차 runbook snippet
```text
# auth-server DB schema change — 2026-04-16 02:00 UTC maintenance window
## Pre-check (T-1d)
1. Pending migration 검토: 로컬 `flyway info`
2. PR review + migration 영향 분석 문서 작성 (expand/migrate/contract 단계)
3. Backup 상태 확인:
kubectl -n data-prod get scheduledbackup auth-pg-daily
kubectl -n data-prod get backup -l cnpg.io/cluster=auth-pg --sort-by=.metadata.creationTimestamp
## T-5min
1. On-demand backup:
cat <<EOF | kubectl apply -f -
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: auth-pg-pre-$(date -u +%Y%m%dT%H%M%SZ)
namespace: data-prod
spec:
cluster: {name: auth-pg}
method: barmanObjectStore
EOF
2. Argo CD sync (dry-run):
argocd app diff auth-server-prod
## Apply
1. argocd app sync auth-server-prod
→ Flyway Job이 sync-wave -1로 먼저 실행
→ Deployment는 wave 0에서 롤아웃
2. Flyway Job 로그 확인:
kubectl -n auth-prod logs job/auth-flyway-migrate
3. Deployment rollout 확인:
kubectl -n auth-prod rollout status deploy/auth-server
## Post-check
1. flyway info (적용 결과)
2. 앱 스모크 테스트
3. DB 메트릭 (slow query, error rate)
4. Next PITR recovery point 확인
```
왜 좋은가:
- migration 직전 on-demand backup
- migration → app rollout 순서가 선언 (sync-wave)으로 보장됨
- 실패 시 PITR 복구 지점이 명확
---
## 나쁜 예시 5: 앱 시작 시 자동 migration
```yaml
# Spring Boot application.properties
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
# 앱이 기동될 때마다 Flyway migrate 수행
```
문제:
- replicas=3이면 3개 Pod가 동시에 migrate 시도 (Flyway advisory lock이 막아주지만 기동 latency 증가)
- app rollout 실패와 migration 실패가 섞임 — 원인 추적 어려움
- 신규 Pod가 기동되는 rolling restart 시에도 매번 validate 수행
---
## 나쁜 예시 6: pg_dump 하나만으로 운영 복구
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: pg-dump-nightly
spec:
schedule: "0 3 * * *"
# ... pg_dumpall > /backup/dump.sql
```
문제:
- PITR 불가, RPO = 24h
- replication slot / extension / large object 누락
- 대규모 DB에서 restore 시간 폭증
- 같은 cluster 안 PVC에 저장하면 동시 소실
---
## 나쁜 예시 7: U__ undo migration 작성
```
flyway/
V120__add_column.sql
U120__drop_column.sql ← OSS Flyway는 실행 불가
```
문제:
- Flyway Community(OSS)는 undo 미지원 → `flyway undo`가 에러
- rollback 전략은 forward-only migration + PITR로 대체
+560
View File
@@ -0,0 +1,560 @@
# Flyway 예시
전 예시는 `kubectl apply -f` 가능한 완성 매니페스트다. 1000+ 서비스 규모에서 복사/수정해 쓸 수 있도록 full manifest로 구성했다.
---
## 좋은 예시 1: 완전한 Flyway Job (Helm hook 패턴)
### (1) ConfigMap — migration SQL
```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-flyway-sql
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: Helm
data:
V1__init_auth_schema.sql: |
CREATE TABLE IF NOT EXISTS users (
id bigserial PRIMARY KEY,
email text NOT NULL,
display_name text,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE UNIQUE INDEX IF NOT EXISTS idx_users_email ON users(lower(email));
V2__add_refresh_tokens.sql: |
CREATE TABLE IF NOT EXISTS refresh_tokens (
id bigserial PRIMARY KEY,
user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE,
token_hash bytea NOT NULL,
expires_at timestamptz NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX IF NOT EXISTS idx_refresh_tokens_user_id ON refresh_tokens(user_id);
V3__add_last_login_column.sql: |
ALTER TABLE users ADD COLUMN IF NOT EXISTS last_login_at timestamptz;
V4__create_idx_last_login_concurrently.sql: |
-- flyway:executeInTransaction=false
-- Long-running DDL. Schedule in low-traffic window.
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_last_login
ON users(last_login_at);
R__refresh_active_users_view.sql: |
CREATE OR REPLACE VIEW active_users AS
SELECT id, email, display_name, last_login_at
FROM users
WHERE last_login_at > now() - interval '30 days';
```
### (2) Vault Secrets Operator — DB password
```yaml
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: auth-pg-app
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
spec:
type: kv-v2
mount: kv
path: auth-prod/postgres/app
destination:
name: auth-pg-app
create: true
type: Opaque
refreshAfter: 1h
vaultAuthRef: vault-auth-auth-prod
```
### (3) Flyway Job — pre-upgrade / pre-install
```yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-flyway
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
---
apiVersion: batch/v1
kind: Job
metadata:
name: auth-flyway-migrate
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
app.kubernetes.io/component: db-migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: Helm
app.kubernetes.io/version: "2026.04.16"
annotations:
"helm.sh/hook": "pre-upgrade,pre-install"
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
spec:
parallelism: 1
completions: 1
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
spec:
serviceAccountName: auth-flyway
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile: {type: RuntimeDefault}
initContainers:
- name: flyway-info
image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
imagePullPolicy: IfNotPresent
args: ["info"]
env: &flywayEnv
- {name: FLYWAY_URL, value: "jdbc:postgresql://auth-pg-rw.data-prod.svc:5432/auth?sslmode=require"}
- {name: FLYWAY_USER, value: "auth_app"}
- {name: FLYWAY_LOCATIONS, value: "filesystem:/flyway/sql"}
- {name: FLYWAY_SCHEMAS, value: "auth_server"}
- {name: FLYWAY_DEFAULT_SCHEMA, value: "auth_server"}
- {name: FLYWAY_TABLE, value: "flyway_schema_history"}
- {name: FLYWAY_VALIDATE_ON_MIGRATE, value: "true"}
- {name: FLYWAY_BASELINE_ON_MIGRATE, value: "false"}
- {name: FLYWAY_OUT_OF_ORDER, value: "false"}
- {name: FLYWAY_MIXED, value: "false"}
- {name: FLYWAY_CLEAN_DISABLED, value: "true"}
- name: FLYWAY_PASSWORD
valueFrom: {secretKeyRef: {name: auth-pg-app, key: password}}
resources:
requests: {cpu: "50m", memory: "128Mi"}
limits: {cpu: "500m", memory: "512Mi"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: ["ALL"]}
volumeMounts:
- {name: sql, mountPath: /flyway/sql, readOnly: true}
- {name: tmp, mountPath: /tmp}
- name: flyway-validate
image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
imagePullPolicy: IfNotPresent
args: ["validate"]
env: *flywayEnv
resources:
requests: {cpu: "50m", memory: "128Mi"}
limits: {cpu: "500m", memory: "512Mi"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: ["ALL"]}
volumeMounts:
- {name: sql, mountPath: /flyway/sql, readOnly: true}
- {name: tmp, mountPath: /tmp}
containers:
- name: flyway-migrate
image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
imagePullPolicy: IfNotPresent
args: ["-X", "migrate"]
env: *flywayEnv
resources:
requests: {cpu: "100m", memory: "256Mi"}
limits: {cpu: "1", memory: "1Gi"}
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: ["ALL"]}
volumeMounts:
- {name: sql, mountPath: /flyway/sql, readOnly: true}
- {name: tmp, mountPath: /tmp}
volumes:
- name: sql
configMap: {name: auth-flyway-sql}
- name: tmp
emptyDir: {}
```
왜 좋은가:
- Helm hook으로 app Deployment보다 **먼저** 실행 (`pre-upgrade,pre-install`, weight `-10`)
- `before-hook-creation,hook-succeeded` 삭제 정책으로 과거 Job 정리
- initContainer로 `info` + `validate`를 먼저 실행해 실패를 앞당김
- 메인 container에서 `migrate` (advisory lock 덕분에 같은 Job이 중복 실행돼도 직렬화됨)
- `FLYWAY_CLEAN_DISABLED=true` (production 필수)
- `FLYWAY_BASELINE_ON_MIGRATE=false`, `FLYWAY_OUT_OF_ORDER=false`
- digest pinning, restricted PSA, anchor/alias로 env 중복 제거
- `parallelism: 1`, `backoffLimit: 0`, `activeDeadlineSeconds: 1800`
---
## 좋은 예시 2: Argo CD sync-wave 패턴
```yaml
---
apiVersion: batch/v1
kind: Job
metadata:
name: auth-flyway-migrate
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: db-migration
app.kubernetes.io/managed-by: argocd
annotations:
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
parallelism: 1
completions: 1
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
# (containers 세부는 예시 1과 동일; 요지만 재현)
- name: flyway
image: flyway/flyway@sha256:7d9f7c4e2a1b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d
args: ["-X", "migrate"]
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 1Gi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
annotations:
argocd.argoproj.io/sync-wave: "0"
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.24.0
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { memory: 1536Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
왜 좋은가:
- Argo CD가 wave `-1``0` 순서로 sync
- Helm hook과 혼용하지 않음
- `BeforeHookCreation` 정책으로 이전 Job 정리 후 새 Job 실행
---
## 좋은 예시 3: non-transactional DDL 전용 migration
`V4__create_idx_last_login_concurrently.sql`:
```sql
-- flyway:executeInTransaction=false
-- CREATE INDEX CONCURRENTLY는 Postgres에서 트랜잭션 내 실행 불가.
-- Flyway 8.2+ directive로 파일 단위 트랜잭션 비활성화.
-- Runtime estimate: 약 15분 (50M rows 기준).
-- Deploy window: 주간 트래픽 저점 (예: 화요일 03:00 UTC)
CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_last_login
ON users(last_login_at);
```
왜 좋은가:
- 파일 단독으로 분리 (다른 statement 없음)
- 주석에 runtime / window 명시
- `IF NOT EXISTS`로 재실행 안전성 (CREATE INDEX CONCURRENTLY 실패 시 INVALID 인덱스가 남을 수 있음 — 별도 cleanup 필요)
❌ 나쁜 예시 1: 트랜잭션 내 CREATE INDEX CONCURRENTLY
```sql
-- V4__.sql (executeInTransaction directive 없음)
CREATE INDEX CONCURRENTLY idx_users_last_login ON users(last_login_at);
```
문제:
- Flyway가 자동으로 트랜잭션을 열어 실행 → `ERROR: CREATE INDEX CONCURRENTLY cannot run inside a transaction block`
- 해결: `-- flyway:executeInTransaction=false` directive
---
## 좋은 예시 4: history table schema를 명시적으로 분리
env:
```yaml
- {name: FLYWAY_CREATE_SCHEMAS, value: "false"}
- {name: FLYWAY_INIT_SQL, value: "CREATE SCHEMA IF NOT EXISTS auth_server; CREATE SCHEMA IF NOT EXISTS flyway_history"}
- {name: FLYWAY_DEFAULT_SCHEMA, value: "flyway_history"}
- {name: FLYWAY_SCHEMAS, value: "flyway_history,auth_server"}
- {name: FLYWAY_TABLE, value: "flyway_schema_history"}
```
왜 좋은가:
- history table은 `flyway_history.flyway_schema_history`
- migration 대상 schema는 `auth_server`
- `createSchemas=false` 조건 하에서 `initSql`로 schema 사전 생성
---
## 좋은 예시 5: 운영 절차 (Helm + on-demand CNPG backup 연계)
```bash
# 1. pending migration 확인 (로컬)
docker run --rm -v $PWD/sql:/flyway/sql:ro \
-e FLYWAY_URL=jdbc:postgresql://stage.../auth \
-e FLYWAY_USER=auth_app -e FLYWAY_PASSWORD=... \
flyway/flyway@sha256:... info
# 2. PR review + migration 영향 분석
# 3. 운영 배포 직전 on-demand backup
cat <<EOF | kubectl apply -f -
apiVersion: postgresql.cnpg.io/v1
kind: Backup
metadata:
name: auth-pg-pre-2026-04-16
namespace: data-prod
spec:
cluster: {name: auth-pg}
method: barmanObjectStore
EOF
# 4. Helm upgrade (pre-upgrade hook이 Flyway Job 실행)
helm upgrade auth-server ./charts/auth-server \
--namespace auth-prod \
--values values/prod.yaml \
--atomic --timeout 20m
# 5. Flyway Job 로그 확인
kubectl -n auth-prod logs job/auth-flyway-migrate --all-containers
# 6. Deployment rollout 확인
kubectl -n auth-prod rollout status deploy/auth-server --timeout=10m
# 7. flyway info 재실행 (post-check)
```
왜 좋은가:
- migration 직전 on-demand backup으로 PITR 지점 확보
- `helm upgrade --atomic`으로 실패 시 자동 롤백
- hook이 `hook-succeeded` 정책으로 정리됨
---
## 좋은 예시 6: expand → migrate → contract (여러 릴리즈)
### Release 1 (V120~V121) — Expand
```sql
-- V120__add_email_canonical_nullable.sql
-- flyway:executeInTransaction=false
ALTER TABLE users ADD COLUMN email_canonical text;
CREATE INDEX CONCURRENTLY idx_users_email_canonical ON users(email_canonical);
```
```sql
-- V121__backfill_email_canonical.sql
-- Flyway 기본 트랜잭션 모드 — 소규모 테이블용. 대용량은 별도 배치 Job.
UPDATE users
SET email_canonical = lower(trim(email))
WHERE email_canonical IS NULL
AND email IS NOT NULL;
```
앱: 쓰기 시 두 컬럼 채움. 읽기는 아직 `email`.
### Release 2 (V122) — Migrate
```sql
-- V122__add_email_canonical_constraints.sql
ALTER TABLE users ALTER COLUMN email_canonical SET NOT NULL;
ALTER TABLE users ADD CONSTRAINT users_email_canonical_unique UNIQUE (email_canonical);
```
앱: 읽기/쓰기 모두 `email_canonical`. 기존 `email`도 fallback 유지.
### Release 3 (V130) — Contract
```sql
-- V130__drop_legacy_email_column.sql
ALTER TABLE users DROP COLUMN email;
```
왜 좋은가:
- N-1 ↔ N 동시 배포 허용
- 각 릴리즈가 독립 롤백 가능 (V130 제외 모두 non-destructive)
- expand와 contract가 같은 릴리즈에 섞이지 않음
---
## 좋은 예시 7: repeatable migration은 정의성 오브젝트에만
```
sql/
V120__add_email_canonical_nullable.sql
V121__backfill_email_canonical.sql
V122__add_email_canonical_constraints.sql
V130__drop_legacy_email_column.sql
R__refresh_active_users_view.sql
R__user_signup_function.sql
```
왜 좋은가:
- 핵심 schema change는 versioned
- view / function만 repeatable — 체크섬 변경 시 재적용
❌ 나쁜 예시 2: 순서 중요한 schema change를 R__로
```
R__create_users_table.sql ← 잘못. 순서 보장 없음
R__add_refresh_tokens.sql
```
문제:
- repeatable은 ordering 보장 없음 — 의존성 있는 change에 부적합
---
## 나쁜 예시 3: app startup에 migration 숨김
```properties
# application.properties
spring.flyway.enabled=true
spring.flyway.baseline-on-migrate=true
spring.flyway.out-of-order=true
```
문제:
- replicas=3이면 Pod 3개가 동시 migrate 시도 (advisory lock이 직렬화는 하지만 기동 latency 증가)
- app rollout 실패와 migration 실패가 섞임
- 신규 Pod 기동마다 validate 수행 → 오차 탐지 시점이 흐려짐
- `baseline-on-migrate=true` + `out-of-order=true` 조합은 migration history 신뢰도 저하
---
## 나쁜 예시 4: validate 실패 후 바로 repair
```bash
flyway validate || flyway repair
flyway migrate
```
문제:
- history 문제를 원인 분석 없이 덮음
- repair를 정상 운영 흐름처럼 사용 — 탐지력 저하
---
## 나쁜 예시 5: 적용된 migration 파일 수정
```
V42__add_refresh_token_column.sql
# 처음엔 빈 migration
# prod apply 후 컬럼 타입을 나중에 editor로 수정
```
문제:
- checksum mismatch → validate 실패
- 환경 간 재현성 깨짐
- 대응은 "새 V__ migration으로 교정"
---
## 나쁜 예시 6: U__ undo migration 작성
```
V120__add_column.sql
U120__drop_column.sql ← OSS Flyway는 실행 불가
```
문제:
- `flyway undo`는 Teams/Enterprise 전용
- OSS 환경에서는 U__ 파일이 실행되지 않아 오해 유발
- rollback은 forward-only + PITR로
---
## 나쁜 예시 7: parallelism 누락 + 재시도 무한
```yaml
spec:
# parallelism, backoffLimit, activeDeadlineSeconds 모두 누락
template:
spec:
restartPolicy: OnFailure # 무한 재시도 유발
```
문제:
- `backoffLimit` 기본 6 + `restartPolicy: OnFailure` → 실패 시 지수 backoff로 계속 재시도
- `activeDeadlineSeconds` 없음 → hang된 migration이 영원히 살아있음
- advisory lock이 걸린 실패 Job이 새 Job을 블록
---
## 나쁜 예시 8: Helm hook + Argo CD hook 혼용
```yaml
annotations:
"helm.sh/hook": "pre-upgrade"
"helm.sh/hook-weight": "-10"
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
```
문제:
- Argo CD가 Helm chart를 렌더링할 때 Helm hook annotation을 일반 리소스의 annotation으로 해석
- 결과적으로 Flyway Job이 일반 리소스로 취급되거나, 두 시스템이 서로 다른 시점에 Job을 만들어 race 발생
- 하나의 배포 도구에 맞춰 한쪽만 사용할 것
+400
View File
@@ -0,0 +1,400 @@
# K3s-specific 예시
모든 config 파일과 manifest는 1000+ 서비스 production 기준. YAML은 `kubectl apply --server-side --dry-run=server` 통과.
config.yaml은 `k3s server --help`와 공식 docs에 대응하는 키만 사용.
---
## 좋은 예시 1: prod server config.yaml (disable 세트 + audit + etcd snapshot S3)
```yaml
# /etc/rancher/k3s/config.yaml
# Single source of truth, identically applied to every server node via Ansible/CI.
write-kubeconfig-mode: "0640"
# --- Cluster network (must match on ALL server nodes) ---
cluster-cidr: "10.42.0.0/16"
service-cidr: "10.43.0.0/16"
cluster-dns: "10.43.0.10"
cluster-domain: "cluster.local"
flannel-backend: "vxlan"
# --- Disable packaged components (prod defaults) ---
disable:
- traefik
- servicelb
- local-storage
disable-cloud-controller: false
disable-network-policy: false
disable-helm-controller: false
# --- TLS SAN for kube-apiserver cert ---
tls-san:
- "k3s.prod.example.internal"
- "10.0.1.10"
- "10.0.1.11"
- "10.0.1.12"
# --- Audit logging ---
kube-apiserver-arg:
- "audit-log-path=/var/log/k3s/audit.log"
- "audit-log-maxage=30"
- "audit-log-maxbackup=10"
- "audit-log-maxsize=100"
- "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml"
- "feature-gates=ServerSideApply=true"
# --- Kubelet hardening ---
kubelet-arg:
- "config=/etc/rancher/k3s/kubelet.yaml"
# --- etcd snapshot to S3 (every 6h, keep 72h) ---
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-retention: 12
etcd-s3: true
etcd-s3-endpoint: "s3.ap-northeast-2.amazonaws.com"
etcd-s3-bucket: "k3s-etcd-backups-prod"
etcd-s3-region: "ap-northeast-2"
etcd-s3-folder: "prod-cluster"
# etcd-s3-access-key / etcd-s3-secret-key loaded from /etc/rancher/k3s/.env via systemd EnvironmentFile
# --- Registries mirror (cluster-internal pull accelerator) ---
# Not using embedded-registry (Spegel); using external Harbor mirror instead.
# See /etc/rancher/k3s/registries.yaml.
# --- Node labels / taints applied to this server's kubelet ---
node-label:
- "example.com/role=control-plane"
- "example.com/environment=prod"
node-taint:
- "node-role.kubernetes.io/control-plane=:NoSchedule"
```
**왜 좋은가:**
- `cluster-cidr` / `service-cidr` / `cluster-dns` / `cluster-domain` / `flannel-backend` / `disable` 세트가 Git 하나의 파일에 고정 → 다음 server 노드 조인 시 mismatch 불가
- audit log 설정이 kube-apiserver에 강제 주입됨 (SOC2/ISO27001 요구)
- etcd snapshot이 6시간 주기 + S3 업로드로 DR 대비
- ssm key는 파일에 없고 systemd EnvironmentFile로 주입 (Secret을 Git에 박지 않음)
---
## 좋은 예시 2: config.yaml.d drop-in 분할 (역할별 파일)
```yaml
# /etc/rancher/k3s/config.yaml.d/10-networking.yaml
cluster-cidr: "10.42.0.0/16"
service-cidr: "10.43.0.0/16"
flannel-backend: "vxlan"
```
```yaml
# /etc/rancher/k3s/config.yaml.d/20-audit.yaml
kube-apiserver-arg:
- "audit-log-path=/var/log/k3s/audit.log"
- "audit-policy-file=/etc/rancher/k3s/audit-policy.yaml"
```
```yaml
# /etc/rancher/k3s/config.yaml.d/30-etcd-backup.yaml
etcd-snapshot-schedule-cron: "0 */6 * * *"
etcd-snapshot-retention: 12
etcd-s3: true
etcd-s3-endpoint: "s3.ap-northeast-2.amazonaws.com"
etcd-s3-bucket: "k3s-etcd-backups-prod"
```
**왜 좋은가:**
- 역할별 파일 = 팀별 CODEOWNERS 분리 (네트워크 / 감사 / DR)
- 변경 diff가 좁아짐
- K3s는 drop-in 파일들을 병합해서 로드
---
## 좋은 예시 3: Traefik을 유지해야 할 때 (dev 클러스터) `HelmChartConfig`
```yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
labels:
app.kubernetes.io/name: traefik
app.kubernetes.io/instance: traefik-dev
app.kubernetes.io/component: ingress-controller
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
example.com/environment: dev
spec:
valuesContent: |-
deployment:
replicas: 2
ports:
web:
forwardedHeaders:
trustedIPs:
- 10.0.0.0/8
- 172.16.0.0/12
proxyProtocol:
trustedIPs:
- 10.0.0.0/8
websecure:
tls:
enabled: true
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
podSecurityContext:
runAsNonRoot: true
runAsUser: 65532
seccompProfile:
type: RuntimeDefault
metrics:
prometheus:
enabled: true
serviceMonitor:
enabled: true
```
**왜 좋은가:**
- packaged manifest를 직접 수정하지 않음
- `metadata.name` + `namespace`가 K3s 생성 `HelmChart`와 일치 → override가 merge됨
- secret/TLS 민감값은 `valuesSecrets`로 분리 가능 (이 예시는 non-sensitive만 보여줌)
- ServiceMonitor 활성화로 observability 자동 연결
---
## 좋은 예시 4: embedded registry mirror (Spegel) opt-in + registries.yaml
```yaml
# /etc/rancher/k3s/config.yaml (partial — applied to every node, server AND agent)
embedded-registry: true
```
```yaml
# /etc/rancher/k3s/registries.yaml
mirrors:
docker.io:
endpoint:
- "https://harbor.prod.example.internal"
registry.k8s.io:
endpoint:
- "https://harbor.prod.example.internal"
"*":
# Spegel will also share images between nodes via p2p
configs:
"harbor.prod.example.internal":
auth:
username: "robot$k3s-pull"
password: "__HARBOR_PULL_TOKEN__"
tls:
insecure_skip_verify: false
ca_file: "/etc/rancher/k3s/harbor-ca.crt"
```
**네트워크 전제 (반드시 검증):**
```bash
# From each node, to every other node:
nc -zv <other-node-ip> 5001 # Spegel p2p gossip
nc -zv <other-node-ip> 6443 # Local registry + K3s supervisor
```
**왜 좋은가:**
- 공식 문서 기준 포트 (`TCP 5001 + TCP 6443`) 정확히 반영
- external mirror (Harbor) + intra-cluster p2p 공유 조합 → airgap 경계 대비
- `embedded-registry: true`가 **모든 노드 (server+agent)의 config.yaml에 동일**하게 박혀야 함
---
## 좋은 예시 5: local-path를 dev/test에만 제한 (StorageClass 레벨)
```yaml
# local-path: default false, only used when explicitly requested
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: local-path
annotations:
storageclass.kubernetes.io/is-default-class: "false"
labels:
app.kubernetes.io/name: local-path
app.kubernetes.io/instance: local-path-dev
app.kubernetes.io/component: storage
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
example.com/environment: dev
example.com/storage-tier: local-ephemeral
provisioner: rancher.io/local-path
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: longhorn-replicated
annotations:
storageclass.kubernetes.io/is-default-class: "true"
labels:
app.kubernetes.io/name: longhorn
app.kubernetes.io/instance: longhorn-prod
app.kubernetes.io/component: storage
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/storage-tier: replicated-persistent
provisioner: driver.longhorn.io
allowVolumeExpansion: true
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
parameters:
numberOfReplicas: "3"
staleReplicaTimeout: "30"
fromBackup: ""
fsType: "ext4"
dataLocality: "best-effort"
```
**왜 좋은가:**
- `local-path`는 default가 아니고 `example.com/storage-tier: local-ephemeral`로 dev에서만 수용
- prod default는 Longhorn replicated (3 replica) + `reclaimPolicy: Retain`
- DB/Vault/MinIO PVC는 `storageClassName: longhorn-replicated` 명시
---
## 좋은 예시 6: registries.yaml에서 production pull-through mirror
```yaml
# /etc/rancher/k3s/registries.yaml (every node)
mirrors:
docker.io:
endpoint:
- "https://harbor.prod.example.internal/v2/dockerhub-proxy"
quay.io:
endpoint:
- "https://harbor.prod.example.internal/v2/quay-proxy"
registry.k8s.io:
endpoint:
- "https://harbor.prod.example.internal/v2/k8s-proxy"
ghcr.io:
endpoint:
- "https://harbor.prod.example.internal/v2/ghcr-proxy"
configs:
"harbor.prod.example.internal":
tls:
ca_file: "/etc/rancher/k3s/harbor-ca.crt"
auth:
username: "robot$k3s-pull"
password: "__HARBOR_PULL_TOKEN__"
```
**왜 좋은가:**
- public registry rate limit / downtime이 클러스터 pull을 못 죽임
- Harbor에서 CVE scan + image signing 검증
- 모든 노드에 동일 파일 (Ansible/Fleet push)
---
## 나쁜 예시 1: packaged traefik.yaml 직접 edit
```bash
ssh k3s-server-1
sudo vim /var/lib/rancher/k3s/server/manifests/traefik.yaml
# added forwardedHeaders.trustedIPs inline
sudo systemctl restart k3s
```
**문제:** K3s는 재시작 시 이 파일을 packaged 기본값으로 overwrite한다. 커스터마이징이 조용히 사라지고 서버별로 drift까지 생긴다. `HelmChartConfig`만 허용되는 경로.
---
## 나쁜 예시 2: server 간 서로 다른 critical 플래그
```yaml
# k3s-server-1: /etc/rancher/k3s/config.yaml
cluster-cidr: "10.42.0.0/16"
disable: [ traefik, servicelb ]
```
```yaml
# k3s-server-2: /etc/rancher/k3s/config.yaml
cluster-cidr: "10.44.0.0/16" # mismatched
disable: [ traefik ] # mismatched
```
**문제:** `critical configuration value mismatch` 로 server-2의 join이 실패하거나, 최악의 경우 이전 값이 캐시되어 silent drift가 생긴다. critical 값은 **Git 하나의 파일**로 통일해야 한다.
---
## 나쁜 예시 3: embedded registry mirror를 켜고 firewall 포트 미개방
```yaml
# /etc/rancher/k3s/config.yaml (all nodes)
embedded-registry: true
```
```bash
# On each node, firewalld / iptables only allows 6443, 10250, 8472
# Port 5001 is CLOSED between nodes
```
**문제:** Spegel은 **TCP 5001 (p2p) + TCP 6443 (registry + supervisor)** 양쪽이 모든 노드 간 reachable해야 한다. 5001이 막혀있으면 p2p gossip 실패로 image sharing이 작동하지 않고, pull 지연이 오히려 커진다. 공식 기준: `https://docs.k3s.io/installation/registry-mirror`.
---
## 나쁜 예시 4: 운영 AddOn을 서버마다 scp로 push
```bash
scp ingress-custom.yaml k3s-server-1:/var/lib/rancher/k3s/server/manifests/
# forgot server-2 and server-3
```
**문제:** K3s는 이 디렉터리를 server 간 동기화하지 않는다. 리더가 server-2로 바뀌면 AddOn이 사라진 것처럼 보인다. Git + ArgoCD/Flux가 단일 진입점이어야 한다.
---
## 나쁜 예시 5: prod postgres StatefulSet을 `local-path`에 배치
```yaml
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: data-postgres-0
namespace: prod-data-postgres
spec:
storageClassName: local-path
accessModes: [ ReadWriteOnce ]
resources:
requests:
storage: 200Gi
```
**문제:** local-path = 노드 hostPath. 노드가 죽으면 PVC 데이터도 죽는다. 스냅샷 불가, 복제 불가, 마이그레이션 불가. prod DB는 Longhorn replicated / Ceph RBD / 외부 CSI 필수.
---
## 나쁜 예시 6: Traefik 유지하면서 `HelmChartConfig` 이름을 잘못 박음
```yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik-custom # WRONG: must match the packaged HelmChart name
namespace: kube-system
spec:
valuesContent: |-
deployment:
replicas: 3
```
**문제:** `HelmChartConfig``metadata.name`은 K3s가 생성한 `HelmChart`와 **이름·namespace 모두 일치**해야 override가 merge된다. `traefik-custom`은 무시되고, override가 반영되지 않는다. 올바른 이름은 `traefik`.
+668
View File
@@ -0,0 +1,668 @@
# Keycloak 예시
Keycloak 26+ (Quarkus distribution) + Keycloak Operator 기준. 모든 YAML은 그대로 `kubectl apply`로 적용 가능한 완전한 manifest다.
---
## 좋은 예시 1: optimized 이미지 빌드 (두 단계)
`kc.sh build`로 Quarkus augmentation을 굽고, 실행 이미지를 분리한다.
```dockerfile
# Dockerfile.keycloak
FROM quay.io/keycloak/keycloak:26.0.7 AS builder
ENV KC_DB=postgres
ENV KC_HEALTH_ENABLED=true
ENV KC_METRICS_ENABLED=true
ENV KC_CACHE=ispn
ENV KC_CACHE_STACK=jdbc-ping
ENV KC_FEATURES=token-exchange,admin-fine-grained-authz
ENV KC_HTTP_ENABLED=true
RUN /opt/keycloak/bin/kc.sh build
FROM quay.io/keycloak/keycloak:26.0.7
COPY --from=builder /opt/keycloak/ /opt/keycloak/
USER 1000
ENTRYPOINT ["/opt/keycloak/bin/kc.sh", "start", "--optimized"]
```
**왜 좋은가:**
- 빌드 단계에서 augmentation 완료, 런타임은 runtime-only config만 수신
- `--optimized` 플래그로 매 기동 시 build 재실행 방지 (cold start 50% 단축)
- v26+ `--proxy` 제거 대응: legacy 옵션이 build 시 포함되지 않음
---
## 나쁜 예시 1: dev mode / 매 기동 build
```yaml
args:
- start-dev
```
또는
```yaml
args:
- start
```
**문제:**
- `start-dev`는 hostname-strict=false, H2 in-memory DB, TLS 해제 — production 부적합
- `start`는 optimized 이미지가 아니면 매 기동마다 Quarkus augmentation 수행 → cold start 2배+
- v26에서 `--proxy edge` 같은 legacy 옵션은 아예 기동 실패
---
## 좋은 예시 2: Keycloak Operator Keycloak CR (1차 권장)
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: keycloak
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
---
apiVersion: v1
kind: Secret
metadata:
name: keycloak-db-secret
namespace: keycloak
type: Opaque
stringData:
username: keycloak
password: REPLACE_VIA_VSO
---
apiVersion: v1
kind: Secret
metadata:
name: keycloak-tls
namespace: keycloak
type: kubernetes.io/tls
data:
tls.crt: LS0tLS1CRUdJTi... # cert-manager 발급 권장
tls.key: LS0tLS1CRUdJTi...
---
apiVersion: k8s.keycloak.org/v2alpha1
kind: Keycloak
metadata:
name: keycloak
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: keycloak-operator
spec:
instances: 3
image: registry.example.com/platform/keycloak:26.0.7-optimized
startOptimized: true
db:
vendor: postgres
host: keycloak-db-rw.keycloak.svc.cluster.local
port: 5432
database: keycloak
usernameSecret:
name: keycloak-db-secret
key: username
passwordSecret:
name: keycloak-db-secret
key: password
poolMinSize: 5
poolInitialSize: 5
poolMaxSize: 20
hostname:
hostname: https://auth.example.com
admin: https://admin-auth.example.com
strict: true
backchannelDynamic: false
http:
httpEnabled: true
tlsSecret: keycloak-tls
proxy:
headers: xforwarded
features:
enabled:
- token-exchange
- admin-fine-grained-authz
additionalOptions:
- name: cache
value: ispn
- name: cache-stack
value: jdbc-ping
- name: log-console-output
value: json
- name: metrics-enabled
value: "true"
- name: health-enabled
value: "true"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
scheduling:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app: keycloak
affinity:
podAntiAffinity:
preferredDuringSchedulingIgnoredDuringExecution:
- weight: 100
podAffinityTerm:
topologyKey: kubernetes.io/hostname
labelSelector:
matchLabels:
app: keycloak
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: keycloak
namespace: keycloak
spec:
minAvailable: 2
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app: keycloak
```
**왜 좋은가:**
- Operator가 StatefulSet, Service, cache stack 설정을 자동 관리
- hostname v2 (full URL, admin host 분리, strict=true, backchannelDynamic=false) 명시
- `startOptimized: true`로 Operator가 `kc.sh start --optimized` 실행
- PDB `minAvailable: 2` + topologySpread로 zone-level disruption 방어
---
## 나쁜 예시 2: 수제 Deployment + `--proxy edge`
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: keycloak
spec:
replicas: 1
template:
spec:
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:26.0.7
args: ["start", "--proxy", "edge"]
env:
- name: KC_HOSTNAME
value: auth.example.com
- name: KC_HOSTNAME_STRICT
value: "false"
```
**문제:**
- `--proxy` 옵션은 v26에서 제거되어 기동 실패
- `KC_HOSTNAME`에 scheme 없는 호스트명 단독 전달 → v2 검증에서 경고
- `KC_HOSTNAME_STRICT=false`는 proxy hop이 Host 헤더를 조작할 수 있는 공격 벡터를 열어둠
- replicas: 1 + Deployment → rolling update 시 Infinispan cluster membership 이슈 + 단일 장애
---
## 좋은 예시 3: Probe (management port 9000)
```yaml
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: management
containerPort: 9000
protocol: TCP
startupProbe:
httpGet:
path: /health/started
port: 9000
scheme: HTTP
periodSeconds: 5
failureThreshold: 60
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /health/ready
port: 9000
scheme: HTTP
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /health/live
port: 9000
scheme: HTTP
initialDelaySeconds: 60
periodSeconds: 30
failureThreshold: 3
timeoutSeconds: 3
```
**왜 좋은가:**
- 9000은 management port (`KC_HTTP_MANAGEMENT_PORT` 기본값)
- startupProbe 5분 유예: JVM + Quarkus + DB migration cold start 수용
- readiness는 `/health/ready` (DB connectivity 포함), liveness는 `/health/live` (프로세스 생존)
---
## 나쁜 예시 3: Probe를 8080 `/` 로 설정
```yaml
readinessProbe:
httpGet:
path: /
port: 8080
periodSeconds: 3
failureThreshold: 2
```
**문제:**
- 8080 `/`는 redirect 응답이고 DB / cache readiness를 검증하지 않음
- `failureThreshold: 2` + `periodSeconds: 3`은 cold start 중 pod 재시작 유발
- health endpoint가 켜져 있어도 사용하지 않아 관찰 포인트 상실
---
## 좋은 예시 4: Service + ServiceMonitor
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
spec:
type: ClusterIP
selector:
app: keycloak
ports:
- name: http
port: 8080
targetPort: 8080
- name: management
port: 9000
targetPort: 9000
---
apiVersion: v1
kind: Service
metadata:
name: keycloak-headless
namespace: keycloak
spec:
type: ClusterIP
clusterIP: None
selector:
app: keycloak
ports:
- name: http
port: 8080
targetPort: 8080
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: keycloak
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app.kubernetes.io/name: keycloak
endpoints:
- port: management
path: /metrics
interval: 30s
scrapeTimeout: 10s
```
**왜 좋은가:**
- ClusterIP Service가 사용자 트래픽용(8080), management(9000)을 분리 expose
- Headless service는 cache peer discovery 보조 (jdbc-ping에서는 불필요하지만 DNS_PING fallback 대비)
- ServiceMonitor는 management port의 `/metrics`만 scrape
---
## 좋은 예시 5: Ingress — SSO host + Admin host 분리
```yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak-sso
namespace: keycloak
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-buffer-size: "128k"
nginx.ingress.kubernetes.io/proxy-body-size: "4m"
spec:
ingressClassName: nginx
tls:
- hosts:
- auth.example.com
secretName: keycloak-sso-tls
rules:
- host: auth.example.com
http:
paths:
- path: /realms/
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
- path: /resources/
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
- path: /.well-known/
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
- path: /js/
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak-admin
namespace: keycloak
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.0.0/16"
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
spec:
ingressClassName: nginx
tls:
- hosts:
- admin-auth.example.com
secretName: keycloak-admin-tls
rules:
- host: admin-auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
```
**왜 좋은가:**
- SSO host는 `/realms/`, `/resources/`, `/.well-known/`, `/js/` 만 공개 (필요 최소)
- Admin host는 별도 hostname + IP whitelist + forward-auth 2중 보호
- `/metrics`, `/health*`, `/admin/`이 SSO host에 노출되지 않음
---
## 나쁜 예시 4: 전체 공개 + 9000 노출
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
spec:
rules:
- host: auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 8080
- path: /metrics
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 9000
```
**문제:**
- `/` 공개 → `/admin/` 포함 전부 외부 노출 → credential stuffing / brute force 표면 확장
- `/metrics`는 인증이 없는 운영 데이터 endpoint → 정보 유출
- 9000 management port가 인터넷에 노출 → health/metrics 둘 다 오픈
---
## 좋은 예시 6: KeycloakRealmImport CR
```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: platform-realm
namespace: keycloak
type: Opaque
stringData:
realm.json: |
{
"realm": "platform",
"enabled": true,
"sslRequired": "external",
"registrationAllowed": false,
"loginWithEmailAllowed": true,
"accessTokenLifespan": 300,
"clients": [
{
"clientId": "auth-server",
"protocol": "openid-connect",
"publicClient": false,
"standardFlowEnabled": true,
"redirectUris": ["https://auth-server.example.com/*"],
"webOrigins": ["https://auth-server.example.com"]
}
],
"roles": {
"realm": [
{"name": "platform-admin"},
{"name": "platform-user"}
]
}
}
---
apiVersion: k8s.keycloak.org/v2alpha1
kind: KeycloakRealmImport
metadata:
name: platform-realm
namespace: keycloak
spec:
keycloakCRName: keycloak
realm:
realm: platform
enabled: true
sslRequired: external
registrationAllowed: false
loginWithEmailAllowed: true
accessTokenLifespan: 300
```
**왜 좋은가:**
- Realm을 선언적으로 관리 (GitOps 연계)
- Operator가 `keycloak` CR ready 이후 server-side import Job을 자동 생성
- client secret처럼 민감한 값은 별도 Vault 경로로 분리, realm JSON은 Git 안전
---
## 나쁜 예시 5: kcadm.sh pipeline 직접 호출
```bash
# CI pipeline
kcadm.sh config credentials \
--server https://auth.example.com \
--realm master \
--user admin \
--password $KEYCLOAK_ADMIN_PASSWORD
kcadm.sh create realms -s realm=platform -s enabled=true
kcadm.sh create clients -r platform -s clientId=auth-server
```
**문제:**
- 상태가 선언적이지 않아 drift 탐지 불가
- admin credential이 CI runner 환경에 상주
- 실패 시 재실행 안전성(idempotency) 없음
- GitOps 원칙과 충돌
---
## 좋은 예시 7: SecurityContext + Resource
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: keycloak
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
env:
- name: JAVA_OPTS_APPEND
value: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50 -Djgroups.dns.query=keycloak-headless.keycloak.svc.cluster.local"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
volumeMounts:
- name: tmp
mountPath: /tmp
- name: data
mountPath: /opt/keycloak/data
volumes:
- name: tmp
emptyDir: {}
- name: data
emptyDir: {}
```
**왜 좋은가:**
- Restricted PSS 전부 충족: non-root, no privilege escalation, RO root fs, cap drop ALL
- `MaxRAMPercentage=70`은 JVM이 container limit의 70%까지만 heap 사용 (나머지는 direct memory / metaspace)
- `readOnlyRootFilesystem: true` + emptyDir 마운트로 runtime write path 격리
---
## 좋은 예시 8: Vault에서 DB credential 주입 (VSO)
```yaml
---
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: keycloak-db
namespace: keycloak
spec:
vaultAuthRef: default
mount: kv
path: keycloak/db
type: kv-v2
refreshAfter: 1h
destination:
name: keycloak-db-secret
create: true
overwrite: true
transformation:
excludeRaw: true
templates:
username:
text: '{{ .Secrets.username }}'
password:
text: '{{ .Secrets.password }}'
```
**왜 좋은가:**
- Vault KV v2의 `keycloak/db`에서 credential을 K8s Secret으로 동기화
- 1시간 주기 refresh, VSO가 Pod를 재시작시켜 rotation 적용 가능 (별도 `rolloutRestartTargets` 설정 시)
- Git에 평문 credential이 없다
---
## 나쁜 예시 6: env에 평문 credential
```yaml
env:
- name: KC_DB_PASSWORD
value: "SuperSecret123!"
- name: KEYCLOAK_ADMIN_PASSWORD
value: "admin"
```
**문제:**
- Git에 평문 저장 → 권한 있는 모든 인원이 조회 가능
- 기본 `admin/admin` credential → bootstrap 직후 자동화된 스캐너에 탈취 위험
- rotation 경로 없음
+548
View File
@@ -0,0 +1,548 @@
# Kustomize 예시
모든 예시는 Kustomize v5 문법 기준. 렌더 검증:
```bash
kubectl kustomize <dir> | kubectl apply --server-side --field-manager=ci --dry-run=server -f -
```
---
## 좋은 예시 1: base / components / overlays 전체 구조 + 실제 base `kustomization.yaml`
```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/
prod/kr-main/
kustomization.yaml
patches/
auth-resources.yaml
auth-ingress-host.yaml
```
```yaml
# k8s/base/app/units/identity/auth/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- servicemonitor.yaml
- pdb.yaml
- hpa.yaml
labels:
- pairs:
app.kubernetes.io/name: auth
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
includeSelectors: false
includeTemplates: true
```
**왜 좋은가:**
- base가 환경·region을 모른다 (namespace / replicas / host / image tag 전부 없음)
- `labels:` (v5) 사용, `commonLabels` 없음 → selector immutability 안전
- `includeTemplates: true`로 Pod label에는 전파되어 observability 쿼리 가능
- selector에 들어가는 label은 base의 Deployment 내부에서 명시적으로 고정
---
## 좋은 예시 2: base Deployment (완전 apply-ready)
```yaml
# k8s/base/app/units/identity/auth/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
annotations:
example.com/owner-email: identity-sre@example.com
spec:
replicas: 2
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
template:
metadata:
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
prometheus.io/path: "/actuator/prometheus"
spec:
serviceAccountName: auth
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
terminationGracePeriodSeconds: 45
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
containers:
- name: auth
image: registry.example.com/auth:placeholder
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: management
containerPort: 8081
protocol: TCP
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75 -XX:+UseG1GC"
envFrom:
- configMapRef:
name: auth-config
- secretRef:
name: auth-secrets
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
startupProbe:
httpGet:
path: /actuator/health/liveness
port: management
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: management
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: management
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumeMounts:
- name: tmp
mountPath: /tmp
- name: cache
mountPath: /app/cache
volumes:
- name: tmp
emptyDir:
sizeLimit: 64Mi
- name: cache
emptyDir:
sizeLimit: 256Mi
```
**왜 좋은가:**
- image는 `:placeholder`, overlay의 `images:`가 digest로 patch → base는 버전 모름
- `revisionHistoryLimit: 5` → 대규모 cluster에서 ReplicaSet 누적 방지
- PodSecurity restricted 호환 (non-root, seccomp RuntimeDefault, capabilities drop ALL, readOnlyRootFilesystem)
- startup/liveness/readiness 3종이 타이밍 분리 (startup 150s, liveness 30s, readiness 15s 윈도우)
- topologySpreadConstraints로 zone별 분산
- `automountServiceAccountToken: false` (ServiceAccount token을 쓰지 않는 워크로드)
---
## 좋은 예시 3: overlay prod/kr-main — 환경 차이만
```yaml
# k8s/overlays/prod/kr-main/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod-identity-auth
resources:
- ../../../base/app/units/identity/auth
components:
- ../../../components/with-topology-spread-zone
- ../../../components/with-pdb-tier1
labels:
- pairs:
example.com/environment: prod
example.com/region: kr-main
example.com/slo-tier: tier-1
includeSelectors: false
includeTemplates: true
images:
- name: registry.example.com/auth
digest: "sha256:f1a2b3c4d5e6f7081920aabbccddeeff00112233445566778899aabbccddeeff"
replicas:
- name: auth
count: 6
patches:
- target:
kind: Deployment
name: auth
path: patches/auth-resources.yaml
- target:
kind: Ingress
name: auth-public
patch: |-
- op: replace
path: /spec/rules/0/host
value: auth.example.com
- op: replace
path: /spec/tls/0/hosts/0
value: auth.example.com
```
```yaml
# k8s/overlays/prod/kr-main/patches/auth-resources.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
spec:
selector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
template:
metadata:
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth
image: registry.example.com/auth-server:1.24.0
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
**왜 좋은가:**
- overlay 자체가 짧음 (base를 재작성하지 않음)
- digest 기반 image pinning
- `components:`로 zone spread + PDB tier-1을 재사용
- `labels:` 사용, `includeSelectors: false` → selector immutability 안전
- replicas override는 HPA minReplicas와 일치 (HPA base에서 `minReplicas: 6`으로 설정되어 있다고 가정)
---
## 좋은 예시 4: Kustomize Component — `with-pdb-tier1`
```yaml
# k8s/components/with-pdb-tier1/kustomization.yaml
apiVersion: kustomize.config.k8s.io/v1alpha1
kind: Component
resources:
- pdb.yaml
```
```yaml
# k8s/components/with-pdb-tier1/pdb.yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/slo-tier: tier-1
spec:
minAvailable: 50%
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
```
**왜 좋은가:**
- `kind: Component`로 선언 → 여러 overlay에서 `components:` 키로 재사용
- tier-1의 PDB 정책(50% minAvailable)이 단일 파일에 고정
- 다른 tier는 별도 component (`with-pdb-tier2`, `with-pdb-tier3`)
---
## 좋은 예시 5: ConfigMap generator + hash suffix를 활용한 자동 rollout
```yaml
# k8s/base/app/units/identity/auth/kustomization.yaml (with generator)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
configMapGenerator:
- name: auth-config
files:
- application.yaml=config/application.yaml
- logback.xml=config/logback.xml
options:
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/component: config
generatorOptions:
disableNameSuffixHash: false
```
**왜 좋은가:**
- ConfigMap 내용 변경 시 hash suffix가 바뀜 → Deployment가 새 이름을 참조 → rolling update 자동 트리거
- annotation 기반 "checksum" hack 불필요
- Secret은 generator로 만들지 않고 External Secrets로 관리
---
## 좋은 예시 6: HPA v2 + behavior (base 리소스)
```yaml
# k8s/base/app/units/identity/auth/hpa.yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: auth
minReplicas: 2
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Resource
resource:
name: memory
target:
type: Utilization
averageUtilization: 80
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
```
**왜 좋은가:**
- HPA v2 behavior로 scaleDown stabilization (5분) vs scaleUp aggressive (즉시) 분리
- overlay는 `minReplicas` / `maxReplicas`만 override하고 behavior는 상속
---
## 나쁜 예시 1: `commonLabels`로 environment 주입 → selector immutable 에러
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
namespace: prod-identity-auth
resources:
- ../../base/app/units/identity/auth
commonLabels:
example.com/environment: prod
```
**문제:** `commonLabels``spec.selector.matchLabels`에 자동 주입된다. 이미 live 상태인 Deployment/StatefulSet에 apply하면 `The Deployment "auth" is invalid: spec.selector: Invalid value: ...: field is immutable` 로 차단. 해결: `labels:` + `includeSelectors: false`로 교체.
---
## 나쁜 예시 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)
```
**문제:** overlay가 base의 95%를 복붙 + 몇 줄 수정. drift 발생 시점부터 base가 의미 없어진다. 해결: overlay는 `patches:` + `images:` + `replicas:` + `labels:`만 쓰고 전체 리소스는 base에서 가져온다.
---
## 나쁜 예시 3: 운영 secret을 `secretGenerator`로 plaintext Git 커밋
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD)
secretGenerator:
- name: auth-secrets
literals:
- OAUTH_CLIENT_SECRET=s3cr3t-prod-value
- DB_PASSWORD=prod-db-password
```
**문제:** plaintext secret이 Git에 박힌다. 해결: External Secrets Operator + Vault / AWS Secrets Manager / Bitwarden Secrets. 또는 SealedSecrets (public key encrypted).
---
## 나쁜 예시 4: `patchesStrategicMerge` / `patchesJson6902` (deprecated)
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD, v5 deprecated)
patchesStrategicMerge:
- patches/auth-resources.yaml
patchesJson6902:
- target:
group: apps
version: v1
kind: Deployment
name: auth
path: patches/auth-env.yaml
```
**문제:** 두 필드는 Kustomize v5에서 deprecated (여전히 동작하지만 신규 사용 금지). 하나의 `patches:` 필드로 통합되어 strategic merge + JSON patch 양쪽을 지원하므로 혼재할 이유 없음. 해결: `patches:` 단일 키 사용.
---
## 나쁜 예시 5: base에 환경 host / domain 고정
```yaml
# k8s/base/app/units/identity/auth/ingress.yaml (BAD)
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-public
spec:
rules:
- host: auth.example.com # prod host hardcoded in base
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: auth
port:
number: 8080
```
**문제:** base가 prod를 전제한다. dev/staging overlay가 host를 교체하려고 `patches:`를 추가해야 하고, base는 더 이상 환경 중립이 아니다. 해결: base에서는 host를 placeholder (`auth.placeholder.invalid`)로 두고 overlay `patches:`에서 주입.
---
## 나쁜 예시 6: `bases:` 사용 (v2.1에서 `resources:`로 통합됨)
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD)
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
bases:
- ../../base/app/units/identity/auth
```
**문제:** `bases:`는 v2.1에서 `resources:`에 흡수됨. 신규 코드에서 사용 금지. 해결: `resources:` 사용.
---
## 나쁜 예시 7: HPA가 있는 Deployment에 overlay `replicas:`로 고정값 주입
```yaml
# k8s/overlays/prod/kustomization.yaml (BAD — conflicts with HPA)
replicas:
- name: auth
count: 3
```
(한편 HPA는 `minReplicas: 6 / maxReplicas: 20`)
**문제:** Kustomize가 `replicas: 3`으로 apply → HPA가 즉시 6으로 끌어올림 → 매 ArgoCD sync마다 `out-of-sync` flap. 해결: HPA 활성 리소스에서는 overlay `replicas:`를 쓰지 않고, HPA `minReplicas`를 환경별로 patch.
+855
View File
@@ -0,0 +1,855 @@
# MinIO 예시
MinIO Operator + Tenant CRD (`minio.min.io/v2`) + KES + Vault transit 기준. 모든 manifest는 `kubectl apply` 적용 가능한 완전한 형태다.
---
## 좋은 예시 1: Namespace + Tenant configuration Secret
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: minio-prod
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/audit: restricted
app.kubernetes.io/part-of: storage-platform
---
apiVersion: v1
kind: Secret
metadata:
name: minio-tenant-env
namespace: minio-prod
type: Opaque
stringData:
config.env: |
export MINIO_ROOT_USER="REPLACE_VIA_VSO"
export MINIO_ROOT_PASSWORD="REPLACE_VIA_VSO"
export MINIO_STORAGE_CLASS_STANDARD="EC:4"
export MINIO_STORAGE_CLASS_RRS="EC:2"
export MINIO_BROWSER_REDIRECT_URL="https://minio-console.internal.example.com"
export MINIO_SERVER_URL="https://s3.example.com"
export MINIO_IDENTITY_OPENID_CONFIG_URL="https://auth.example.com/realms/platform/.well-known/openid-configuration"
export MINIO_IDENTITY_OPENID_CLIENT_ID="minio"
export MINIO_IDENTITY_OPENID_CLAIM_NAME="policy"
export MINIO_IDENTITY_OPENID_SCOPES="openid,profile,email"
export MINIO_PROMETHEUS_AUTH_TYPE="jwt"
---
# 실 운영에서는 VSO가 이 Secret을 채움
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: minio-root-creds
namespace: minio-prod
spec:
vaultAuthRef: default
mount: kv
path: minio/prod/root
type: kv-v2
refreshAfter: 24h
destination:
name: minio-tenant-env
create: false
overwrite: true
transformation:
excludeRaw: true
templates:
config.env:
text: |
export MINIO_ROOT_USER="{{ .Secrets.username }}"
export MINIO_ROOT_PASSWORD="{{ .Secrets.password }}"
export MINIO_STORAGE_CLASS_STANDARD="EC:4"
export MINIO_BROWSER_REDIRECT_URL="https://minio-console.internal.example.com"
export MINIO_SERVER_URL="https://s3.example.com"
export MINIO_IDENTITY_OPENID_CONFIG_URL="https://auth.example.com/realms/platform/.well-known/openid-configuration"
export MINIO_IDENTITY_OPENID_CLIENT_ID="minio"
export MINIO_IDENTITY_OPENID_CLIENT_SECRET="{{ .Secrets.oidc_client_secret }}"
export MINIO_IDENTITY_OPENID_CLAIM_NAME="policy"
export MINIO_IDENTITY_OPENID_SCOPES="openid,profile,email"
export MINIO_PROMETHEUS_AUTH_TYPE="jwt"
```
**왜 좋은가:**
- Tenant configuration은 **shell-source 형식**(`export KEY=VALUE`) Secret으로 전달 (Operator 규약)
- Root credential을 Vault KV에서 VSO가 주입 — Git에 평문 없음
- OIDC 통합 (Keycloak), storage class EC:4, Prometheus JWT auth 한 파일에 고정
- `MINIO_SERVER_URL`로 외부 S3 endpoint 명시 (presigned URL 생성 시 사용)
---
## 좋은 예시 2: Tenant CR — 4 server × 4 volume + KES + TLS
```yaml
apiVersion: minio.min.io/v2
kind: Tenant
metadata:
name: minio
namespace: minio-prod
labels:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio-prod
app.kubernetes.io/part-of: storage-platform
app.kubernetes.io/managed-by: minio-operator
annotations:
prometheus.io/path: /minio/v2/metrics/cluster
prometheus.io/port: "9000"
prometheus.io/scrape: "true"
spec:
image: quay.io/minio/minio:RELEASE.2025-01-20T14-49-07Z
imagePullPolicy: IfNotPresent
mountPath: /export
configuration:
name: minio-tenant-env
requestAutoCert: true
certConfig:
commonName: minio.minio-prod.svc.cluster.local
organizationName:
- example.com
dnsNames:
- minio.minio-prod.svc.cluster.local
- "*.minio-hl.minio-prod.svc.cluster.local"
- s3.example.com
pools:
- name: pool-0
servers: 4
volumesPerServer: 4
volumeClaimTemplate:
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 500Gi
storageClassName: local-xfs-retain
resources:
requests:
cpu: 500m
memory: 2Gi
limits:
cpu: "4"
memory: 8Gi
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
v1.min.io/tenant: minio
v1.min.io/pool: pool-0
topologyKey: kubernetes.io/hostname
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
v1.min.io/tenant: minio
tolerations:
- key: storage
operator: Equal
value: dedicated
effect: NoSchedule
features:
bucketDNS: false
domains:
console: https://minio-console.internal.example.com
minio:
- https://s3.example.com
kes:
image: quay.io/minio/kes:2025-01-16T16-24-39Z
replicas: 2
kesSecret:
name: kes-configuration
imagePullPolicy: IfNotPresent
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
containerSecurityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
prometheusOperator: true
podManagementPolicy: Parallel
exposeServices:
minio: true
console: false
logging:
anonymous: false
json: true
quiet: false
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: minio
namespace: minio-prod
spec:
minAvailable: 3
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
v1.min.io/tenant: minio
```
**왜 좋은가:**
- `servers × volumesPerServer = 4 × 4 = 16` drive → erasure coding 최소 요건 충족, `EC:4` 기본 parity (4 drive 장애 허용)
- `requestAutoCert: true` + `certConfig.dnsNames`로 Operator가 API/Console TLS 자동 발급
- `podAntiAffinity` hostname required → 한 node에 MinIO pod 복수 배치 금지 (EC 의미 보존)
- KES가 별도 2 replica로 사이드카 없이 Deployment로 분리 (Tenant CR에서 관리됨)
- `exposeServices.console: false` → Console은 Tenant Service에서 Ingress로 별도 처리만 허용
- `minAvailable: 3` → 4 server 중 1 동시 drain까지 허용 (write quorum 보존)
---
## 나쁜 예시 1: 단일 Deployment로 MinIO
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
spec:
replicas: 1
template:
spec:
containers:
- name: minio
image: minio/minio
args: ["server", "/data"]
env:
- name: MINIO_ROOT_USER
value: minioadmin
- name: MINIO_ROOT_PASSWORD
value: minioadmin
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
emptyDir: {}
```
**문제:**
- Single-drive MinIO → erasure coding 없음, 1 drive 장애 = 전체 data loss
- Deployment = 재시작 시 PVC binding 보장 없음, 복수 replica 시 동일 volume 충돌
- emptyDir → pod 재시작 시 모든 object 사라짐
- 기본 `minioadmin/minioadmin` credential → 공개 인터넷 스캐너가 수 분 내 탈취
- Operator + Tenant가 자동화하는 인증서, 서비스, headless, auto-restart를 전부 수제로 다시 만들어야 함
---
## 좋은 예시 3: KES configuration + Vault transit
```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: kes-configuration
namespace: minio-prod
type: Opaque
stringData:
server-config.yaml: |
version: v1
address: 0.0.0.0:7373
admin:
identity: disabled
tls:
key: /tmp/kes/server.key
cert: /tmp/kes/server.cert
policy:
minio-app:
allow:
- /v1/key/create/minio-*
- /v1/key/generate/minio-*
- /v1/key/decrypt/minio-*
- /v1/key/bulk/decrypt/minio-*
- /v1/key/list/minio-*
- /v1/status
- /v1/metrics
- /v1/api
identities:
- ${MINIO_KES_IDENTITY}
keystore:
vault:
endpoint: https://vault.vault.svc.cluster.local:8200
engine: transit
version: v1
namespace: ""
prefix: minio
approle:
id: ${VAULT_APPROLE_ID}
secret: ${VAULT_APPROLE_SECRET}
retry: 15s
tls:
ca: /tmp/kes/vault-ca.crt
status:
ping: 10s
---
# Vault AppRole credential은 VSO 또는 별도 Secret으로 주입
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: kes-vault-approle
namespace: minio-prod
spec:
vaultAuthRef: default
mount: kv
path: minio/kes/approle
type: kv-v2
refreshAfter: 24h
destination:
name: kes-vault-approle
create: true
overwrite: true
```
그리고 bucket에 SSE-KMS 적용:
```bash
mc alias set minio https://s3.example.com $ROOT_USER $ROOT_PASS
# Vault transit에 key 생성
mc admin kms key create minio minio-critical
# bucket에 SSE-KMS 기본 적용
mc encrypt set sse-kms minio-critical minio/critical-bucket
```
**왜 좋은가:**
- KES가 Vault transit을 key store로 사용 → master key는 Vault가 관리, MinIO는 DEK만 캐시
- KES policy로 `minio-*` prefix key만 access 허용 (최소 권한)
- AppRole credential은 Vault → VSO → Secret 경로
- bucket level SSE-KMS → 업로드되는 모든 object가 per-object DEK로 자동 암호화
---
## 나쁜 예시 2: KES 없이 평문 저장
```yaml
# Tenant CR
spec:
kes: {} # 미설정
# ... SSE 설정 없음
```
```bash
mc cp secret.pdf minio/bucket/secret.pdf
# object가 disk에 평문 저장
```
**문제:**
- PVC가 탈취되거나 물리 drive가 반출되면 평문 유출
- 감사/규제 요구(GDPR, PCI-DSS, ISO 27001) 위반
- SSE-S3를 대신 쓰더라도 master key가 MinIO 자체에 있어 키 라이프사이클 관리 불가
---
## 좋은 예시 4: Probe — live + cluster-read
Tenant CR이 자동으로 probe를 구성하지만, 커스텀 오버라이드가 필요할 때:
```yaml
spec:
pools:
- name: pool-0
# ...
containers:
- name: minio
livenessProbe:
httpGet:
path: /minio/health/live
port: 9000
scheme: HTTPS
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /minio/health/cluster/read
port: 9000
scheme: HTTPS
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
startupProbe:
httpGet:
path: /minio/health/live
port: 9000
scheme: HTTPS
periodSeconds: 5
failureThreshold: 60
timeoutSeconds: 5
```
**왜 좋은가:**
- `readinessProbe``/minio/health/cluster/read`**read quorum** 검사. rolling update 중에도 read가 가능하면 Service에 남아있음
- `/minio/health/cluster` (write quorum)을 readiness로 쓰면 rolling 재시작 시 pod가 전부 빠져 완전 unavailable
- `livenessProbe`는 단순 프로세스 생존만 확인 → 일시적 quorum 상실로 pod 강제 재시작 방지
- HTTPS scheme (requestAutoCert과 일치)
---
## 나쁜 예시 3: readiness를 write quorum으로
```yaml
readinessProbe:
httpGet:
path: /minio/health/cluster
port: 9000
periodSeconds: 5
failureThreshold: 1
```
**문제:**
- rolling update로 pod 1개를 재시작하면 write quorum이 일시적으로 무너져 살아있는 pod들도 NotReady
- Service가 endpoint를 전부 제거 → **읽기도 불가능**
- `failureThreshold: 1` + 5초 주기 → 한 번 느린 응답으로 pod 빠짐
---
## 좋은 예시 5: Ingress — API는 공개, Console은 내부
```yaml
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio-api
namespace: minio-prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/proxy-body-size: "0"
nginx.ingress.kubernetes.io/proxy-request-buffering: "off"
nginx.ingress.kubernetes.io/proxy-buffering: "off"
nginx.ingress.kubernetes.io/backend-protocol: HTTPS
spec:
ingressClassName: nginx
tls:
- hosts:
- s3.example.com
secretName: minio-api-ingress-tls
rules:
- host: s3.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio
port:
number: 443
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio-console
namespace: minio-prod
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
nginx.ingress.kubernetes.io/whitelist-source-range: "10.0.0.0/8,192.168.0.0/16"
nginx.ingress.kubernetes.io/auth-url: "https://oauth2-proxy.example.com/oauth2/auth"
nginx.ingress.kubernetes.io/backend-protocol: HTTPS
spec:
ingressClassName: nginx-internal
tls:
- hosts:
- minio-console.internal.example.com
secretName: minio-console-ingress-tls
rules:
- host: minio-console.internal.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio-console
port:
number: 9443
```
**왜 좋은가:**
- API Ingress는 `proxy-body-size: 0` + request/response buffering off → 대용량 multipart upload 지원
- Console은 내부 ingress class + IP whitelist + OIDC forward-auth 2중 보호
- `backend-protocol: HTTPS` → MinIO의 auto-cert TLS를 TLS passthrough 형태로 전달 (인증서 SAN 보존)
---
## 나쁜 예시 4: Console 외부 공개
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: minio-all
spec:
rules:
- host: minio.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: minio
port:
number: 9090
```
**문제:**
- Console이 인터넷에 그대로 노출 → root/admin credential brute force 표면 확장
- OIDC forward-auth 없음 → 기본 login 페이지가 공격자에게 노출
- IP 제한 없음
- Bucket 목록, access key 관리, 사용자 관리가 모두 공개 domain에 위치
---
## 좋은 예시 6: ServiceMonitor (Prometheus bearer-token)
먼저 MinIO 내부에서 scrape token 발급:
```bash
mc admin prometheus generate minio cluster
# 출력에 bearer token과 scrape config가 나옴
```
그 결과 token을 Secret로 저장:
```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: minio-prometheus-token
namespace: minio-prod
type: Opaque
stringData:
token: "eyJhbGciOiJIUzUxMiIsInR5cCI6IkpXVCJ9..."
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: minio
namespace: minio-prod
labels:
app.kubernetes.io/name: minio
release: kube-prometheus-stack
spec:
selector:
matchLabels:
v1.min.io/tenant: minio
endpoints:
- port: https-minio
scheme: https
path: /minio/v2/metrics/cluster
interval: 30s
scrapeTimeout: 10s
bearerTokenSecret:
name: minio-prometheus-token
key: token
tlsConfig:
insecureSkipVerify: false
ca:
secret:
name: minio-tls
key: ca.crt
serverName: minio.minio-prod.svc.cluster.local
```
**왜 좋은가:**
- `MINIO_PROMETHEUS_AUTH_TYPE=jwt` 와 매칭 (기본값)
- `/minio/v2/metrics/cluster`는 cluster-wide view (replication lag, bucket 사용량, API latency)
- TLS 검증 유지 (`insecureSkipVerify: false`, CA bundle 제공)
- 외부 노출 없이 내부 scrape만
---
## 좋은 예시 7: Bucket 초기화 (Job) — versioning + Object Lock + lifecycle
```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: minio-bootstrap
namespace: minio-prod
data:
init.sh: |
#!/bin/sh
set -eu
mc alias set minio https://minio.minio-prod.svc.cluster.local "$MINIO_ROOT_USER" "$MINIO_ROOT_PASSWORD" --api S3v4
# Object Lock은 bucket 생성 시점에만 활성화 가능
mc mb --with-lock minio/critical-audit || true
mc retention set --default COMPLIANCE 2555d minio/critical-audit # 7년 보관
# 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
# 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
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
metadata:
name: minio-bootstrap
namespace: minio-prod
spec:
backoffLimit: 3
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: OnFailure
serviceAccountName: minio-bootstrap
containers:
- name: mc
image: quay.io/minio/mc:RELEASE.2025-01-17T23-25-50Z
command: ["/bin/sh", "/scripts/init.sh"]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }
env:
- name: MINIO_ROOT_USER
valueFrom:
secretKeyRef:
name: minio-root-creds
key: username
- name: MINIO_ROOT_PASSWORD
valueFrom:
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
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
volumes:
- name: scripts
configMap:
name: minio-bootstrap
defaultMode: 0755
items:
- key: init.sh
path: init.sh
- name: policies
configMap:
name: minio-bootstrap
items:
- key: auth-server-rw.json
path: auth-server-rw.json
```
**왜 좋은가:**
- `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로 제한
- `backoffLimit: 3` + idempotent 명령 (`|| true`) → 재실행 안전
---
## 나쁜 예시 5: mc mirror만으로 DR
```bash
# 매일 자정 crontab
mc mirror minio/critical remote-minio/critical-backup
```
**문제:**
- `mc mirror`**현재 object만** 동기화 — 버전 히스토리 유실
- Object Lock 상태, bucket policy, IAM 설정 미복제
- 메타데이터 중 일부(tag, legal hold) 누락
- RPO = 1일 (하루 단위 손실), replication은 async ms 단위 RPO
- DR 연습(resync 절차) 불가
대안: `mc admin replicate add` (site replication, IAM + bucket + object 전부 async 동기화).
---
## 좋은 예시 8: Bucket replication
```bash
# source alias 설정
mc alias set source https://minio.minio-prod.svc.cluster.local $SRC_USER $SRC_PASS
mc alias set target https://minio.minio-dr.svc.cluster.local $TGT_USER $TGT_PASS
# target에 replication 전용 user + policy
mc admin policy create target replication-target /policies/replication.json
mc admin user add target replication-bot $(openssl rand -hex 16)
mc admin policy attach target replication-target --user replication-bot
# source에서 remote target 등록
mc replicate add source/app-data \
--remote-bucket https://replication-bot:PASS@minio.minio-dr.svc.cluster.local/app-data \
--replicate "delete,delete-marker,existing-objects,metadata-sync" \
--priority 1
```
`replication.json`:
```json
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Action": [
"s3:GetBucketVersioning",
"s3:PutBucketVersioning",
"s3:GetReplicationConfiguration",
"s3:ReplicateObject",
"s3:ReplicateDelete",
"s3:ReplicateTags",
"s3:GetObjectVersion",
"s3:GetObjectVersionTagging",
"s3:GetObjectVersionForReplication"
],
"Resource": ["arn:aws:s3:::app-data/*", "arn:aws:s3:::app-data"]
}
]
}
```
**왜 좋은가:**
- `existing-objects` 옵션으로 기존 데이터 backfill
- `delete` + `delete-marker`로 삭제도 복제 (true mirror)
- 전용 replication user + 최소 권한 policy
- async 복제, bucket versioning 전제
---
## 좋은 예시 9: Keycloak OIDC STS 로그인
Keycloak에 `minio` client 생성 후:
```bash
# 앱에서 JWT를 받은 다음
curl -X POST https://s3.example.com/ \
-d "Action=AssumeRoleWithWebIdentity" \
-d "Version=2011-06-15" \
-d "WebIdentityToken=${KEYCLOAK_ID_TOKEN}" \
-d "DurationSeconds=3600"
```
응답의 `AccessKeyId`, `SecretAccessKey`, `SessionToken`을 S3 SDK에 주입.
```bash
# AWS CLI 예시
aws configure set aws_access_key_id "$STS_ACCESS_KEY"
aws configure set aws_secret_access_key "$STS_SECRET_KEY"
aws configure set aws_session_token "$STS_SESSION_TOKEN"
aws s3 ls s3://app-data/ --endpoint-url https://s3.example.com
```
**왜 좋은가:**
- 앱/사용자는 Keycloak에 로그인만 하면 됨 — MinIO에 user 등록 불필요
- 임시 credential (1시간 TTL) → 유출 시 피해 제한
- JWT의 `policy` claim이 MinIO 정책과 자동 매핑
- 장기 access key 배포 없음
+578
View File
@@ -0,0 +1,578 @@
# network / ingress / TLS 예시
모든 YAML은 `kubectl apply --dry-run=server -f -` 기준 clean. Traefik은 `ingress-traefik` namespace에 IngressClass `traefik`으로 설치되어 있다고 가정한다. cert-manager는 `cert-manager` namespace에 설치되어 있다.
---
## 좋은 예시 1: cert-manager ClusterIssuer (staging + prod) + DNS-01 wildcard
```yaml
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: platform@example.com
privateKeySecretRef:
name: letsencrypt-staging-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: platform@example.com
privateKeySecretRef:
name: letsencrypt-prod-account-key
solvers:
- http01:
ingress:
ingressClassName: traefik
selector:
dnsZones:
- example.com
---
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-prod-dns
spec:
acme:
server: https://acme-v02.api.letsencrypt.org/directory
email: platform@example.com
privateKeySecretRef:
name: letsencrypt-prod-dns-account-key
solvers:
- dns01:
route53:
region: us-east-1
hostedZoneID: Z2FDTNDATAQYW2
selector:
dnsZones:
- example.com
```
**왜 좋은가:**
- Staging issuer로 먼저 발급 테스트(LE rate limit 절약). 검증 후 prod로 교체.
- HTTP-01 solver는 `ingressClassName: traefik`으로 challenge Ingress가 정확히 Traefik만 수락.
- DNS-01 solver는 wildcard(`*.example.com`) 발급에 필수. Route53 hosted zone ID 고정.
---
## 좋은 예시 2: Certificate CRD로 TLS Secret 자동 생성 + Ingress 재사용
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: auth-example-com
namespace: auth-prod
spec:
secretName: auth-example-com-tls
secretTemplate:
annotations:
reflector.v1.k8s.emberstack.com/reflection-allowed: "false"
labels:
app.kubernetes.io/part-of: identity-platform
duration: 2160h # 90d
renewBefore: 360h # 15d
privateKey:
algorithm: ECDSA
size: 256
rotationPolicy: Always
usages:
- server auth
- digital signature
- key encipherment
dnsNames:
- auth.example.com
issuerRef:
kind: ClusterIssuer
name: letsencrypt-prod
```
**왜 좋은가:**
- cert-manager가 `auth-example-com-tls`라는 `kubernetes.io/tls` Secret을 자동 생성·회전(15일 전).
- ECDSA P-256 + 회전 정책으로 key lifecycle 관리.
- `usages` 명시로 SAN certificate의 Extended Key Usage 제어.
---
## 좋은 예시 3: Traefik Middleware(HSTS + HTTPS redirect) + TLSOption
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: https-redirect
namespace: ingress-traefik
spec:
redirectScheme:
scheme: https
permanent: true
---
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: security-headers
namespace: ingress-traefik
spec:
headers:
stsSeconds: 31536000
stsIncludeSubdomains: true
stsPreload: true
forceSTSHeader: true
contentTypeNosniff: true
browserXssFilter: true
referrerPolicy: strict-origin-when-cross-origin
frameDeny: true
---
apiVersion: traefik.io/v1alpha1
kind: TLSOption
metadata:
name: modern-tls
namespace: ingress-traefik
spec:
minVersion: VersionTLS12
sniStrict: true
cipherSuites:
- TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256
- TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384
- TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305
- TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305
curvePreferences:
- CurveP521
- CurveP384
```
**왜 좋은가:**
- HSTS preload 조건(`max-age>=31536000` + `includeSubDomains` + `preload`)을 모두 만족.
- TLS 1.2+ 강제, 취약 cipher 제거. `sniStrict: true`로 SNI 없는 클라이언트 차단.
- 재사용 가능한 platform middleware — 각 namespace Ingress가 참조.
---
## 좋은 예시 4: auth-server Service + Ingress (TLS, HSTS, HTTPS redirect)
```yaml
apiVersion: v1
kind: Service
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
appProtocol: http
- name: metrics
port: 9090
targetPort: metrics
protocol: TCP
appProtocol: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/part-of: identity-platform
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.tls.options: ingress-traefik-modern-tls@kubernetescrd
traefik.ingress.kubernetes.io/router.middlewares: ingress-traefik-https-redirect@kubernetescrd,ingress-traefik-security-headers@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- auth.example.com
secretName: auth-example-com-tls
rules:
- host: auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: auth-server
port:
name: http
```
**왜 좋은가:**
- `ingressClassName: traefik` 필드 사용, deprecated annotation 미사용.
- `cert-manager.io/cluster-issuer` annotation으로 TLS Secret(`auth-example-com-tls`)이 자동 발급.
- Traefik middleware 체인으로 HSTS + HTTPS redirect + TLSOption 적용.
- Service는 named port `http`, `metrics` 분리. Ingress는 `http`만 라우팅, metrics는 NetworkPolicy로 Prometheus만 허용.
---
## 나쁜 예시 1: deprecated ingress.class annotation + TLS 누락
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: auth-server
namespace: auth-prod
annotations:
kubernetes.io/ingress.class: traefik
spec:
rules:
- host: auth.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: auth-server
port:
number: 80
```
**문제:**
- `kubernetes.io/ingress.class` annotation은 1.22부터 deprecated. 일부 컨트롤러는 무시한다.
- `spec.tls` 없음 → 평문 HTTP로 노출. 인증 시스템에는 특히 부적절.
- HSTS/HTTPS redirect 미적용.
- Service 포트를 `number: 80`으로 hard-code. named port drift에 취약.
---
## 좋은 예시 5: Keycloak은 `/realms/`, `/resources/`, `/.well-known/`만 공개
```yaml
apiVersion: cert-manager.io/v1
kind: Certificate
metadata:
name: sso-example-com
namespace: keycloak
spec:
secretName: sso-example-com-tls
duration: 2160h
renewBefore: 360h
privateKey:
algorithm: ECDSA
size: 256
dnsNames:
- sso.example.com
issuerRef:
kind: ClusterIssuer
name: letsencrypt-prod
---
apiVersion: v1
kind: Service
metadata:
name: keycloak
namespace: keycloak
labels:
app.kubernetes.io/name: keycloak
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: keycloak
ports:
- name: http
port: 8080
targetPort: http
protocol: TCP
appProtocol: http
- name: management
port: 9000
targetPort: management
protocol: TCP
appProtocol: http
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: keycloak
namespace: keycloak
annotations:
cert-manager.io/cluster-issuer: letsencrypt-prod
traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.tls.options: ingress-traefik-modern-tls@kubernetescrd
traefik.ingress.kubernetes.io/router.middlewares: ingress-traefik-https-redirect@kubernetescrd,ingress-traefik-security-headers@kubernetescrd
spec:
ingressClassName: traefik
tls:
- hosts:
- sso.example.com
secretName: sso-example-com-tls
rules:
- host: sso.example.com
http:
paths:
- path: /realms/
pathType: Prefix
backend:
service:
name: keycloak
port:
name: http
- path: /resources/
pathType: Prefix
backend:
service:
name: keycloak
port:
name: http
- path: /.well-known/
pathType: Prefix
backend:
service:
name: keycloak
port:
name: http
```
**왜 좋은가:**
- Keycloak 공식 권장 공개 경로만 노출.
- `/admin/`, `/metrics`, `/health`는 Ingress에 없음 → 외부에서 접근 불가.
- `management`(9000) 포트는 Service에만 존재하고 Ingress에는 없음.
---
## 나쁜 예시 2: host 없는 defaultBackend + admin 노출
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: catch-all
namespace: keycloak
spec:
ingressClassName: traefik
defaultBackend:
service:
name: keycloak
port:
number: 8080
rules:
- http:
paths:
- path: /admin
pathType: Prefix
backend:
service:
name: keycloak
port:
number: 9000
```
**문제:**
- `defaultBackend`가 모든 host의 unmatched 요청을 Keycloak으로 포워딩 → 다른 앱 공격면 확대.
- `/admin`을 관리 포트 9000으로 프록시 → Keycloak 공식 권고 위반, 관리 콘솔 외부 노출.
- TLS/HSTS/redirect 미적용.
---
## 좋은 예시 6: Traefik IngressRoute + Middleware(TLS 1.2, rate-limit, BasicAuth)
```yaml
apiVersion: traefik.io/v1alpha1
kind: Middleware
metadata:
name: rate-limit
namespace: auth-prod
spec:
rateLimit:
average: 100
burst: 200
period: 1s
---
apiVersion: traefik.io/v1alpha1
kind: IngressRoute
metadata:
name: auth-server
namespace: auth-prod
spec:
entryPoints:
- websecure
routes:
- kind: Rule
match: Host(`auth.example.com`) && PathPrefix(`/api/v1`)
services:
- kind: Service
name: auth-server
port: http
scheme: http
passHostHeader: true
middlewares:
- name: https-redirect
namespace: ingress-traefik
- name: security-headers
namespace: ingress-traefik
- name: rate-limit
namespace: auth-prod
tls:
secretName: auth-example-com-tls
options:
name: modern-tls
namespace: ingress-traefik
```
**왜 좋은가:**
- Traefik CRD 네이티브. match 표현이 강력(Host + PathPrefix 조합, Header match 가능).
- Middleware 체인(HSTS + redirect + rate-limit)을 순서대로 지정.
- `TLSOption`을 Route마다 override 가능(특정 host만 mTLS 요구 등).
---
## 좋은 예시 7: Vault/DB는 Ingress 없이 ClusterIP
```yaml
apiVersion: v1
kind: Service
metadata:
name: vault
namespace: vault
labels:
app.kubernetes.io/name: vault
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: vault
ports:
- name: https
port: 8200
targetPort: https
protocol: TCP
appProtocol: https
- name: cluster
port: 8201
targetPort: cluster
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: identity-postgres
namespace: data-prod
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: identity-postgres
spec:
type: ClusterIP
clusterIP: None
selector:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: identity-postgres
ports:
- name: postgres
port: 5432
targetPort: postgres
protocol: TCP
appProtocol: postgresql
```
**왜 좋은가:**
- Vault/Postgres 둘 다 Ingress 없음 → 외부 L7 공격면 0.
- Postgres는 headless(`clusterIP: None`) → StatefulSet Pod에 직접 DNS.
- named port `https`/`postgres` 사용.
---
## 나쁜 예시 3: 운영 DB를 NodePort로 공개
```yaml
apiVersion: v1
kind: Service
metadata:
name: identity-postgres
namespace: data-prod
spec:
type: NodePort
ports:
- port: 5432
targetPort: 5432
nodePort: 30032
```
**문제:**
- 모든 노드의 30032 포트가 외부에서 접근 가능 → DB가 인터넷에 노출될 수 있음.
- TLS/mTLS/NetworkPolicy 어디서도 통제 불가.
- `services.nodeports: 0` quota를 걸어 namespace 단에서 차단해야 한다.
---
## 좋은 예시 8: K3s Traefik HelmChartConfig override
```yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
deployment:
replicas: 3
service:
spec:
externalTrafficPolicy: Local
ports:
web:
redirectTo:
port: websecure
priority: 10
websecure:
tls:
enabled: true
ingressClass:
enabled: true
isDefaultClass: true
additionalArguments:
- "--providers.kubernetesingress.ingressclass=traefik"
- "--metrics.prometheus=true"
- "--entrypoints.websecure.http.tls.options=modern-tls@kubernetescrd"
resources:
requests:
cpu: 200m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
```
**왜 좋은가:**
- K3s packaged Traefik manifest는 건드리지 않고, override만 선언적으로 관리.
- replica 3, `externalTrafficPolicy: Local`로 source IP 보존.
- 기본 websecure에 `modern-tls` TLSOption을 묶어 platform 전역 TLS 정책 통일.
+601
View File
@@ -0,0 +1,601 @@
# observability / health 예시
---
## 좋은 예시 1: ServiceMonitor (kube-prometheus-stack 표준)
```yaml
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
app.kubernetes.io/part-of: identity-platform
release: kube-prometheus-stack
spec:
namespaceSelector:
matchNames:
- auth-prod
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
endpoints:
- port: metrics # named port (required)
path: /actuator/prometheus
scheme: http
interval: 30s
scrapeTimeout: 10s
honorLabels: false
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
- sourceLabels: [__meta_kubernetes_namespace]
targetLabel: namespace
- sourceLabels: [__meta_kubernetes_pod_label_app_kubernetes_io_version]
targetLabel: version
- action: labeldrop
regex: "pod_template_hash|controller_revision_hash"
metricRelabelings:
- sourceLabels: [__name__]
regex: "jvm_gc_pause_seconds_.*"
action: keep
- sourceLabels: [__name__]
regex: "debug_.*"
action: drop
```
**왜 좋은가:**
- `namespaceSelector` 명시로 암묵적 전체 허용 방지.
- `port: metrics` 는 Service/Deployment의 named port를 참조 → 포트 번호 변경에 내성.
- `interval / scrapeTimeout` 관계 유지 (timeout < interval).
- `relabelings` 로 pod / namespace / version label 정리, noise label drop.
- `metricRelabelings` 로 불필요 metric drop (cardinality / storage 절감).
- `release: kube-prometheus-stack` label 로 Operator가 선택.
---
## 좋은 예시 2: ServiceMonitor with bearer token (Vault telemetry)
```yaml
---
apiVersion: v1
kind: Secret
metadata:
name: vault-metrics-token
namespace: vault
type: Opaque
stringData:
token: "hvs.xxxx.prometheus-readonly"
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: vault
namespace: vault
labels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault-prod
release: kube-prometheus-stack
spec:
namespaceSelector:
matchNames:
- vault
selector:
matchLabels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault-prod
endpoints:
- port: https
path: /v1/sys/metrics
params:
format: ["prometheus"]
scheme: https
interval: 30s
scrapeTimeout: 10s
bearerTokenSecret:
name: vault-metrics-token
key: token
tlsConfig:
insecureSkipVerify: false
ca:
secret:
name: vault-ca
key: ca.crt
serverName: vault.vault.svc
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
```
**왜 좋은가:**
- Vault `/sys/metrics` 는 read token 필수. `bearerTokenSecret` 참조로 Operator가 주입.
- TLS CA pinning + serverName 으로 MitM 방지.
- `params` 로 Prometheus format 요청.
---
## 좋은 예시 3: PodMonitor (Service 없는 워크로드)
```yaml
---
apiVersion: monitoring.coreos.com/v1
kind: PodMonitor
metadata:
name: batch-worker
namespace: batch
labels:
release: kube-prometheus-stack
spec:
namespaceSelector:
matchNames:
- batch
selector:
matchLabels:
app.kubernetes.io/name: batch-worker
podMetricsEndpoints:
- port: metrics
path: /metrics
interval: 30s
scrapeTimeout: 10s
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
```
**왜 좋은가:**
- Job / headless workload처럼 Service 뒤에 없는 경우 PodMonitor로 직접 pod 매칭.
---
## 좋은 예시 4: Annotation-based fallback (Operator 없는 환경 only)
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: legacy-app
namespace: legacy
annotations:
prometheus.io/scrape: "true"
prometheus.io/port: "8081"
prometheus.io/path: "/metrics"
prometheus.io/scheme: "http"
spec:
selector:
app.kubernetes.io/name: legacy-app
ports:
- name: http
port: 80
targetPort: 8080
- name: metrics
port: 8081
targetPort: 8081
```
**왜 좋은가 (조건부):**
- kube-prometheus-stack이 없는 legacy 환경에서만 유효.
- Operator가 있으면 ServiceMonitor로 전환.
---
## 좋은 예시 5: NetworkPolicy — prometheus namespace만 metrics scrape 허용
```yaml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: auth-server-default-deny
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes: ["Ingress", "Egress"]
ingress: []
egress: []
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: auth-server-allow-metrics
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- port: metrics
protocol: TCP
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: auth-server-allow-http-from-ingress
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: http
protocol: TCP
```
**왜 좋은가:**
- default-deny → allow-list 패턴.
- metrics port는 monitoring namespace의 prometheus pod만.
- http port는 ingress controller namespace만.
---
## 좋은 예시 6: JSON structured log (Spring Boot logback)
```xml
<!-- logback-spring.xml -->
<configuration>
<appender name="JSON" class="ch.qos.logback.core.ConsoleAppender">
<encoder class="net.logstash.logback.encoder.LogstashEncoder">
<includeMdcKeyName>trace_id</includeMdcKeyName>
<includeMdcKeyName>span_id</includeMdcKeyName>
<includeMdcKeyName>request_id</includeMdcKeyName>
<customFields>{"service":"auth-server"}</customFields>
</encoder>
</appender>
<root level="INFO">
<appender-ref ref="JSON"/>
</root>
</configuration>
```
Actual output:
```json
{"timestamp":"2026-04-16T09:31:42.017Z","level":"INFO","service":"auth-server","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","logger":"c.e.auth.LoginController","thread":"http-nio-8080-exec-3","message":"login success","user_id_hash":"ab12..."}
```
**왜 좋은가:**
- ISO 8601 UTC timestamp.
- trace_id / span_id 가 MDC에서 자동 주입 → Tempo / Jaeger와 correlate.
- service label이 customFields로 고정.
- user_id는 hashed → cardinality/PII 안전.
---
## 좋은 예시 7: OpenTelemetry Collector (DaemonSet agent + Deployment gateway)
```yaml
---
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-agent
namespace: observability
spec:
mode: daemonset
image: otel/opentelemetry-collector-contrib:0.101.0
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
send_batch_size: 1024
timeout: 5s
k8sattributes:
passthrough: false
extract:
metadata:
- k8s.pod.name
- k8s.namespace.name
- k8s.node.name
exporters:
otlp/gateway:
endpoint: otel-gateway.observability.svc:4317
tls:
insecure: true
service:
pipelines:
traces:
receivers: [otlp]
processors: [k8sattributes, batch]
exporters: [otlp/gateway]
metrics:
receivers: [otlp]
processors: [k8sattributes, batch]
exporters: [otlp/gateway]
---
apiVersion: opentelemetry.io/v1beta1
kind: OpenTelemetryCollector
metadata:
name: otel-gateway
namespace: observability
spec:
mode: deployment
replicas: 3
image: otel/opentelemetry-collector-contrib:0.101.0
config:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
processors:
batch:
send_batch_size: 2048
timeout: 5s
tail_sampling:
decision_wait: 10s
policies:
- name: errors-keep
type: status_code
status_code: { status_codes: [ERROR] }
- name: slow-keep
type: latency
latency: { threshold_ms: 500 }
- name: default-10pct
type: probabilistic
probabilistic: { sampling_percentage: 10 }
attributes/redact:
actions:
- key: http.request.header.authorization
action: delete
- key: user.email
action: hash
exporters:
otlp/tempo:
endpoint: tempo.observability.svc:4317
tls:
insecure: true
prometheusremotewrite:
endpoint: http://prometheus.monitoring.svc:9090/api/v1/write
service:
pipelines:
traces:
receivers: [otlp]
processors: [attributes/redact, tail_sampling, batch]
exporters: [otlp/tempo]
metrics:
receivers: [otlp]
processors: [batch]
exporters: [prometheusremotewrite]
```
**왜 좋은가:**
- agent (DaemonSet) → gateway (Deployment) 2단 구조.
- gateway에서 tail-based sampling (error + slow + 10% 나머지).
- PII redaction을 gateway에서 중앙 처리.
- agent가 node-local이라 app은 localhost endpoint만 알면 됨.
---
## 좋은 예시 8: Loki + Grafana Alloy DaemonSet (log shipping)
```yaml
---
apiVersion: v1
kind: ConfigMap
metadata:
name: alloy-config
namespace: observability
data:
config.alloy: |
discovery.kubernetes "pods" {
role = "pod"
}
discovery.relabel "pods" {
targets = discovery.kubernetes.pods.targets
rule {
source_labels = ["__meta_kubernetes_namespace"]
target_label = "namespace"
}
rule {
source_labels = ["__meta_kubernetes_pod_label_app_kubernetes_io_name"]
target_label = "service"
}
}
loki.source.kubernetes "pods" {
targets = discovery.relabel.pods.output
forward_to = [loki.write.default.receiver]
}
loki.write "default" {
endpoint {
url = "http://loki.observability.svc:3100/loki/api/v1/push"
}
}
```
**왜 좋은가:**
- Alloy DaemonSet이 node-level log tail.
- label은 namespace / service 두 개로 제한 (cardinality 안전).
---
## 좋은 예시 9: `kubectl events` (1.27+ stable)
```bash
# cluster-wide live watch, warnings only
kubectl events -A --types=Warning --watch
# specific pod
kubectl events -n auth-prod --for pod/auth-server-abc123
# last hour
kubectl events -n auth-prod --since=1h
```
**왜 좋은가:**
- `--for` 로 특정 오브젝트 event 만 필터링.
- `--watch``get events -w` 보다 안정적.
- timestamp sort 기본 제공.
---
## 나쁜 예시 1: `/metrics` 를 Ingress로 외부 공개
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
spec:
rules:
- host: auth.example.com
http:
paths:
- path: /metrics # BAD
pathType: Prefix
backend:
service:
name: auth-server
port:
number: 8081
```
**문제:**
- Prometheus metric으로 내부 구조 / error rate / version 노출.
- DoS vector (scrape 비용).
- audit / compliance 위반.
**Fix:** metrics port는 외부 비공개, NetworkPolicy로 monitoring namespace만 허용.
---
## 나쁜 예시 2: high-cardinality label
```yaml
# app code
http_requests_total{user_id="12345", path="/users/12345/orders/98765", request_id="a1b2c3..."}
```
**문제:**
- user_id × path × request_id = 수백만 time series → Prometheus OOM.
- query 성능 붕괴.
**Fix:**
```
http_requests_total{route="/users/:id/orders/:id", method="GET", status_class="2xx"}
```
- route template 화, status는 bucket (2xx/4xx/5xx).
- user_id 는 logging에만, metric label 금지.
---
## 나쁜 예시 3: ServiceMonitor에 namespaceSelector 없음
```yaml
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
spec:
selector:
matchLabels:
app: my-app
# namespaceSelector 없음 → Operator 설정에 따라 전체 cluster scan
endpoints:
- port: metrics
```
**문제:**
- 암묵적으로 너무 넓은 범위 (Operator 설정에 따라 다름).
- 동일 label 를 다른 namespace에서 쓰면 의도치 않은 scrape.
**Fix:** `namespaceSelector.matchNames` 명시.
---
## 나쁜 예시 4: probe가 `/metrics` 사용
```yaml
readinessProbe:
httpGet:
path: /metrics # BAD
port: 8081
periodSeconds: 5
```
**문제:**
- `/metrics` 는 비용이 큰 endpoint (모든 registry dump).
- periodSeconds 5초 × N pod = unnecessary load.
- readiness 의미와 무관.
**Fix:** `/actuator/health/readiness` 같은 전용 shallow endpoint.
---
## 나쁜 예시 5: 로그에 access token 그대로
```
2026-04-16T09:32:11.002 INFO Exchanging code for token: access_token=eyJhbGciOi...
```
**문제:**
- token이 log index에 그대로 저장 → 유출 리스크.
- 중앙 로그 시스템 (OpenSearch / Loki) 에 영구 보관.
**Fix:**
- 애플리케이션에서 token 값 로깅 금지.
- 중앙 파이프라인에 regex redaction (`access_token=[^ ]+``access_token=***`).
- debug 로그에서도 masking.
---
## 나쁜 예시 6: 로그를 PVC / file로 적재
```yaml
volumeMounts:
- name: app-logs
mountPath: /var/log/app # BAD
volumes:
- name: app-logs
persistentVolumeClaim:
claimName: app-logs-pvc
```
**문제:**
- 컨테이너 표준 (stdout/stderr) 위반.
- Pod 삭제 시 로그 손실 또는 orphan PVC.
- `kubectl logs` 로 안 보임.
- node log agent가 수집 못 함.
**Fix:** stdout/stderr로 출력, DaemonSet agent가 수집.
@@ -0,0 +1,744 @@
# operations / runbook / upgrade / rollback 예시
---
## 좋은 예시 1: 표준 application 변경 절차
```bash
# 1) render
kubectl kustomize k8s/overlays/prod > /tmp/render.yaml
# 2) diff
kubectl diff -k k8s/overlays/prod
# 3) apply
kubectl apply -k k8s/overlays/prod
# 4) rollout status with timeout
kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m
# 5) smoke test
curl -fsS https://auth.internal.example.com/actuator/health/readiness
# 6) SLO dashboard check (p99 latency, error rate)
```
**왜 좋은가:**
- render / diff / apply / status / post-check 가 명시적으로 분리.
- `--timeout` 으로 무한 대기 방지.
- post-check가 단순 curl 이 아니라 readiness endpoint 대상.
---
## 좋은 예시 2: Deployment rollingUpdate 파라미터 워크로드별 튜닝
```yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
replicas: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 10% # latency-sensitive면 0
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.25.0
ports:
- { name: http, containerPort: 8080 }
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { memory: 1536Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-singleton
namespace: legacy
spec:
replicas: 1
strategy:
type: Recreate # singleton이며 동시성 금지
selector:
matchLabels:
app.kubernetes.io/name: legacy-singleton
template:
metadata:
labels:
app.kubernetes.io/name: legacy-singleton
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/legacy/singleton:1.0.0
resources:
requests: { cpu: 100m, memory: 256Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: { drop: ["ALL"] }
```
**왜 좋은가:**
- fleet 규모에 맞춘 maxSurge/maxUnavailable.
- singleton 에 Recreate (PVC ReadWriteOnce 전제 충족).
---
## 좋은 예시 3: Argo Rollouts canary with AnalysisTemplate
```yaml
---
apiVersion: argoproj.io/v1alpha1
kind: AnalysisTemplate
metadata:
name: success-rate
namespace: auth-prod
spec:
args:
- name: service-name
metrics:
- name: success-rate
interval: 1m
count: 5
successCondition: result[0] >= 0.99
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring.svc:9090
query: |
sum(rate(http_requests_total{service="{{args.service-name}}",status_class=~"2.."}[2m]))
/
sum(rate(http_requests_total{service="{{args.service-name}}"}[2m]))
- name: p99-latency
interval: 1m
count: 5
successCondition: result[0] <= 0.5
failureLimit: 2
provider:
prometheus:
address: http://prometheus.monitoring.svc:9090
query: |
histogram_quantile(0.99,
sum by (le) (rate(http_request_duration_seconds_bucket{service="{{args.service-name}}"}[2m]))
)
---
apiVersion: argoproj.io/v1alpha1
kind: Rollout
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
replicas: 10
revisionHistoryLimit: 5
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.25.0
ports:
- name: http
containerPort: 8080
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
memory: "1536Mi"
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
strategy:
canary:
canaryService: auth-server-canary
stableService: auth-server-stable
trafficRouting:
nginx:
stableIngress: auth-server
steps:
- setWeight: 10
- pause: { duration: 2m }
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: auth-server
- setWeight: 25
- pause: { duration: 5m }
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: auth-server
- setWeight: 50
- pause: { duration: 10m }
- analysis:
templates:
- templateName: success-rate
args:
- name: service-name
value: auth-server
- setWeight: 100
---
apiVersion: v1
kind: Service
metadata:
name: auth-server-stable
namespace: auth-prod
spec:
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: v1
kind: Service
metadata:
name: auth-server-canary
namespace: auth-prod
spec:
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
ports:
- name: http
port: 80
targetPort: http
```
**왜 좋은가:**
- `AnalysisTemplate` 이 Prometheus success-rate + p99 latency 를 동시에 측정.
- `failureLimit: 2` → 두 번 실패 시 자동 abort.
- canary step: 10% → 25% → 50% → 100% 각 단계에 pause + analysis.
- stable/canary Service 두 개 + NGINX ingress traffic routing.
---
## 좋은 예시 4: K3s System Upgrade Controller Plan (server + agent)
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: system-upgrade
---
apiVersion: v1
kind: Secret
metadata:
name: k3s-upgrade-token
namespace: system-upgrade
type: Opaque
stringData:
# 실제 환경은 K3S_TOKEN 값
token: "REPLACE_WITH_NODE_TOKEN"
---
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: k3s-server
namespace: system-upgrade
labels:
k3s-upgrade: server
spec:
concurrency: 1
nodeSelector:
matchExpressions:
- { key: node-role.kubernetes.io/control-plane, operator: In, values: ["true"] }
serviceAccountName: system-upgrade
cordon: true
drain:
force: true
deleteEmptydirData: true
ignoreDaemonSets: true
skipWaitForDeleteTimeout: 60
upgrade:
image: rancher/k3s-upgrade
version: v1.30.3+k3s1
---
apiVersion: upgrade.cattle.io/v1
kind: Plan
metadata:
name: k3s-agent
namespace: system-upgrade
labels:
k3s-upgrade: agent
spec:
concurrency: 1
nodeSelector:
matchExpressions:
- { key: node-role.kubernetes.io/control-plane, operator: NotIn, values: ["true"] }
serviceAccountName: system-upgrade
prepare:
image: rancher/k3s-upgrade
args: ["prepare", "k3s-server"] # server plan 완료 대기
cordon: true
drain:
force: true
deleteEmptydirData: true
ignoreDaemonSets: true
skipWaitForDeleteTimeout: 60
upgrade:
image: rancher/k3s-upgrade
version: v1.30.3+k3s1
```
**왜 좋은가:**
- server-plan → agent-plan 분리 + agent 가 `prepare` 로 server 완료 대기.
- `concurrency: 1` → 한 번에 한 노드만 업그레이드 (가용성 보호).
- `cordon + drain` → PDB 존중.
- `deleteEmptydirData: true, ignoreDaemonsets: true` 표준.
- `version` 명시 (channel 사용 시 의도치 않은 upgrade 가능).
---
## 좋은 예시 5: ArgoCD sync wave + PreSync migration hook
```yaml
---
apiVersion: batch/v1
kind: Job
metadata:
name: flyway-migrate
namespace: auth-prod
annotations:
argocd.argoproj.io/hook: PreSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
argocd.argoproj.io/sync-wave: "-1"
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
app.kubernetes.io/component: db-migration
spec:
backoffLimit: 0
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 86400
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: flyway
image: flyway/flyway:10.15.0
args: ["-url=jdbc:postgresql://postgres:5432/auth", "validate", "info", "migrate"]
envFrom:
- secretRef:
name: auth-db
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 512Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- name: tmp
emptyDir: {}
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
annotations:
argocd.argoproj.io/sync-wave: "0"
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
# ... (생략)
---
apiVersion: batch/v1
kind: Job
metadata:
name: smoke-test
namespace: auth-prod
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: HookSucceeded
argocd.argoproj.io/sync-wave: "1"
spec:
backoffLimit: 2
activeDeadlineSeconds: 300
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: OnFailure
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: smoke
image: registry.example.com/tools/smoke:1.4.0
args: ["--target", "https://auth.internal.example.com"]
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 128Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
**왜 좋은가:**
- PreSync Job 으로 Flyway migrate 가 app rollout 앞 단계에 실행.
- PostSync Job 으로 smoke test 자동 실행.
- sync-wave 로 순서 명시 (-1 → 0 → 1).
- `BeforeHookCreation` 으로 이전 Job 충돌 방지.
---
## 좋은 예시 6: blue/green via two Services (수동 패턴)
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: auth-server # live traffic
namespace: auth-prod
spec:
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
version: blue # <- 이 label만 바꾸면 cutover
ports:
- name: http
port: 80
targetPort: http
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server-blue
namespace: auth-prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: auth-server
version: blue
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
version: blue
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.24.0
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { memory: 1536Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server-green
namespace: auth-prod
spec:
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: auth-server
version: green
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
version: green
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.25.0
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { memory: 1536Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
Cutover:
```bash
kubectl patch svc auth-server -n auth-prod \
-p '{"spec":{"selector":{"app.kubernetes.io/name":"auth-server","app.kubernetes.io/instance":"auth-server-prod","version":"green"}}}'
```
**왜 좋은가:**
- Service selector version label 하나로 전환 / rollback.
- canary 가 아니라 instant cutover.
- 데이터 호환성이 깨진 경우만 사용.
---
## 좋은 예시 7: node maintenance flow
```bash
NODE=worker-3
# 1) cordon
kubectl cordon "${NODE}"
# 2) drain (PDB 존중)
kubectl drain "${NODE}" \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=30 \
--timeout=10m
# 3) 작업 수행 (OS patch, reboot, ...)
# 4) 복귀
kubectl uncordon "${NODE}"
# 5) 재배치 확인
kubectl get pods -A -o wide --field-selector spec.nodeName="${NODE}"
```
**왜 좋은가:**
- cordon → drain → uncordon 표준 시퀀스.
- PDB 위반 시 drain 이 대기, `--timeout=10m` 로 무한 대기 방지.
- 플래그 조합이 표준.
---
## 좋은 예시 8: Git revision rollback
```bash
# 1) 이전 release tag 체크아웃
git checkout v1.24.0
# 2) diff
kubectl diff -k k8s/overlays/prod
# 3) apply
kubectl apply -k k8s/overlays/prod
# 4) rollout status
kubectl rollout status deployment/auth-server -n auth-prod --timeout=10m
```
**왜 좋은가:**
- live-cluster 수정이 아니라 declarative source of truth 기준.
- 재현 가능.
- `kubectl rollout undo` 대비 audit trail 이 명확 (Git commit 기반).
---
## 나쁜 예시 1: diff 없이 apply
```bash
kubectl apply -k k8s/overlays/prod
```
**문제:**
- 실제 변경 범위를 모른 채 적용.
- review / 승인 / 검증 프로세스 약화.
- 의도치 않은 리소스 삭제/수정 가능 (특히 pruned resource).
**Fix:** `kubectl diff -k` 선행.
---
## 나쁜 예시 2: migration을 app startup에 숨김
```yaml
# Deployment container
command: ["/bin/sh", "-c", "flyway migrate && java -jar app.jar"]
```
**문제:**
- app rollout 실패와 schema 변경 실패가 섞임.
- rollout 중 여러 replica 가 동시에 migrate → race condition / lock contention.
- 롤백 시 schema 변경이 남음.
**Fix:** PreSync Job 또는 별도 CI 단계로 Flyway migrate 를 분리.
---
## 나쁜 예시 3: rollout undo 로 DB rollback 기대
```bash
kubectl rollout undo deployment/auth-server
# ... 이제 DB schema 도 되돌아갔을 것이다?
```
**문제:**
- rollout undo 는 workload pod template 만 되돌린다.
- schema 변경은 남아 있음 → 이전 버전 app이 새 schema 와 mismatch → 500 error.
- **rollback ≠ DB rollback**.
**Fix:** schema 는 expand/contract 패턴으로 forward-compatible. 이전 버전 코드가 새 schema 에서도 동작하도록 릴리스를 분리.
---
## 나쁜 예시 4: 운영 노드 manifests 디렉터리 직접 편집
```bash
ssh k3s-server-1
vim /var/lib/rancher/k3s/server/manifests/auth-server.yaml
```
**문제:**
- Git source of truth 우회.
- 멀티 서버 간 동기화 없음.
- packaged AddOn 동작과 충돌 가능.
- ArgoCD 가 drift 로 인식하고 되돌릴 수 있음.
**Fix:** Git PR → render → diff → apply 흐름.
---
## 나쁜 예시 5: Recreate strategy 를 stateless app에 사용
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
spec:
replicas: 5
strategy:
type: Recreate # BAD - stateless 인데 downtime 발생
```
**문제:**
- 모든 replica 동시 종료 → full downtime.
- rolling update 의 장점 (점진 전환, rollback 용이) 상실.
**Fix:** stateless app은 `RollingUpdate` + 워크로드별 maxSurge/maxUnavailable 튜닝.
---
## 나쁜 예시 6: PDB 없이 drain
```bash
kubectl drain worker-3 --ignore-daemonsets --delete-emptydir-data
```
**문제:**
- PDB 가 없으면 critical workload 가 동시에 evict → downtime.
- 특히 replica < 3 이면 완전 손실.
**Fix:** PDB 설계 선결 조건. 좋은 예시 7 참조.
---
## 나쁜 예시 7: `kubectl rollout status` 에 timeout 없음
```bash
kubectl rollout status deployment/auth-server -n auth-prod
# 무한 대기 가능
```
**문제:**
- rollout 이 hang 상태일 때 CI/CD pipeline 이 무한 대기.
- 자동화 실패 원인이 숨는다.
**Fix:** 항상 `--timeout=10m` (워크로드별 조정).
@@ -0,0 +1,659 @@
# resources / probes / availability 예시
아래 예시는 1000+ 서비스를 운영하는 기준선이다. 모든 YAML은 그대로 `kubectl apply -f` 가능한 형태이며, 라벨 / probe / PDB / HPA / topologySpread / ServiceMonitor / NetworkPolicy가 한 세트로 맞물린다.
---
## 좋은 예시 1: auth-server 완전 매니페스트 세트 (Burstable + HPA)
### Deployment
```yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
app.kubernetes.io/version: "1.24.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
replicas: 3
revisionHistoryLimit: 10
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
app.kubernetes.io/version: "1.24.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
spec:
serviceAccountName: auth-server
terminationGracePeriodSeconds: 45
securityContext:
runAsNonRoot: true
runAsUser: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
containers:
- name: auth-server
image: registry.example.com/identity/auth-server:1.24.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
- name: metrics
containerPort: 8081
env:
- name: JAVA_OPTS
value: "-XX:MaxRAMPercentage=75.0 -XX:+UseG1GC"
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
memory: "1536Mi"
startupProbe:
httpGet:
path: /actuator/health/started
port: http
periodSeconds: 5
failureThreshold: 24
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
periodSeconds: 15
failureThreshold: 3
timeoutSeconds: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
```
**왜 좋은가:**
- QoS는 의도적으로 Burstable (CPU limit 생략으로 throttling 회피, memory는 1.5× headroom).
- startup probe가 최대 120초 (24×5) cold start를 덮으며 그 전까지 readiness/liveness는 실행되지 않는다.
- topologySpreadConstraints로 zone 장애 격리 + host 분산.
- app.kubernetes.io/* 표준 라벨 full set.
- preStop sleep 15초로 endpoint 제거 전파 시간을 확보한다.
- rolling update `maxUnavailable: 0`으로 항상 N replica 이상 유지.
### Service + PDB
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
ports:
- name: http
port: 80
targetPort: http
- name: metrics
port: 8081
targetPort: metrics
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
```
### HorizontalPodAutoscaler v2 with behavior block
```yaml
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: auth-server
minReplicas: 3
maxReplicas: 20
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 70
- type: Pods
pods:
metric:
name: http_requests_in_flight
target:
type: AverageValue
averageValue: "50"
behavior:
scaleUp:
stabilizationWindowSeconds: 0
policies:
- type: Percent
value: 100
periodSeconds: 30
- type: Pods
value: 4
periodSeconds: 30
selectPolicy: Max
scaleDown:
stabilizationWindowSeconds: 300
policies:
- type: Percent
value: 25
periodSeconds: 60
selectPolicy: Max
```
**왜 좋은가:**
- Resource metric과 custom Pods metric을 동시에 평가.
- scaleUp stabilization 0s → 스파이크에 즉시 반응.
- scaleDown 300s stabilization + 25%/min rate → flapping 방지.
- Pods metric은 pod당 in-flight request 수 (label cardinality 안전).
### ServiceMonitor (kube-prometheus-stack)
```yaml
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
release: kube-prometheus-stack
spec:
namespaceSelector:
matchNames:
- auth-prod
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
endpoints:
- port: metrics
path: /actuator/prometheus
scheme: http
interval: 30s
scrapeTimeout: 10s
honorLabels: false
relabelings:
- sourceLabels: [__meta_kubernetes_pod_name]
targetLabel: pod
- sourceLabels: [__meta_kubernetes_namespace]
targetLabel: namespace
- action: labeldrop
regex: "pod_template_hash"
```
### NetworkPolicy (scrape만 prometheus namespace에서 허용)
```yaml
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: auth-server-metrics-from-prom
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
policyTypes: ["Ingress"]
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- port: metrics
protocol: TCP
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: http
protocol: TCP
```
---
## 좋은 예시 2: Keycloak — Guaranteed QoS + slow startup
```yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: keycloak
namespace: identity
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
app.kubernetes.io/version: "24.0.4"
app.kubernetes.io/component: identity-provider
app.kubernetes.io/part-of: identity-platform
spec:
serviceName: keycloak-headless
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
template:
metadata:
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
app.kubernetes.io/version: "24.0.4"
spec:
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
containers:
- name: keycloak
image: quay.io/keycloak/keycloak:24.0.4
args: ["start"]
ports:
- name: http
containerPort: 8080
- name: mgmt
containerPort: 9000
resources:
requests:
cpu: "1"
memory: "2Gi"
limits:
cpu: "1"
memory: "2Gi"
startupProbe:
httpGet:
path: /health/started
port: mgmt
periodSeconds: 10
failureThreshold: 30
timeoutSeconds: 5
readinessProbe:
httpGet:
path: /health/ready
port: mgmt
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /health/live
port: mgmt
periodSeconds: 30
failureThreshold: 3
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: data, mountPath: /opt/keycloak/data }
volumes:
- name: tmp
emptyDir: {}
- name: data
emptyDir: {}
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: keycloak
namespace: identity
spec:
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak-prod
```
**왜 좋은가:**
- Guaranteed QoS (request == limit 모든 리소스) → eviction 우선순위 최고.
- startup budget = 10s × 30 = 300s, Keycloak cold boot p99 덮음.
- management port 9000에만 health, HTTP 8080은 traffic 전용.
- PDB maxUnavailable: 1로 3-node infinispan cluster 중 최소 2개 생존 보장.
---
## 좋은 예시 3: 1.29+ native sidecar (log forwarder)
```yaml
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: report-worker
namespace: reporting
labels:
app.kubernetes.io/name: report-worker
app.kubernetes.io/instance: report-worker-prod
spec:
replicas: 2
selector:
matchLabels:
app.kubernetes.io/name: report-worker
app.kubernetes.io/instance: report-worker-prod
template:
metadata:
labels:
app.kubernetes.io/name: report-worker
app.kubernetes.io/instance: report-worker-prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
initContainers:
- name: schema-check
image: registry.example.com/tools/schema-check:1.2.0
command: ["/bin/schema-check", "--fail-fast"]
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 128Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop: ["ALL"]
- name: log-forwarder
image: grafana/alloy:v1.2.0
restartPolicy: Always # <- native sidecar (1.29+)
args: ["run", "/etc/alloy/config.alloy"]
resources:
requests: { cpu: 50m, memory: 128Mi }
limits: { memory: 256Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: alloy-config
mountPath: /etc/alloy
- name: shared-logs
mountPath: /var/log/app
containers:
- name: worker
image: registry.example.com/reporting/worker:2.3.1
resources:
requests:
cpu: "200m"
memory: "512Mi"
limits:
memory: "768Mi"
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: shared-logs
mountPath: /var/log/app
- name: tmp
mountPath: /tmp
volumes:
- name: alloy-config
configMap:
name: alloy-config
- name: shared-logs
emptyDir: {}
- name: tmp
emptyDir: {}
```
**왜 좋은가:**
- `restartPolicy: Always` on init container = native sidecar 패턴 (1.29+).
- init container 순서: schema-check 완료 → log-forwarder sidecar 시작 → main container.
- sidecar는 main 종료 후 SIGTERM 받음 (log flush 가능).
---
## 나쁜 예시 1: CPU limit 기계적 설정 (throttling 유발)
```yaml
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "500m" # BAD
memory: "1Gi"
```
**문제:**
- Linux CFS quota가 100ms period 내 burst만으로도 throttle을 발생시킨다.
- p99 latency가 간헐적으로 튀어도 원인이 숨는다 (metric은 평균 usage 기준).
- Google SRE / Tim Hockin 공식 가이드: "대부분의 워크로드에서 CPU limit를 제거하라".
**Fix:** CPU는 request만, memory만 limit로.
---
## 나쁜 예시 2: liveness로 readiness 대신함
```yaml
livenessProbe:
httpGet:
path: /actuator/health # BAD - deep check
port: 8080
periodSeconds: 5
failureThreshold: 2
# readiness 없음
```
**문제:**
- deep `/actuator/health`는 DB/외부 의존성 포함. DB blip → 모든 Pod 재시작 → cascading failure.
- 트래픽 수용 준비 상태를 표현할 수단이 없다.
**Fix:** startup / readiness / liveness 세 축 분리. liveness는 `/health/live` 같은 shallow check.
---
## 나쁜 예시 3: podAntiAffinity로 spread 시도 (legacy)
```yaml
affinity:
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchExpressions:
- key: app
operator: In
values: ["auth-server"]
topologyKey: kubernetes.io/hostname
```
**문제:**
- replica 수가 노드 수보다 많으면 스케줄 불가.
- zone 분산이 회계되지 않는다 (skew 개념 없음).
- maxSkew 튜닝 불가.
**Fix:** topologySpreadConstraints 사용 (좋은 예시 1 참조).
---
## 나쁜 예시 4: replica 1 서비스에 PDB
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
spec:
minAvailable: 1 # BAD - with replica=1
selector:
matchLabels:
app: singleton-app
```
**문제:**
- node drain이 영구 블록된다 (`PDB violation`).
- Kubernetes 업그레이드가 불가능해진다.
**Fix:** replica 1은 PDB 제거. 필요 시 replica 2+로 늘리고 PDB 적용.
---
## 나쁜 예시 5: HPA v1 스타일 (behavior 없음)
```yaml
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: auth-server
minReplicas: 1
maxReplicas: 10
metrics:
- type: Resource
resource:
name: cpu
target:
type: Utilization
averageUtilization: 80
# behavior block 없음
```
**문제:**
- 기본 scale-down stabilization 300s지만 scale-up도 쓸데없이 보수적.
- 트래픽 burst에 대응 지연.
- 트래픽 drop 뒤 flapping 발생 가능 (policy 정의 없음).
**Fix:** `behavior` block 필수 (좋은 예시 1 참조).
---
## 나쁜 예시 6: limit만 있고 request 없음
```yaml
resources:
limits:
cpu: "1"
memory: "1Gi"
```
**문제:**
- Kubernetes가 request = limit로 복사 → 암묵적 Guaranteed.
- 스케줄러 회계가 과대 평가되어 cluster density 저하.
- 의도한 QoS class와 다름.
**Fix:** requests 명시 필수.
+602
View File
@@ -0,0 +1,602 @@
# infra scripts 예시
---
## 좋은 예시 1: `scripts/lib/common.sh` (공통 라이브러리)
```bash
#!/usr/bin/env bash
# common.sh - shared helpers. source this from bin/ scripts.
# do NOT execute directly.
# shellcheck disable=SC2034 # variables may be used by callers
readonly COMMON_SH_LOADED=1
log() {
local level="$1"; shift
local ts
ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
printf '%s [%s] %s\n' "${ts}" "${level}" "$*" >&2
}
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
fatal() { log FATAL "$@"; exit 1; }
require_cmd() {
local cmd="$1"
command -v "${cmd}" >/dev/null 2>&1 \
|| fatal "required command not found: ${cmd}"
}
require_env() {
local name="$1"
local val="${!name:-}"
[[ -n "${val}" ]] || fatal "required env var not set: ${name}"
}
confirm() {
# usage: confirm "delete namespace foo?" || return 1
local prompt="${1:-continue?}"
if [[ "${CONFIRM:-no}" == "yes" || "${YES:-0}" -eq 1 ]]; then
return 0
fi
local reply
printf '%s [y/N] ' "${prompt}" >&2
read -r reply
[[ "${reply}" == "y" || "${reply}" == "Y" ]]
}
mask_secrets() {
sed -E \
-e 's/(password=)[^ ]+/\1***/g' \
-e 's/(token=)[^ ]+/\1***/g' \
-e 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1***/g'
}
retry() {
local max="$1"; shift
local delay="$1"; shift
local n=0
until "$@"; do
n=$((n + 1))
if (( n >= max )); then
error "retry exhausted after ${max} attempts: $*"
return 1
fi
warn "retry $n/$max failed, sleeping ${delay}s"
sleep "${delay}"
done
}
```
**왜 좋은가:**
- log 함수가 ISO 8601 UTC + LEVEL + stderr.
- require_cmd / require_env / confirm / mask_secrets / retry 가 재사용 가능한 작은 단위.
- shellcheck suppression 은 이유 주석과 함께.
---
## 좋은 예시 2: `scripts/bin/render-diff-apply` (render → diff → apply wrapper)
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
source "${SCRIPT_DIR}/../lib/common.sh"
usage() {
cat <<'EOF' >&2
Usage: render-diff-apply [OPTIONS]
--overlay PATH kustomize overlay directory (required)
--context NAME kube context name (required)
--namespace NS target namespace (optional, derived from overlay)
--timeout DUR rollout status timeout (default: 10m)
--yes skip interactive confirmation for apply
--dry-run render + diff only, no apply
-h, --help show this help
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
EOF
}
parse_args() {
OVERLAY=""
CONTEXT=""
NAMESPACE=""
TIMEOUT="10m"
YES=0
DRY_RUN=0
while [[ $# -gt 0 ]]; do
case "$1" in
--overlay) OVERLAY="$2"; shift 2 ;;
--context) CONTEXT="$2"; shift 2 ;;
--namespace) NAMESPACE="$2"; shift 2 ;;
--timeout) TIMEOUT="$2"; shift 2 ;;
--yes) YES=1; shift ;;
--dry-run) DRY_RUN=1; shift ;;
-h|--help) usage; exit 0 ;;
*) usage; fatal "unknown arg: $1" ;;
esac
done
[[ -n "${OVERLAY}" ]] || { usage; fatal "--overlay is required"; }
[[ -n "${CONTEXT}" ]] || { usage; fatal "--context is required"; }
[[ -d "${OVERLAY}" ]] || fatal "overlay not found: ${OVERLAY}"
}
kctx() {
kubectl --context="${CONTEXT}" "$@"
}
render() {
local out="$1"
info "rendering ${OVERLAY}"
kubectl kustomize "${OVERLAY}" > "${out}"
info "rendered $(wc -l < "${out}") lines to ${out}"
}
validate() {
local rendered="$1"
info "server-side dry-run validation"
kctx apply -f "${rendered}" --dry-run=server >/dev/null
}
show_diff() {
info "computing diff"
# kubectl diff exit code: 0 no diff, 1 diff, >1 error
set +e
kctx diff -k "${OVERLAY}"
local rc=$?
set -e
case "${rc}" in
0) info "no diff" ;;
1) info "diff present" ;;
*) fatal "diff failed with code ${rc}" ;;
esac
return "${rc}"
}
apply_overlay() {
info "applying ${OVERLAY} to context=${CONTEXT}"
kctx apply -k "${OVERLAY}"
}
watch_rollout() {
[[ -n "${NAMESPACE}" ]] || return 0
local deployments
deployments="$(kctx -n "${NAMESPACE}" get deploy -o jsonpath='{.items[*].metadata.name}' || true)"
for d in ${deployments}; do
info "rollout status: deployment/${d}"
retry 3 5 kctx -n "${NAMESPACE}" rollout status "deployment/${d}" --timeout="${TIMEOUT}"
done
}
main() {
parse_args "$@"
require_cmd kubectl
require_cmd kustomize
TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT INT TERM
local rendered="${TMPDIR}/rendered.yaml"
render "${rendered}"
validate "${rendered}"
local diff_rc=0
show_diff || diff_rc=$?
if (( DRY_RUN == 1 )); then
info "dry-run mode: skipping apply"
exit 0
fi
if (( diff_rc == 0 )); then
info "no changes, nothing to apply"
exit 0
fi
if (( YES != 1 )) && [[ "${CONFIRM:-no}" != "yes" ]]; then
confirm "apply changes to context=${CONTEXT} overlay=${OVERLAY}?" \
|| fatal "aborted by user"
fi
apply_overlay
watch_rollout
info "done"
}
main "$@"
```
**왜 좋은가:**
- strict mode + trap + usage + main "$@" + log 전부 포함.
- `--yes` / `CONFIRM=yes` 이중 gate.
- `--dry-run=server` validation 이 apply 전 필수.
- `kubectl diff` 의 exit code (0/1/>1) 정확히 분기.
- retry 함수로 rollout status 불안정성 흡수.
- secret 을 argv / 로그에 쓰지 않음.
- jsonpath 로 deployment 목록 파싱, regex 없음.
---
## 좋은 예시 3: `scripts/bin/backup-k3s` (etcd snapshot backup, destructive-aware)
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
source "${SCRIPT_DIR}/../lib/common.sh"
usage() {
cat <<'EOF' >&2
Usage: backup-k3s [OPTIONS]
--node HOST server node to snapshot on (required)
--s3-endpoint URL S3 endpoint for offsite copy (optional)
--retention N days to keep local snapshots (default: 7)
-h, --help show this help
Environment:
SSH_USER ssh user (default: current user)
S3_ACCESS_KEY required if --s3-endpoint is set
S3_SECRET_KEY required if --s3-endpoint is set
EOF
}
main() {
local NODE="" S3_ENDPOINT="" RETENTION=7
while [[ $# -gt 0 ]]; do
case "$1" in
--node) NODE="$2"; shift 2 ;;
--s3-endpoint) S3_ENDPOINT="$2"; shift 2 ;;
--retention) RETENTION="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) usage; fatal "unknown arg: $1" ;;
esac
done
[[ -n "${NODE}" ]] || { usage; fatal "--node required"; }
require_cmd ssh
if [[ -n "${S3_ENDPOINT}" ]]; then
require_env S3_ACCESS_KEY
require_env S3_SECRET_KEY
fi
local ts
ts="$(date -u +'%Y%m%dT%H%M%SZ')"
local snap="k3s-snapshot-${ts}.db"
info "creating snapshot on node=${NODE}"
ssh "${SSH_USER:-$USER}@${NODE}" \
"sudo k3s etcd-snapshot save --name ${snap}"
info "pruning snapshots older than ${RETENTION} days on ${NODE}"
ssh "${SSH_USER:-$USER}@${NODE}" \
"sudo find /var/lib/rancher/k3s/server/db/snapshots -name 'k3s-snapshot-*.db' -mtime +${RETENTION} -print -delete"
if [[ -n "${S3_ENDPOINT}" ]]; then
info "uploading ${snap} to ${S3_ENDPOINT} (credentials masked)"
# secret 은 env 로 mc 에 전달, argv 노출 금지
ssh "${SSH_USER:-$USER}@${NODE}" \
"S3_ACCESS_KEY='${S3_ACCESS_KEY}' S3_SECRET_KEY='${S3_SECRET_KEY}' \
mc alias set backup ${S3_ENDPOINT} \"\${S3_ACCESS_KEY}\" \"\${S3_SECRET_KEY}\" 2>&1 | mask-secrets || true && \
mc cp /var/lib/rancher/k3s/server/db/snapshots/${snap} backup/k3s-snapshots/${snap}"
fi
info "backup complete: ${snap}"
}
main "$@"
```
**왜 좋은가:**
- backup 은 destructive 가 아니므로 `--yes` 는 없지만, prune 은 retention 일수로 guard.
- secret 은 argv 로 전달 X, env 로 ssh 내부에서만.
- ISO 8601 UTC timestamp 로 이름 충돌 방지.
- require_env 로 credential 선검증.
---
## 좋은 예시 4: destructive 스크립트 예시 (`scripts/bin/delete-namespace`)
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
source "${SCRIPT_DIR}/../lib/common.sh"
usage() {
cat <<'EOF' >&2
Usage: delete-namespace --context CTX --namespace NS [--yes]
DANGER: this deletes the namespace and all its resources (including PVCs
if reclaimPolicy=Delete). Requires --yes or CONFIRM=yes.
EOF
}
main() {
local CONTEXT="" NS="" YES=0
while [[ $# -gt 0 ]]; do
case "$1" in
--context) CONTEXT="$2"; shift 2 ;;
--namespace) NS="$2"; shift 2 ;;
--yes) YES=1; shift ;;
-h|--help) usage; exit 0 ;;
*) usage; fatal "unknown arg: $1" ;;
esac
done
[[ -n "${CONTEXT}" ]] || { usage; fatal "--context required"; }
[[ -n "${NS}" ]] || { usage; fatal "--namespace required"; }
require_cmd kubectl
if (( YES != 1 )) && [[ "${CONFIRM:-no}" != "yes" ]]; then
usage
fatal "destructive op requires --yes or CONFIRM=yes"
fi
warn "will DELETE namespace=${NS} in context=${CONTEXT}"
local pvc_count
pvc_count="$(kubectl --context="${CONTEXT}" -n "${NS}" get pvc -o json | jq '.items | length')"
warn "PVC count in namespace: ${pvc_count}"
kubectl --context="${CONTEXT}" delete namespace "${NS}" --wait=true
info "deleted namespace=${NS}"
}
main "$@"
```
**왜 좋은가:**
- destructive op 는 `--yes` / `CONFIRM=yes` 이중 gate.
- 삭제 전 PVC 수를 jq 로 보여줌 (사용자 자각).
- `--wait=true` 로 실제 삭제 완료 확인.
- JSON 파싱은 jq, regex 없음.
---
## 좋은 예시 5: local 선언과 command substitution 분리
```bash
get_current_context() {
local ctx
ctx="$(kubectl config current-context)" # 분리
printf '%s\n' "${ctx}"
}
```
**왜 좋은가:**
- ShellCheck SC2155: `local ctx="$(...)"``local` 의 exit status 가 cmd substitution 을 가리므로 에러가 숨는다.
- 분리해야 `$?` 가 실제 kubectl 결과 반영.
---
## 좋은 예시 6: JSON 파싱
```bash
# jsonpath
get_image() {
local ns="$1" deploy="$2"
kubectl -n "${ns}" get deploy "${deploy}" \
-o jsonpath='{.spec.template.spec.containers[0].image}'
}
# jq
get_all_images() {
local ns="$1"
kubectl -n "${ns}" get pods -o json \
| jq -r '.items[].spec.containers[].image' \
| sort -u
}
```
**왜 좋은가:**
- jsonpath / jq 는 구조적 파싱 → field 순서나 formatting 변화에 내성.
---
## 좋은 예시 7: secret masking 적용 예
```bash
deploy_with_debug() {
local overlay="$1"
if [[ "${DEBUG:-0}" -eq 1 ]]; then
set -x
fi
kubectl apply -k "${overlay}" 2>&1 | mask_secrets
if [[ "${DEBUG:-0}" -eq 1 ]]; then
set +x
fi
}
```
**왜 좋은가:**
- debug 시에도 stdout/stderr 에 secret 이 새지 않음.
- mask_secrets 가 common lib 에서 재사용.
---
## 나쁜 예시 1: strict mode 없음
```bash
#!/bin/bash
# strict mode 없음
TMP=/tmp/foo
rm -rf $TMP
mkdir $TMP
some_command
# 실패해도 계속 진행
```
**문제:**
- 실패가 조용히 통과 (`set -e` 없음).
- unset variable 에서 빈 경로로 rm → 재앙 가능.
- unquoted `$TMP` 공백 split.
**Fix:** `set -euo pipefail` + `IFS=$'\n\t'` + trap + quote.
---
## 나쁜 예시 2: heredoc YAML 생성기
```bash
deploy_auth() {
cat <<EOF > /tmp/auth.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
spec:
replicas: ${REPLICAS}
template:
spec:
containers:
- name: auth
image: auth:${VERSION}
EOF
kubectl apply -f /tmp/auth.yaml
}
```
**문제:**
- 선언형 원본이 스크립트 안에 숨음.
- Git diff 로 환경별 차이 추적 불가.
- 리뷰 / audit / kustomize 기능 모두 상실.
**Fix:** Kustomize overlay → `kubectl apply -k`.
---
## 나쁜 예시 3: regex 로 kubectl 출력 파싱
```bash
kubectl get pods | grep Running | awk '{print $1}'
```
**문제:**
- column 순서나 추가 field 변화에 깨짐.
- `Running` 이 pod 이름에 포함되면 오인식.
**Fix:**
```bash
kubectl get pods --field-selector=status.phase=Running -o jsonpath='{.items[*].metadata.name}'
```
---
## 나쁜 예시 4: secret 을 argv 로 전달
```bash
mc alias set backup https://s3.example.com "${ACCESS}" "${SECRET}"
# ps aux 에 노출, history 에 기록
```
**문제:**
- `ps` 나 audit log 에서 credential 유출.
- bash history (`HISTFILE`) 에 기록 가능.
**Fix:**
```bash
mc alias set backup https://s3.example.com \
"$(echo "${ACCESS}")" "$(cat /run/secrets/s3-secret)"
# 또는 환경변수로 mc 가 직접 읽도록
MC_HOST_backup="https://${ACCESS}:${SECRET}@s3.example.com" mc cp ...
```
---
## 나쁜 예시 5: confirmation 없는 destructive
```bash
#!/usr/bin/env bash
kubectl delete ns prod
```
**문제:**
- 의도 / 권한 / audit 전혀 없음.
- 사고 직결.
**Fix:** 좋은 예시 4 참조 (`--yes` / `CONFIRM=yes` gate + 사전 정보 표시).
---
## 나쁜 예시 6: trap 없이 임시파일
```bash
TMP="$(mktemp)"
do_something > "${TMP}"
# 실패 시 /tmp 에 쓰레기 남음
rm "${TMP}"
```
**문제:**
- 스크립트 실패 / Ctrl-C 시 임시 파일 누적.
- secret 이 들어있으면 유출.
**Fix:**
```bash
TMP="$(mktemp)"
trap 'rm -f "${TMP}"' EXIT INT TERM
do_something > "${TMP}"
```
---
## 나쁜 예시 7: `local` 과 command substitution 한 줄
```bash
bad() {
local ctx="$(kubectl config current-context)" # $? 가려짐
}
```
**문제:**
- ShellCheck SC2155. `local` 의 exit status 가 cmd substitution 을 덮어 에러 감지 실패.
**Fix:**
```bash
good() {
local ctx
ctx="$(kubectl config current-context)"
}
```
+528
View File
@@ -0,0 +1,528 @@
# security hardening 예시
이 문서의 모든 YAML은 `kubectl apply --dry-run=server -f -` 기준 clean을 목표로 한다. 1000+ 서비스 운영 클러스터의 auth-server namespace를 기준 예시로 사용한다.
---
## 좋은 예시 1: Namespace에 Pod Security Admission 라벨 enforce
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: auth-prod
labels:
app.kubernetes.io/part-of: identity-platform
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.29
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.29
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.29
annotations:
platform.example.com/owner: identity-team
platform.example.com/adr: ADR-0017-psa-restricted-baseline
```
**왜 좋은가:**
- 운영 namespace의 PSA 기본값을 `restricted`로 enforce. violation Pod는 API server 단에서 reject된다.
- version을 pin해 Kubernetes 업그레이드 시 silent behavior drift를 방지한다.
- audit/warn을 함께 붙여 위반을 audit log와 kubectl warning으로 수집한다.
---
## 좋은 예시 2: Restricted 프로파일을 완전히 만족하는 Deployment
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/version: 1.42.0
app.kubernetes.io/managed-by: argocd
spec:
replicas: 6
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/version: 1.42.0
app.kubernetes.io/managed-by: argocd
spec:
serviceAccountName: auth-server
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth-server
containers:
- name: auth-server
image: registry.example.com/identity/auth-server@sha256:8f3c0a8c6b3a2a7a0f1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4e5f6071
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
envFrom:
- secretRef:
name: auth-server-db
resources:
requests:
cpu: 200m
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: http
initialDelaySeconds: 5
periodSeconds: 5
failureThreshold: 3
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: http
initialDelaySeconds: 30
periodSeconds: 10
failureThreshold: 5
startupProbe:
httpGet:
path: /actuator/health/liveness
port: http
failureThreshold: 30
periodSeconds: 5
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: tmp
mountPath: /tmp
- name: workdir
mountPath: /workspace
volumes:
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 64Mi
- name: workdir
emptyDir:
sizeLimit: 256Mi
imagePullSecrets:
- name: registry-example-com
```
**왜 좋은가:**
- `restricted` 프로파일의 전 필드(runAsNonRoot, numeric UID/GID, fsGroup, seccompProfile, allowPrivilegeEscalation, readOnlyRootFilesystem, drop ALL capabilities)를 Pod+컨테이너 양쪽에 일관 명시한다.
- image는 digest pin. mutable tag에 의존하지 않는다.
- ServiceAccount는 전용 SA + `automountServiceAccountToken: false`.
- writable 경로는 `emptyDir`로 분리해 root FS는 read-only 유지.
---
## 나쁜 예시 1: Restricted 프로파일 위반 Pod
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
namespace: auth-prod
spec:
replicas: 1
selector:
matchLabels:
app: auth-server
template:
metadata:
labels:
app: auth-server
spec:
containers:
- name: auth-server
image: auth-server:latest
securityContext:
privileged: true
```
**문제:**
- `privileged: true`는 baseline조차 위반. PSA enforce=restricted namespace에서는 API server가 reject한다.
- `runAsNonRoot`, `allowPrivilegeEscalation`, `capabilities.drop`, `seccompProfile`, `readOnlyRootFilesystem` 전부 누락.
- image tag `latest`는 digest 고정 없이 rolling silently breaks.
- SA 미지정 → `default` SA가 토큰 자동 마운트.
---
## 좋은 예시 3: Default-deny + DNS + Ingress + DB + Prometheus allow NetworkPolicy 세트
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: default-deny-all
namespace: auth-prod
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-dns-egress
namespace: auth-prod
spec:
podSelector: {}
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: kube-system
podSelector:
matchLabels:
k8s-app: kube-dns
ports:
- protocol: UDP
port: 53
- protocol: TCP
port: 53
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-from-ingress-traefik
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-traefik
podSelector:
matchLabels:
app.kubernetes.io/name: traefik
ports:
- protocol: TCP
port: 8080
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-to-postgres
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes:
- Egress
egress:
- to:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: data-prod
podSelector:
matchLabels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: identity-postgres
ports:
- protocol: TCP
port: 5432
---
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: allow-metrics-scrape-from-prometheus
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
```
**왜 좋은가:**
- `namespaceSelector``podSelector`**동일 `from` 엔트리** 안에 있으므로 AND(교집합): monitoring namespace 안의 Prometheus Pod만 9090 scrape 허용된다.
- default-deny + minimum allow 세트로 ingress/egress 모두 통제.
- DNS는 `kube-system``k8s-app=kube-dns` Pod로 한정, egress 전체를 열지 않음.
---
## 나쁜 예시 2: NetworkPolicy AND/OR 혼동
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: broken-scrape
namespace: auth-prod
spec:
podSelector:
matchLabels:
app.kubernetes.io/name: auth-server
policyTypes:
- Ingress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: monitoring
- podSelector:
matchLabels:
app.kubernetes.io/name: prometheus
ports:
- protocol: TCP
port: 9090
```
**문제:**
- `namespaceSelector``podSelector`**별도 엔트리**(두 개의 `-`) → OR로 해석된다.
- 결과: ① monitoring namespace의 **모든 Pod**가 허용되고, ② `auth-prod` namespace의 label `app.kubernetes.io/name=prometheus`를 가진 **아무 Pod**도 허용된다.
- 의도했던 "monitoring의 Prometheus만 허용"이 아니라 훨씬 넓은 경로가 열린다. 실제 클러스터에서 NetworkPolicy 버그의 1순위.
---
## 좋은 예시 4: Namespace-scoped RBAC (Role + RoleBinding)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server-secret-rotator
namespace: auth-prod
automountServiceAccountToken: true
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: auth-server-secret-reader
namespace: auth-prod
rules:
- apiGroups: [""]
resources: ["secrets"]
resourceNames:
- auth-server-db
- auth-server-oidc-client
verbs: ["get"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: auth-server-secret-reader
namespace: auth-prod
subjects:
- kind: ServiceAccount
name: auth-server-secret-rotator
namespace: auth-prod
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: auth-server-secret-reader
```
**왜 좋은가:**
- namespace 경계 안에서 특정 Secret 이름 2개만 `get`. `list`/`watch` 미부여.
- SA/Role/RoleBinding 모두 같은 namespace에 명시. `---`로 분리된 다중 리소스 문서.
- `system:masters``cluster-admin` 같은 전능 role과 무관.
---
## 나쁜 예시 3: cluster-admin ClusterRoleBinding 남용
```yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: auth-server-admin
subjects:
- kind: ServiceAccount
name: auth-server
namespace: auth-prod
roleRef:
kind: ClusterRole
name: cluster-admin
apiGroup: rbac.authorization.k8s.io
```
**문제:**
- 단일 SA가 모든 namespace의 모든 리소스(Secret, Node, CRD)를 수정할 수 있다.
- 앱 노드 1개가 compromise되면 전체 클러스터가 compromise된다.
- least privilege 원칙의 정반대.
---
## 좋은 예시 5: Private registry ImagePullSecret
```yaml
apiVersion: v1
kind: Secret
metadata:
name: registry-example-com
namespace: auth-prod
type: kubernetes.io/dockerconfigjson
data:
.dockerconfigjson: eyJhdXRocyI6eyJyZWdpc3RyeS5leGFtcGxlLmNvbSI6eyJ1c2VybmFtZSI6ImNpLWJvdCIsInBhc3N3b3JkIjoiPFJFREFDVEVEPiIsImF1dGgiOiI8UkVEQUNURUQ+In19fQ==
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server
namespace: auth-prod
automountServiceAccountToken: false
imagePullSecrets:
- name: registry-example-com
```
**왜 좋은가:**
- type이 `kubernetes.io/dockerconfigjson`으로 정확. kubelet이 이 포맷만 pull credential로 인식한다.
- SA에 `imagePullSecrets`를 묶어 Deployment 마다 반복 선언 불필요.
- 실제 운영에서는 이 Secret 자체도 VSO로 Vault → K8s로 sync(config-and-secrets 문서 참고).
---
## 나쁜 예시 4: 정책 없는 운영 namespace
```yaml
apiVersion: v1
kind: Namespace
metadata:
name: auth-prod
```
**문제:**
- PSA 라벨 없음 → `privileged` Pod도 통과.
- NetworkPolicy 없음 → ingress/egress 모두 allow-all. 침해 시 lateral movement 자유.
- ResourceQuota/LimitRange 없음 → 한 Deployment가 namespace CPU/memory 전부 점유 가능.
- 1000-서비스 운영에서 이런 namespace는 허용되지 않는다.
---
## 좋은 예시 6: ResourceQuota + LimitRange 묶음
```yaml
apiVersion: v1
kind: ResourceQuota
metadata:
name: auth-prod-quota
namespace: auth-prod
spec:
hard:
requests.cpu: "50"
requests.memory: 100Gi
limits.cpu: "100"
limits.memory: 200Gi
pods: "200"
services.loadbalancers: "0"
services.nodeports: "0"
---
apiVersion: v1
kind: LimitRange
metadata:
name: auth-prod-defaults
namespace: auth-prod
spec:
limits:
- type: Container
default:
cpu: 500m
memory: 512Mi
defaultRequest:
cpu: 100m
memory: 128Mi
max:
cpu: "4"
memory: 4Gi
```
**왜 좋은가:**
- `services.loadbalancers=0`, `services.nodeports=0`으로 namespace 내 외부 노출 Service 생성을 금지(ingress 경유 강제).
- LimitRange로 컨테이너별 default request/limit을 보장해 limit 누락 Pod를 예방.
+641
View File
@@ -0,0 +1,641 @@
# storage / PVC 예시
모든 예시는 `kubectl apply -f` 로 바로 적용 가능한 완성 매니페스트다.
생략(`...`)이 있는 곳은 의도적으로 다른 문서로 위임한 부분이다.
---
## 좋은 예시 1: 운영 StorageClass 표준 세트 (WaitForFirstConsumer + Retain)
```yaml
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: fast-ssd-retain
labels:
app.kubernetes.io/part-of: platform-storage
storage.platform.io/tier: gold
annotations:
storage.platform.io/description: "prod stateful (DB, vault, object store). retain on PVC delete."
provisioner: driver.longhorn.io
parameters:
numberOfReplicas: "3"
staleReplicaTimeout: "30"
fsType: ext4
reclaimPolicy: Retain
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: standard-delete
labels:
app.kubernetes.io/part-of: platform-storage
storage.platform.io/tier: silver
annotations:
storage.platform.io/description: "dev/test, ephemeral, rebuild-safe data. deletes on PVC removal."
provisioner: driver.longhorn.io
parameters:
numberOfReplicas: "2"
fsType: ext4
reclaimPolicy: Delete
volumeBindingMode: WaitForFirstConsumer
allowVolumeExpansion: true
---
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: rwx-shared
labels:
app.kubernetes.io/part-of: platform-storage
storage.platform.io/tier: shared
provisioner: nfs.csi.k8s.io
parameters:
server: nfs.storage.svc.cluster.local
share: /exports/shared
reclaimPolicy: Retain
volumeBindingMode: Immediate # 네트워크 스토리지이고 topology 제약 없음 → 예외적으로 Immediate 허용
allowVolumeExpansion: true
mountOptions:
- nfsvers=4.1
- hard
- noatime
```
왜 좋은가:
- `volumeBindingMode: WaitForFirstConsumer` 기본, `Immediate`는 이유를 주석으로 명시
- `reclaimPolicy`가 데이터 등급에 따라 다르게 선언됨 (Retain / Delete)
- `allowVolumeExpansion: true` 기본
- 라벨/annotation으로 용도 구분
❌ 나쁜 예시 1: 기본값 의존 + Immediate 바인딩
```yaml
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: default
provisioner: driver.longhorn.io
# reclaimPolicy 미지정 → 기본 Delete (운영 데이터도 삭제됨)
# volumeBindingMode 미지정 → 기본 Immediate (topology 충돌 유발)
# allowVolumeExpansion 미지정 → 확장 불가
```
문제:
- `reclaimPolicy` 기본 `Delete`: 실수로 PVC를 지우면 PV와 데이터까지 사라진다
- `volumeBindingMode` 기본 `Immediate`: Pod가 스케줄되지 못하는 zone/node에 PV가 붙을 수 있다
- 확장 불가
---
## 좋은 예시 2: VolumeSnapshotClass를 StorageClass와 매칭
```yaml
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: fast-ssd-snap-retain
labels:
app.kubernetes.io/part-of: platform-storage
velero.io/csi-volumesnapshot-class: "true"
driver: driver.longhorn.io
deletionPolicy: Retain
parameters:
type: bak
csi.storage.k8s.io/snapshotter-secret-name: longhorn-backup-secret
csi.storage.k8s.io/snapshotter-secret-namespace: longhorn-system
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: standard-snap-delete
labels:
app.kubernetes.io/part-of: platform-storage
driver: driver.longhorn.io
deletionPolicy: Delete
```
왜 좋은가:
- snapshot class가 StorageClass 등급과 1:1 매칭
- 운영 데이터용은 `deletionPolicy: Retain`
- Velero가 인식하도록 `velero.io/csi-volumesnapshot-class: "true"` 라벨 부여
---
## 좋은 예시 3: PostgreSQL StatefulSet + PVC retention Retain
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: data-prod
labels:
pod-security.kubernetes.io/enforce: restricted
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres
namespace: data-prod
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-prod
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: argocd
app.kubernetes.io/version: "16.4"
spec:
serviceName: postgres
replicas: 1
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
selector:
matchLabels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-prod
template:
metadata:
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-prod
app.kubernetes.io/component: database
spec:
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres
image: postgres@sha256:8a6b7c6f0e0b5e2b0e2b0e2b0e2b0e2b0e2b0e2b0e2b0e2b0e2b0e2b0e2b0e2b
imagePullPolicy: IfNotPresent
ports:
- name: pg
containerPort: 5432
env:
- name: POSTGRES_DB
value: auth
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
envFrom:
- secretRef:
name: postgres-credentials
resources:
requests:
cpu: "500m"
memory: "1Gi"
limits:
cpu: "2"
memory: "4Gi"
readinessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 10
periodSeconds: 5
livenessProbe:
exec:
command: ["pg_isready", "-U", "postgres"]
initialDelaySeconds: 30
periodSeconds: 10
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: tmp
mountPath: /tmp
- name: run
mountPath: /var/run/postgresql
volumes:
- name: tmp
emptyDir: {}
- name: run
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-prod
backup.platform.io/tier: gold
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd-retain
resources:
requests:
storage: 50Gi
```
왜 좋은가:
- `persistentVolumeClaimRetentionPolicy`가 명시적으로 `Retain`
- StorageClass `fast-ssd-retain`에 맞물리는 `RWO`
- `fsGroup` + `fsGroupChangePolicy` 설정
- restricted PSA 준수 (runAsNonRoot, readOnlyRootFilesystem, capabilities drop all)
- `emptyDir`로 tmp/run 분리 (PVC 남발 방지)
- 라벨 full set + backup tier 라벨
---
## 좋은 예시 4: 단일 PVC Deployment (RWO, replicas 1)
```yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: minio-data
namespace: object-prod
labels:
app.kubernetes.io/name: minio
app.kubernetes.io/component: object-store
backup.platform.io/tier: gold
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd-retain
resources:
requests:
storage: 500Gi
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: minio
namespace: object-prod
labels:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio-prod
app.kubernetes.io/component: object-store
app.kubernetes.io/part-of: platform-storage
spec:
replicas: 1
strategy:
type: Recreate # RWO 단일 PVC이므로 RollingUpdate 금지
selector:
matchLabels:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio-prod
template:
metadata:
labels:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio-prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: minio
image: quay.io/minio/minio@sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789
args: ["server", "/data", "--console-address", ":9001"]
ports:
- {name: s3, containerPort: 9000}
- {name: console, containerPort: 9001}
envFrom:
- secretRef:
name: minio-root-credentials
resources:
requests: {cpu: "250m", memory: "512Mi"}
limits: {cpu: "2", memory: "4Gi"}
readinessProbe:
httpGet: {path: /minio/health/ready, port: s3}
periodSeconds: 5
livenessProbe:
httpGet: {path: /minio/health/live, port: s3}
periodSeconds: 20
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- {name: data, mountPath: /data}
volumes:
- name: data
persistentVolumeClaim:
claimName: minio-data
```
왜 좋은가:
- StatefulSet 없이도 stable한 단일 writer 구성
- `strategy: Recreate`로 RWO 충돌 방지
- PVC와 StorageClass가 명시적으로 매칭
- backup tier 라벨 → Velero selector와 연동
❌ 나쁜 예시 2: RWO에 RollingUpdate + replicas 2
```yaml
spec:
replicas: 2
strategy:
type: RollingUpdate
template:
spec:
containers:
- volumeMounts:
- {name: data, mountPath: /data}
volumes:
- name: data
persistentVolumeClaim:
claimName: minio-data # RWO인데 두 Pod가 동시에 마운트 시도
```
문제:
- RWO PVC를 두 Pod가 동시에 잡을 수 없어 신규 Pod가 영원히 Pending
- RollingUpdate가 old→new 전환 시 마운트 충돌
- 해결: replicas=1 + Recreate, 또는 RWX, 또는 StatefulSet
---
## 좋은 예시 5: separate PVC (정당한 수명/복구 단위 차이)
```yaml
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: archive-ledger
namespace: finance-prod
labels:
app.kubernetes.io/name: archive-ledger
app.kubernetes.io/instance: archive-ledger-prod
spec:
serviceName: archive-ledger
replicas: 3
selector:
matchLabels:
app.kubernetes.io/name: archive-ledger
app.kubernetes.io/instance: archive-ledger-prod
template:
metadata:
labels:
app.kubernetes.io/name: archive-ledger
app.kubernetes.io/instance: archive-ledger-prod
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: archive-ledger
image: registry.example.com/finance/archive-ledger@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79
ports:
- { name: http, containerPort: 8080 }
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { memory: 2Gi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: data, mountPath: /var/lib/ledger }
- { name: audit-archive, mountPath: /var/lib/ledger/audit }
volumeClaimTemplates:
- metadata:
name: data
labels:
backup.platform.io/tier: gold # 5분 RPO, 매일 snapshot
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd-retain
resources:
requests: {storage: 500Gi}
- metadata:
name: audit-archive
labels:
backup.platform.io/tier: bronze # 24h RPO, 주 1회 snapshot, 7년 보존
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: archive-retain
resources:
requests: {storage: 2Ti}
```
왜 좋은가:
- data와 audit-archive의 RPO/retention이 다름
- StorageClass도 다름 (SSD vs 아카이브)
- backup tier 라벨이 Velero schedule selector에 의해 다르게 잡힘
❌ 나쁜 예시 3: separate PVC 남발
```yaml
volumeClaimTemplates:
- {metadata: {name: logs}}
- {metadata: {name: tmp}}
- {metadata: {name: config-copy}}
- {metadata: {name: cache}}
```
문제:
- 로그/tmp/cache는 `emptyDir` 또는 stdout 대상
- PVC 4개는 수명 구분 없이 쪼갠 것 — 운영 복잡도만 증가
- snapshot/backup 단위가 파편화됨
---
## 좋은 예시 6: 파이프라인 전체 — PVC → Snapshot → Restore → Verify
아래 6개 블록은 순서대로 `kubectl apply` 한다.
### (1) PVC
```yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data
namespace: app-prod
labels:
app.kubernetes.io/name: app
backup.platform.io/tier: gold
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd-retain
resources:
requests: {storage: 20Gi}
```
### (2) VolumeSnapshotClass (전역 1회)
```yaml
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshotClass
metadata:
name: fast-ssd-snap-retain
labels:
velero.io/csi-volumesnapshot-class: "true"
driver: driver.longhorn.io
deletionPolicy: Retain
```
### (3) On-demand VolumeSnapshot
```yaml
---
apiVersion: snapshot.storage.k8s.io/v1
kind: VolumeSnapshot
metadata:
name: app-data-2026-04-16-pre-migration
namespace: app-prod
labels:
app.kubernetes.io/name: app
snapshot.platform.io/reason: pre-migration
spec:
volumeSnapshotClassName: fast-ssd-snap-retain
source:
persistentVolumeClaimName: app-data
```
### (4) Restore: snapshot을 소스로 하는 새 PVC
```yaml
---
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: app-data-restored
namespace: app-prod
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: fast-ssd-retain
resources:
requests: {storage: 20Gi}
dataSource:
name: app-data-2026-04-16-pre-migration
kind: VolumeSnapshot
apiGroup: snapshot.storage.k8s.io
```
### (5) Verify Job
```yaml
---
apiVersion: batch/v1
kind: Job
metadata:
name: app-data-restore-verify
namespace: app-prod
spec:
backoffLimit: 0
ttlSecondsAfterFinished: 3600
template:
spec:
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
fsGroup: 1000
containers:
- name: verify
image: busybox@sha256:3fbc632167424a6d997e74f52b878d7cc478225cffac6bc977eedfe51c7f4e79
command:
- sh
- -c
- |
set -eu
test -d /data
COUNT=$(find /data -type f | wc -l)
echo "file_count=${COUNT}"
test "${COUNT}" -gt 0
resources:
requests: { cpu: 50m, memory: 64Mi }
limits: { memory: 128Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities: {drop: ["ALL"]}
volumeMounts:
- {name: data, mountPath: /data, readOnly: true}
volumes:
- name: data
persistentVolumeClaim:
claimName: app-data-restored
```
### (6) 최종 확인
```bash
kubectl -n app-prod get volumesnapshot,pvc,job
kubectl -n app-prod logs job/app-data-restore-verify
```
왜 좋은가:
- PVC → SnapshotClass → Snapshot → dataSource 기반 PVC restore → Job 검증의 end-to-end 흐름
- `deletionPolicy: Retain`으로 snapshot을 실수로 삭제해도 PV는 남음
- Job이 restricted PSA 준수, 이미지 digest 고정, `backoffLimit: 0`
---
## 나쁜 예시 4: hostPath를 운영 PV로 사용
```yaml
apiVersion: v1
kind: PersistentVolume
metadata:
name: pg-host
spec:
capacity: {storage: 50Gi}
accessModes: ["ReadWriteOnce"]
hostPath:
path: /data/postgres
```
문제:
- 노드 장애 = 데이터 손실
- snapshot / expansion / 다중 노드 스케줄링 전부 불가
- 운영 표준 아님
---
## 나쁜 예시 5: K3s local-path로 production Postgres
```yaml
volumeClaimTemplates:
- metadata: {name: data}
spec:
accessModes: ["ReadWriteOnce"]
storageClassName: local-path # K3s default
resources: {requests: {storage: 100Gi}}
```
문제:
- local-path는 snapshot 미지원 → Velero CSI snapshot 불가
- expansion 미지원 → 용량 부족 시 마이그레이션 필요
- 노드 pin → 노드 장애 시 Postgres 복구 불가
- 해결: Longhorn / OpenEBS / cloud CSI driver로 교체
---
## 나쁜 예시 6: StorageClass 생략
```yaml
spec:
accessModes: ["ReadWriteOnce"]
resources: {requests: {storage: 20Gi}}
# storageClassName 미지정 → 클러스터 default annotation 사용
```
문제:
- 어떤 tier를 기대했는지 선언에서 드러나지 않음
- 클러스터 default가 바뀌면 침묵적으로 다른 StorageClass로 바인딩
- 환경 간 재현 불가
+777
View File
@@ -0,0 +1,777 @@
# Vault 예시
Vault 1.17+ + Helm chart `hashicorp/vault` + VSO 0.8+ 기준. 모든 manifest는 `kubectl apply` 적용 가능한 완전한 형태다.
---
## 좋은 예시 1: Helm values.yaml — HA Raft + auto-unseal + audit
```yaml
# values/vault-prod.yaml
global:
enabled: true
tlsDisable: false
injector:
enabled: true
replicas: 2
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
server:
image:
repository: hashicorp/vault
tag: "1.17.6"
resources:
requests:
cpu: 250m
memory: 256Mi
limits:
cpu: "1"
memory: 512Mi
extraEnvironmentVars:
VAULT_CACERT: /vault/tls/ca.crt
VAULT_TLSCERT: /vault/tls/tls.crt
VAULT_TLSKEY: /vault/tls/tls.key
AWS_REGION: ap-northeast-2
volumes:
- name: vault-tls
secret:
secretName: vault-tls
volumeMounts:
- name: vault-tls
mountPath: /vault/tls
readOnly: true
serviceAccount:
create: true
name: vault
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::123456789012:role/vault-autounseal
readinessProbe:
enabled: true
path: "/v1/sys/health?standbyok=true&perfstandbyok=true&uninitcode=204"
port: 8200
scheme: HTTPS
failureThreshold: 2
initialDelaySeconds: 5
periodSeconds: 5
timeoutSeconds: 3
livenessProbe:
enabled: true
path: "/v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204"
port: 8200
scheme: HTTPS
failureThreshold: 3
initialDelaySeconds: 60
periodSeconds: 30
timeoutSeconds: 3
dataStorage:
enabled: true
size: 20Gi
storageClass: ebs-gp3
accessMode: ReadWriteOnce
mountPath: /vault/data
auditStorage:
enabled: true
size: 10Gi
storageClass: ebs-gp3
accessMode: ReadWriteOnce
mountPath: /vault/audit
service:
enabled: true
type: ClusterIP
port: 8200
targetPort: 8200
ha:
enabled: true
replicas: 3
apiAddr: "https://$(POD_IP):8200"
clusterAddr: "https://$(HOSTNAME).vault-internal:8201"
raft:
enabled: true
setNodeId: true
config: |
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_cert_file = "/vault/tls/tls.crt"
tls_key_file = "/vault/tls/tls.key"
tls_min_version = "tls13"
}
storage "raft" {
path = "/vault/data"
retry_join {
leader_api_addr = "https://vault-0.vault-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
leader_client_cert_file = "/vault/tls/tls.crt"
leader_client_key_file = "/vault/tls/tls.key"
}
retry_join {
leader_api_addr = "https://vault-1.vault-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
leader_client_cert_file = "/vault/tls/tls.crt"
leader_client_key_file = "/vault/tls/tls.key"
}
retry_join {
leader_api_addr = "https://vault-2.vault-internal:8200"
leader_ca_cert_file = "/vault/tls/ca.crt"
leader_client_cert_file = "/vault/tls/tls.crt"
leader_client_key_file = "/vault/tls/tls.key"
}
}
seal "awskms" {
region = "ap-northeast-2"
kms_key_id = "alias/vault-autounseal"
}
service_registration "kubernetes" {}
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
affinity: |
podAntiAffinity:
requiredDuringSchedulingIgnoredDuringExecution:
- labelSelector:
matchLabels:
app.kubernetes.io/name: vault
component: server
topologyKey: kubernetes.io/hostname
topologySpreadConstraints: |
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: vault
component: server
```
**왜 좋은가:**
- `ha.enabled=true` + `raft.enabled=true` + `raft.setNodeId=true` 3종 필수 플래그
- listener와 storage raft stanza가 8200/8201 모두 바인드, `cluster_address` 명시 → peer replication 성립
- `seal "awskms"`로 auto-unseal, Pod 재시작 시 수동 개입 불필요
- `auditStorage.enabled=true` → audit 전용 PVC 분리 (dataStorage 오염 방지)
- IRSA(`eks.amazonaws.com/role-arn`)로 KMS 접근 권한 위임 (static IAM key 없음)
---
## 나쁜 예시 1: chart 기본값 standalone
```bash
helm install vault hashicorp/vault --namespace vault --create-namespace
```
**문제:**
- 기본은 `standalone` + `file` storage → single pod, PVC 1개, HA 없음, snapshot restore로만 복구
- Shamir 수동 unseal → pod 재시작마다 운영자 개입
- audit device 미활성 → 감사 로그 없음
- chart 문서 자체가 "not suitable for production"이라 명시
---
## 좋은 예시 2: Service — 8200 + 8201 둘 다 expose
Helm chart가 자동 생성하지만, 수제 Service 예시:
```yaml
---
apiVersion: v1
kind: Service
metadata:
name: vault
namespace: vault
labels:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault-prod
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: vault
component: server
ports:
- name: https
port: 8200
targetPort: 8200
protocol: TCP
---
apiVersion: v1
kind: Service
metadata:
name: vault-internal
namespace: vault
labels:
app.kubernetes.io/name: vault
spec:
type: ClusterIP
clusterIP: None
publishNotReadyAddresses: true
selector:
app.kubernetes.io/name: vault
component: server
ports:
- name: https
port: 8200
targetPort: 8200
- name: https-internal
port: 8201
targetPort: 8201
```
**왜 좋은가:**
- `vault-internal` headless + `publishNotReadyAddresses: true` → Raft peer가 unseal 전에도 서로 발견 가능
- **8201 포트 expose** → peer-to-peer Raft replication 성립 (누락 시 leader election 영구 실패)
- 사용자용 `vault` Service는 8200만 노출
---
## 나쁜 예시 2: 8201 누락
```yaml
spec:
ports:
- port: 8200
targetPort: 8200
```
**문제:**
- Raft peer가 8201로 서로 통신해야 하는데 Service가 expose하지 않음
- `vault operator raft list-peers`에서 follower가 리더로 못 붙음
- 증상: 단일 노드만 unsealed, 나머지는 "storage: IO error" 로그 루프
---
## 좋은 예시 3: Kubernetes auth bootstrap + role
```bash
# 1. Kubernetes auth method 활성화
vault auth enable kubernetes
# 2. Vault가 Kubernetes TokenReview API를 호출하기 위한 설정
# (Vault Pod 내부에서 실행하거나 reviewer SA의 JWT를 주입)
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://kubernetes.default.svc.cluster.local" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
disable_iss_validation=false
# 3. Policy 생성 (auth-server가 읽을 수 있는 경로만)
vault policy write auth-server-read - <<'EOF'
path "kv/data/auth-server/*" {
capabilities = ["read"]
}
path "database/creds/auth-server" {
capabilities = ["read"]
}
EOF
# 4. Role 생성 — 특정 SA + namespace에만 바인딩
vault write auth/kubernetes/role/auth-server \
bound_service_account_names=auth-server \
bound_service_account_namespaces=auth-prod \
policies=auth-server-read \
ttl=1h \
max_ttl=24h \
audience=vault
```
**왜 좋은가:**
- TokenReview JWT를 명시적으로 구성 → Vault가 SA 토큰 유효성 검증 가능
- Policy는 `kv/data/auth-server/*`, `database/creds/auth-server`만 허용 (최소 권한)
- Role은 `auth-prod` namespace의 `auth-server` SA에만 바인딩
- `audience=vault`로 projected token의 audience 검증 (token confusion 방어)
---
## 나쁜 예시 3: wildcard role
```bash
vault write auth/kubernetes/role/all-apps \
bound_service_account_names="*" \
bound_service_account_namespaces="*" \
policies=default \
ttl=720h
```
**문제:**
- 모든 namespace의 모든 SA가 로그인 가능 → 한 워크로드 침해가 전체 Vault 접근으로 확대
- TTL 30일은 token revocation window가 너무 김
- `default` policy가 넓으면 실질적인 접근 제어 상실
---
## 좋은 예시 4: VSO — 클러스터 수준 연결 + 앱 namespace auth
```yaml
---
apiVersion: v1
kind: Namespace
metadata:
name: vault-secrets-operator-system
---
# 1) 클러스터 전체 1개 VaultConnection
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultConnection
metadata:
name: default
namespace: vault-secrets-operator-system
spec:
address: https://vault.vault.svc.cluster.local:8200
tlsServerName: vault.vault.svc.cluster.local
caCertSecretRef: vault-ca
skipTLSVerify: false
timeout: 60s
---
# 2) 앱 namespace의 ServiceAccount
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server
namespace: auth-prod
---
# 3) 앱 namespace의 VaultAuth (Vault Kubernetes auth role로 로그인)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultAuth
metadata:
name: default
namespace: auth-prod
spec:
vaultConnectionRef: vault-secrets-operator-system/default
method: kubernetes
mount: kubernetes
kubernetes:
role: auth-server
serviceAccount: auth-server
audiences:
- vault
tokenExpirationSeconds: 600
```
**왜 좋은가:**
- `VaultConnection` 1개를 operator namespace에 두고, 앱 namespace에서 cross-reference
- `VaultAuth.method: kubernetes`가 Vault의 `auth/kubernetes/role/auth-server`를 호출
- `audiences: [vault]`로 projected SA token의 audience 바인딩
- `tokenExpirationSeconds: 600` → projected token 10분마다 rotate
---
## 좋은 예시 5: VSO — Static / Dynamic / PKI secret
```yaml
---
# KV v2에서 정적 secret 동기화
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultStaticSecret
metadata:
name: auth-server-config
namespace: auth-prod
spec:
vaultAuthRef: default
mount: kv
path: auth-server/config
type: kv-v2
refreshAfter: 30m
hmacSecretData: true
rolloutRestartTargets:
- kind: Deployment
name: auth-server
destination:
name: auth-server-config
create: true
overwrite: true
---
# Postgres dynamic credential (TTL 1h)
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultDynamicSecret
metadata:
name: auth-server-db
namespace: auth-prod
spec:
vaultAuthRef: default
mount: database
path: creds/auth-server
renewalPercent: 67
rolloutRestartTargets:
- kind: Deployment
name: auth-server
destination:
name: auth-server-db
create: true
overwrite: true
transformation:
excludeRaw: true
templates:
DATABASE_URL:
text: 'postgresql://{{ .Secrets.username }}:{{ .Secrets.password }}@auth-db-rw.auth-prod.svc.cluster.local:5432/authdb?sslmode=require'
---
# PKI 인증서 발급
apiVersion: secrets.hashicorp.com/v1beta1
kind: VaultPKISecret
metadata:
name: auth-server-cert
namespace: auth-prod
spec:
vaultAuthRef: default
mount: pki_int
role: auth-server
commonName: auth-server.auth-prod.svc.cluster.local
altNames:
- auth-server
- auth-server.auth-prod
ipSans: []
ttl: 720h
revoke: true
clear: true
expiryOffset: 120h
destination:
name: auth-server-cert
create: true
type: kubernetes.io/tls
```
**왜 좋은가:**
- 세 패턴(정적 KV, 동적 DB credential, PKI cert)을 한 namespace에서 일관되게 선언
- `rolloutRestartTargets`로 secret 갱신 시 consumer Deployment 자동 롤링 재시작
- `renewalPercent: 67` → TTL 67% 경과 시 갱신 (default는 보통 70%)
- `transformation.templates`로 연결 문자열 포맷 변환 (앱이 username/password 파싱 안 해도 됨)
---
## 좋은 예시 6: Vault Agent Injector (init-only 모드)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: legacy-app
namespace: legacy
spec:
replicas: 2
selector:
matchLabels:
app: legacy-app
template:
metadata:
labels:
app: legacy-app
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "legacy-app"
vault.hashicorp.com/agent-pre-populate-only: "true"
vault.hashicorp.com/agent-inject-secret-db.env: "database/creds/legacy-app"
vault.hashicorp.com/agent-inject-template-db.env: |
{{ with secret "database/creds/legacy-app" -}}
DATABASE_USERNAME={{ .Data.username }}
DATABASE_PASSWORD={{ .Data.password }}
{{- end }}
vault.hashicorp.com/agent-inject-file-db.env: "db.env"
vault.hashicorp.com/agent-limits-cpu: "200m"
vault.hashicorp.com/agent-limits-mem: "128Mi"
vault.hashicorp.com/agent-requests-cpu: "50m"
vault.hashicorp.com/agent-requests-mem: "64Mi"
spec:
serviceAccountName: legacy-app
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/legacy-app:1.4.2
command: ["sh", "-c", "source /vault/secrets/db.env && exec /app/run"]
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { memory: 256Mi }
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- name: tmp
emptyDir: {}
```
**왜 좋은가:**
- `agent-pre-populate-only: "true"` → init container만 돌고 sidecar 없음 → 2 pod 당 컨테이너 1개 절감
- etcd에 Kubernetes Secret 생성 없음 (annotation에 명시적 destination 없음; in-memory volume)
- template로 `.env` 포맷 렌더링 → legacy 앱이 그대로 소비
---
## 나쁜 예시 4: Injector + long-lived sidecar + 무한 renew
```yaml
annotations:
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "legacy-app"
vault.hashicorp.com/agent-inject-secret-creds: "database/creds/legacy-app"
# agent-pre-populate-only 없음 → sidecar 상시 실행
# agent-limits-* 없음 → sidecar가 limit 없이 메모리 증가
```
**문제:**
- sidecar가 Pod 수명 내내 상주 → 1000 서비스 x 3 replica = 3000 추가 컨테이너
- resource limit 미지정 → OOM cascading
- VSO로 대체 가능한데 Injector를 default로 쓰면 운영 복잡도 증가
---
## 좋은 예시 7: Raft snapshot CronJob
```yaml
---
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault-snapshot
namespace: vault
---
apiVersion: batch/v1
kind: CronJob
metadata:
name: vault-raft-snapshot
namespace: vault
spec:
schedule: "0 2 * * *"
concurrencyPolicy: Forbid
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 3
jobTemplate:
spec:
backoffLimit: 2
template:
spec:
restartPolicy: OnFailure
serviceAccountName: vault-snapshot
securityContext:
runAsNonRoot: true
runAsUser: 100
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: snapshot
image: hashicorp/vault:1.17.6
env:
- name: VAULT_ADDR
value: https://vault.vault.svc.cluster.local:8200
- name: VAULT_CACERT
value: /vault/tls/ca.crt
- name: VAULT_TOKEN
valueFrom:
secretKeyRef:
name: vault-snapshot-token
key: token
- name: AWS_REGION
value: ap-northeast-2
command:
- sh
- -c
- |
set -eu
TS=$(date -u +%Y%m%dT%H%M%SZ)
SNAP=/tmp/vault-${TS}.snap
vault operator raft snapshot save "${SNAP}"
aws s3 cp "${SNAP}" "s3://vault-backup.example.com/daily/vault-${TS}.snap" \
--sse aws:kms --sse-kms-key-id alias/vault-backup
rm -f "${SNAP}"
volumeMounts:
- name: vault-tls
mountPath: /vault/tls
readOnly: true
- name: tmp
mountPath: /tmp
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 512Mi
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: ["ALL"]
volumes:
- name: vault-tls
secret:
secretName: vault-tls
- name: tmp
emptyDir: {}
```
**왜 좋은가:**
- 매일 02:00 UTC `raft snapshot save` 실행
- 결과를 S3 SSE-KMS로 off-cluster 보관 (PVC와 독립적 failure domain)
- 짧은 TTL snapshot token을 별도 Secret로 주입 (root token 미사용)
- `concurrencyPolicy: Forbid`로 snapshot 중복 방지
---
## 좋은 예시 8: ServiceMonitor + Prometheus policy
```yaml
---
# Vault policy: Prometheus가 /v1/sys/metrics 읽기 전용
# (이 정책은 Vault 내부에 생성)
# vault policy write prometheus-metrics - <<EOF
# path "sys/metrics" { capabilities = ["read"] }
# EOF
---
apiVersion: monitoring.coreos.com/v1
kind: ServiceMonitor
metadata:
name: vault
namespace: vault
labels:
app.kubernetes.io/name: vault
release: kube-prometheus-stack
spec:
selector:
matchLabels:
app.kubernetes.io/name: vault
endpoints:
- port: https
scheme: https
path: /v1/sys/metrics
params:
format:
- prometheus
interval: 30s
scrapeTimeout: 10s
bearerTokenSecret:
name: prometheus-vault-token
key: token
tlsConfig:
ca:
secret:
name: vault-ca
key: ca.crt
serverName: vault.vault.svc.cluster.local
```
**왜 좋은가:**
- `telemetry { prometheus_retention_time = "24h" }` stanza와 매칭
- Prometheus가 전용 Vault token으로 `sys/metrics`만 read (최소 권한)
- TLS serverName 명시로 hostname 검증
---
## 나쁜 예시 5: Vault Ingress 외부 공개
```yaml
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: vault
spec:
rules:
- host: vault.example.com
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: vault
port:
number: 8200
```
**문제:**
- Vault는 일반 외부 서비스가 아님 — `/v1/auth/*`, `/v1/sys/*` 가 인터넷에 노출되면 brute-force / DoS 표면 확대
- root token / unseal key가 UI에서 한 번이라도 취급되면 공격 가치가 매우 큼
- 관리자 접근은 VPN / port-forward / OIDC 보호된 별도 bastion 경로로
---
## 좋은 예시 9: PodDisruptionBudget + Restricted securityContext
```yaml
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: vault
namespace: vault
spec:
minAvailable: 2
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app.kubernetes.io/name: vault
component: server
```
그리고 values.yaml에서:
```yaml
server:
statefulSet:
securityContext:
pod:
runAsNonRoot: true
runAsUser: 100
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
container:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
add:
- IPC_LOCK
```
**왜 좋은가:**
- Raft 3-node quorum 유지: `minAvailable: 2` → 한 번에 1 pod만 drain 가능
- `IPC_LOCK` capability는 Vault의 mlockall을 허용 (swap으로 secret 유출 방지) — 그 외 capability 전부 drop
- Restricted PSS 전체 충족
+991
View File
@@ -0,0 +1,991 @@
# workload selection 예시
모든 YAML은 `kubectl apply --server-side --dry-run=server` 통과. PodSecurity `restricted` 호환.
각 예시는 namespace 하나에 그대로 붙여 넣을 수 있는 self-contained 단위.
---
## 좋은 예시 1: auth-server Deployment (tier-1 prod, 완전 동반 리소스)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
automountServiceAccountToken: false
---
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/version: "1.24.3"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/slo-tier: tier-1
spec:
replicas: 6
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
template:
metadata:
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/slo-tier: tier-1
spec:
serviceAccountName: auth
automountServiceAccountToken: false
priorityClassName: tier-1-critical
terminationGracePeriodSeconds: 45
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
containers:
- name: auth
image: registry.example.com/auth@sha256:f1a2b3c4d5e6f7081920aabbccddeeff00112233445566778899aabbccddeeff
imagePullPolicy: IfNotPresent
ports:
- { name: http, containerPort: 8080, protocol: TCP }
- { name: management, containerPort: 8081, protocol: TCP }
resources:
requests: { cpu: 500m, memory: 1Gi }
limits: { cpu: "2", memory: 2Gi }
startupProbe:
httpGet: { path: /actuator/health/liveness, port: management }
periodSeconds: 5
failureThreshold: 30
livenessProbe:
httpGet: { path: /actuator/health/liveness, port: management }
periodSeconds: 10
timeoutSeconds: 3
failureThreshold: 3
readinessProbe:
httpGet: { path: /actuator/health/readiness, port: management }
periodSeconds: 5
timeoutSeconds: 2
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/bin/sh", "-c", "sleep 15"]
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ "ALL" ]
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- name: tmp
emptyDir: { sizeLimit: 64Mi }
---
apiVersion: v1
kind: Service
metadata:
name: auth
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
ports:
- { name: http, port: 8080, targetPort: http }
- { name: management, port: 8081, targetPort: management }
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: auth
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/slo-tier: tier-1
spec:
minAvailable: 50%
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
---
apiVersion: autoscaling/v2
kind: HorizontalPodAutoscaler
metadata:
name: auth
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: auth
app.kubernetes.io/instance: auth-prod
app.kubernetes.io/component: api
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
scaleTargetRef:
apiVersion: apps/v1
kind: Deployment
name: auth
minReplicas: 6
maxReplicas: 30
metrics:
- type: Resource
resource:
name: cpu
target: { type: Utilization, averageUtilization: 70 }
- type: Resource
resource:
name: memory
target: { type: Utilization, averageUtilization: 80 }
behavior:
scaleDown:
stabilizationWindowSeconds: 300
policies:
- { type: Percent, value: 25, periodSeconds: 60 }
scaleUp:
stabilizationWindowSeconds: 0
policies:
- { type: Percent, value: 100, periodSeconds: 30 }
- { type: Pods, value: 4, periodSeconds: 30 }
selectPolicy: Max
```
**왜 좋은가:**
- stateless 장기 실행 → Deployment 정답
- PDB + HPA + topologySpread (zone+hostname) 모두 tier-1에 맞게 동반
- digest pinning, restricted PodSecurity 호환, preStop sleep으로 graceful drain
- selector에는 불변 3종만 (version/environment 없음)
---
## 좋은 예시 2: PostgreSQL StatefulSet (operator 없는 fallback 케이스, 완전 schema)
```yaml
apiVersion: v1
kind: Service
metadata:
name: postgres-identity-headless
namespace: prod-data-postgres
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
clusterIP: None
selector:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
ports:
- { name: postgres, port: 5432, targetPort: postgres }
---
apiVersion: v1
kind: Service
metadata:
name: postgres-identity
namespace: prod-data-postgres
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
ports:
- { name: postgres, port: 5432, targetPort: postgres }
---
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: postgres-identity
namespace: prod-data-postgres
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/version: "16.3"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/slo-tier: tier-1
example.com/data-classification: confidential
spec:
serviceName: postgres-identity-headless
replicas: 3
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
revisionHistoryLimit: 5
selector:
matchLabels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
template:
metadata:
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
serviceAccountName: postgres-identity
automountServiceAccountToken: false
priorityClassName: tier-1-critical
terminationGracePeriodSeconds: 120
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
containers:
- name: postgres
image: registry.example.com/postgres@sha256:aabbccddeeff00112233445566778899aabbccddeeff0011223344556677abcd
imagePullPolicy: IfNotPresent
ports:
- { name: postgres, containerPort: 5432, protocol: TCP }
env:
- name: POSTGRES_DB
value: identity
- name: POSTGRES_USER
valueFrom:
secretKeyRef: { name: postgres-identity-creds, key: username }
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef: { name: postgres-identity-creds, key: password }
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
resources:
requests: { cpu: "2", memory: 4Gi }
limits: { cpu: "4", memory: 8Gi }
startupProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
periodSeconds: 5
failureThreshold: 60
livenessProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
exec:
command: ["pg_isready", "-U", "$(POSTGRES_USER)", "-d", "$(POSTGRES_DB)"]
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
securityContext:
runAsNonRoot: true
runAsUser: 999
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ "ALL" ]
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
- { name: tmp, mountPath: /tmp }
- { name: run, mountPath: /var/run/postgresql }
volumes:
- name: tmp
emptyDir: {}
- name: run
emptyDir: {}
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: postgres
app.kubernetes.io/instance: postgres-identity
app.kubernetes.io/component: database
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: longhorn-replicated
resources:
requests:
storage: 200Gi
```
**왜 좋은가:**
- headless Service + clusterIP Service 양쪽 선언 (peer discovery + 일반 client)
- `persistentVolumeClaimRetentionPolicy: {whenDeleted: Retain, whenScaled: Retain}` 명시 (GA 1.27)
- `podManagementPolicy: OrderedReady` + `updateStrategy.partition: 0` (canary 시 1씩)
- zone topologySpread `DoNotSchedule`로 강제 (DB는 AZ 분산이 강건성 핵심)
- Secret 외부 참조 (External Secrets로 관리 가정)
- StorageClass `longhorn-replicated` (local-path 금지)
**실전 주의**: 1000-서비스 스케일에서는 raw StatefulSet 대신 **CloudNativePG Operator** 사용을 강력 권장. 이 예시는 operator 불가 케이스의 reference.
---
## 좋은 예시 3: fluent-bit DaemonSet (로그 shipper)
```yaml
apiVersion: v1
kind: ServiceAccount
metadata:
name: fluent-bit
namespace: prod-platform-observability
labels:
app.kubernetes.io/name: fluent-bit
app.kubernetes.io/instance: fluent-bit-prod
app.kubernetes.io/component: log-shipper
app.kubernetes.io/part-of: observability-platform
app.kubernetes.io/managed-by: argocd
---
apiVersion: apps/v1
kind: DaemonSet
metadata:
name: fluent-bit
namespace: prod-platform-observability
labels:
app.kubernetes.io/name: fluent-bit
app.kubernetes.io/instance: fluent-bit-prod
app.kubernetes.io/version: "3.1.7"
app.kubernetes.io/component: log-shipper
app.kubernetes.io/part-of: observability-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
spec:
selector:
matchLabels:
app.kubernetes.io/name: fluent-bit
app.kubernetes.io/instance: fluent-bit-prod
app.kubernetes.io/component: log-shipper
updateStrategy:
type: RollingUpdate
rollingUpdate:
maxUnavailable: 10%
revisionHistoryLimit: 5
template:
metadata:
labels:
app.kubernetes.io/name: fluent-bit
app.kubernetes.io/instance: fluent-bit-prod
app.kubernetes.io/component: log-shipper
app.kubernetes.io/part-of: observability-platform
app.kubernetes.io/managed-by: argocd
spec:
serviceAccountName: fluent-bit
automountServiceAccountToken: true
priorityClassName: system-node-critical
hostNetwork: false
terminationGracePeriodSeconds: 30
tolerations:
- operator: Exists
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: fluent-bit
image: registry.example.com/fluent-bit@sha256:112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00
imagePullPolicy: IfNotPresent
ports:
- { name: metrics, containerPort: 2020, protocol: TCP }
resources:
requests: { cpu: 100m, memory: 128Mi }
limits: { cpu: 500m, memory: 256Mi }
livenessProbe:
httpGet: { path: /, port: metrics }
periodSeconds: 10
failureThreshold: 3
readinessProbe:
httpGet: { path: /api/v1/health, port: metrics }
periodSeconds: 5
failureThreshold: 3
securityContext:
runAsNonRoot: true
runAsUser: 10001
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ "ALL" ]
add: [ "DAC_READ_SEARCH" ]
volumeMounts:
- { name: varlog, mountPath: /var/log, readOnly: true }
- { name: varlibdockercontainers, mountPath: /var/lib/docker/containers, readOnly: true }
- { name: config, mountPath: /fluent-bit/etc }
volumes:
- name: varlog
hostPath: { path: /var/log, type: Directory }
- name: varlibdockercontainers
hostPath: { path: /var/lib/docker/containers, type: DirectoryOrCreate }
- name: config
configMap: { name: fluent-bit-config }
```
**왜 좋은가:**
- DaemonSet으로 모든 노드에 정확히 1 Pod
- `tolerations: Exists`로 control-plane taint 포함 모든 노드 커버
- `priorityClassName: system-node-critical`로 eviction 방지
- root 필요(hostPath 로그 읽기) 하지만 capabilities는 `DAC_READ_SEARCH`만 추가하고 나머지 drop
- `updateStrategy.rollingUpdate.maxUnavailable: 10%`로 대규모 클러스터 rolling 안정화
### ❌ Bad counterpart (같은 역할을 Deployment로)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fluent-bit
spec:
replicas: 10
```
**문제:** Deployment는 특정 노드에 Pod가 없을 수 있고, 같은 노드에 여러 Pod가 떠서 로그 중복 수집. DaemonSet만 "노드당 정확히 1"을 보장.
---
## 좋은 예시 4: flyway-migrate Job (ArgoCD PostSync hook)
```yaml
apiVersion: batch/v1
kind: Job
metadata:
name: flyway-migrate-identity-1-24-3
namespace: prod-identity-auth
labels:
app.kubernetes.io/name: flyway
app.kubernetes.io/instance: flyway-identity-1-24-3
app.kubernetes.io/version: "1.24.3"
app.kubernetes.io/component: schema-migration
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
annotations:
argocd.argoproj.io/hook: PostSync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
spec:
backoffLimit: 2
activeDeadlineSeconds: 600
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app.kubernetes.io/name: flyway
app.kubernetes.io/instance: flyway-identity-1-24-3
app.kubernetes.io/component: schema-migration
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
restartPolicy: Never
serviceAccountName: flyway-identity
automountServiceAccountToken: false
priorityClassName: tier-1-critical
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
seccompProfile:
type: RuntimeDefault
containers:
- name: flyway
image: registry.example.com/flyway@sha256:ccddeeff0011223344556677889900aabbccddeeff0011223344556677889900
imagePullPolicy: IfNotPresent
args: [ "migrate" ]
env:
- { name: FLYWAY_URL, valueFrom: { secretKeyRef: { name: flyway-identity-creds, key: url } } }
- { name: FLYWAY_USER, valueFrom: { secretKeyRef: { name: flyway-identity-creds, key: username } } }
- { name: FLYWAY_PASSWORD, valueFrom: { secretKeyRef: { name: flyway-identity-creds, key: password } } }
resources:
requests: { cpu: 200m, memory: 256Mi }
limits: { cpu: "1", memory: 512Mi }
securityContext:
runAsNonRoot: true
runAsUser: 1000
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ "ALL" ]
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- name: tmp
emptyDir: { sizeLimit: 32Mi }
```
**왜 좋은가:**
- `restartPolicy: Never` + `backoffLimit: 2` → migration 실패는 debug 가능하게 노출
- `activeDeadlineSeconds: 600` → 무한 lock 방지
- `ttlSecondsAfterFinished: 86400` → 24시간 후 자동 정리 (1000-서비스 스케일 필수)
- ArgoCD `PostSync` hook으로 Deployment rollout 이후 실행
- 이름에 버전 suffix (`-1-24-3`) → 같은 이름 Job 재생성 충돌 방지
---
## 좋은 예시 5: postgres-backup CronJob (timezone + concurrencyPolicy)
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: postgres-identity-backup
namespace: prod-data-postgres
labels:
app.kubernetes.io/name: postgres-backup
app.kubernetes.io/instance: postgres-identity-backup
app.kubernetes.io/component: backup
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
spec:
schedule: "0 */6 * * *"
timeZone: "Asia/Seoul"
concurrencyPolicy: Forbid
startingDeadlineSeconds: 600
successfulJobsHistoryLimit: 3
failedJobsHistoryLimit: 5
jobTemplate:
spec:
backoffLimit: 1
activeDeadlineSeconds: 3600
template:
metadata:
labels:
app.kubernetes.io/name: postgres-backup
app.kubernetes.io/instance: postgres-identity-backup
app.kubernetes.io/component: backup
app.kubernetes.io/part-of: identity-platform
app.kubernetes.io/managed-by: argocd
spec:
restartPolicy: Never
serviceAccountName: postgres-identity-backup
automountServiceAccountToken: false
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
seccompProfile:
type: RuntimeDefault
containers:
- name: pgbackup
image: registry.example.com/pgbackup@sha256:ddeeff00112233445566778899aabbccddeeff00112233445566778899aabbcc
imagePullPolicy: IfNotPresent
env:
- { name: PGHOST, value: postgres-identity.prod-data-postgres.svc.cluster.local }
- { name: PGUSER, valueFrom: { secretKeyRef: { name: pgbackup-creds, key: username } } }
- { name: PGPASSWORD, valueFrom: { secretKeyRef: { name: pgbackup-creds, key: password } } }
- { name: S3_BUCKET, value: pg-backups-prod }
- { name: S3_PREFIX, value: identity }
resources:
requests: { cpu: 200m, memory: 256Mi }
limits: { cpu: "1", memory: 512Mi }
securityContext:
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ "ALL" ]
volumeMounts:
- { name: tmp, mountPath: /tmp }
volumes:
- name: tmp
emptyDir: { sizeLimit: 2Gi }
```
**왜 좋은가:**
- `timeZone: Asia/Seoul` (v1.25+) → DST 안정
- `concurrencyPolicy: Forbid` → 이전 백업이 돌고 있으면 skip
- `startingDeadlineSeconds: 600` → 노드 장애 후 미싱 누적 제한
- history limit으로 완료 Job 정리 (`successfulJobsHistoryLimit: 3`, `failedJobsHistoryLimit: 5`)
- CronJob의 jobTemplate에는 `ttlSecondsAfterFinished`를 설정하지 않음 — history limit과 중복/충돌 방지 (standalone Job에서만 사용)
**실전 주의**: CloudNativePG `ScheduledBackup` CRD를 쓰면 이 CronJob을 operator가 대체한다.
---
## 좋은 예시 6: Ingress controller Deployment + MetalLB (prod 기본)
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: ingress-nginx-public
namespace: prod-platform-ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx-public-prod
app.kubernetes.io/version: "1.11.2"
app.kubernetes.io/component: controller
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
example.com/environment: prod
example.com/exposure: public
example.com/slo-tier: tier-1
spec:
replicas: 3
revisionHistoryLimit: 5
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 1
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx-public-prod
app.kubernetes.io/component: controller
template:
metadata:
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx-public-prod
app.kubernetes.io/component: controller
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
spec:
serviceAccountName: ingress-nginx
automountServiceAccountToken: true
priorityClassName: system-cluster-critical
terminationGracePeriodSeconds: 300
securityContext:
runAsNonRoot: true
runAsUser: 101
fsGroup: 101
seccompProfile: { type: RuntimeDefault }
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx-public-prod
app.kubernetes.io/component: controller
containers:
- name: controller
image: registry.example.com/ingress-nginx@sha256:eeff00112233445566778899aabbccddeeff00112233445566778899aabbccdd
args:
- /nginx-ingress-controller
- --publish-service=$(POD_NAMESPACE)/ingress-nginx-public
- --election-id=ingress-nginx-public-leader
- --controller-class=k8s.io/ingress-nginx-public
- --ingress-class=nginx-public
- --configmap=$(POD_NAMESPACE)/ingress-nginx-public
env:
- { name: POD_NAMESPACE, valueFrom: { fieldRef: { fieldPath: metadata.namespace } } }
- { name: POD_NAME, valueFrom: { fieldRef: { fieldPath: metadata.name } } }
ports:
- { name: http, containerPort: 80, protocol: TCP }
- { name: https, containerPort: 443, protocol: TCP }
- { name: metrics, containerPort: 10254, protocol: TCP }
resources:
requests: { cpu: 500m, memory: 512Mi }
limits: { cpu: "2", memory: 1Gi }
livenessProbe:
httpGet: { path: /healthz, port: 10254, scheme: HTTP }
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 5
readinessProbe:
httpGet: { path: /healthz, port: 10254, scheme: HTTP }
periodSeconds: 5
timeoutSeconds: 3
failureThreshold: 3
lifecycle:
preStop:
exec:
command: ["/wait-shutdown"]
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
runAsNonRoot: true
runAsUser: 101
capabilities:
drop: [ "ALL" ]
add: [ "NET_BIND_SERVICE" ]
volumeMounts:
- { name: tmp, mountPath: /tmp }
- { name: nginx-etc, mountPath: /etc/nginx }
- { name: nginx-cache, mountPath: /var/cache/nginx }
volumes:
- name: tmp
emptyDir: {}
- name: nginx-etc
emptyDir: {}
- name: nginx-cache
emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
name: ingress-nginx-public
namespace: prod-platform-ingress-nginx
labels:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx-public-prod
app.kubernetes.io/component: controller
app.kubernetes.io/part-of: platform
app.kubernetes.io/managed-by: argocd
annotations:
metallb.universe.tf/address-pool: prod-public-pool
metallb.universe.tf/allow-shared-ip: "ingress-nginx-public"
spec:
type: LoadBalancer
externalTrafficPolicy: Local
selector:
app.kubernetes.io/name: ingress-nginx
app.kubernetes.io/instance: ingress-nginx-public-prod
app.kubernetes.io/component: controller
ports:
- { name: http, port: 80, targetPort: http, protocol: TCP }
- { name: https, port: 443, targetPort: https, protocol: TCP }
```
**왜 좋은가:**
- Deployment + LoadBalancer (MetalLB L2/BGP) → HPA 가능, 노드 수 ≠ replica 수
- `externalTrafficPolicy: Local` → source IP 보존 + 노드 horn-in 회피
- `priorityClassName: system-cluster-critical`
- `NET_BIND_SERVICE` capability만 추가 (80/443 바인딩), 나머지 drop
- public / internal IngressClass 분리 가능 (별도 Deployment)
**DaemonSet 선택이 맞는 케이스**: bare-metal + 외부 LB 없음 + 모든 edge 노드가 고정 IP로 80/443 직접 노출. 이 경우 `hostNetwork: true` + DaemonSet + `tolerations`로 edge 노드만 label selector.
---
## 나쁜 예시 1: auth-server를 StatefulSet으로
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: auth-server
spec:
serviceName: auth-server
replicas: 3
selector:
matchLabels: { app: auth-server }
template:
metadata: { labels: { app: auth-server } }
spec:
containers:
- name: auth
image: registry.example.com/auth:1.24.3
```
**문제:** stable identity/storage 요구가 없는 stateless 앱에 StatefulSet. rolling update가 OrderedReady로 느려지고, replica 증설 시 `auth-server-2`, `auth-server-3` 이름이 의미 없이 고정된다. 해결: Deployment + HPA.
---
## 나쁜 예시 2: postgres를 Deployment로
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: postgres
spec:
replicas: 1
template:
spec:
containers:
- name: postgres
image: postgres:16
volumeMounts:
- { name: data, mountPath: /var/lib/postgresql/data }
volumes:
- name: data
persistentVolumeClaim: { claimName: postgres-pvc }
```
**문제:** replica=1 Deployment + 단일 PVC는 rolling update 시 잠깐 새 Pod가 뜨면서 같은 PVC에 두 Pod가 붙으려다 RWO 충돌. StatefulSet이면 `OrderedReady`로 이전 Pod 완전히 내려간 다음 새 Pod가 뜬다. 해결: StatefulSet + volumeClaimTemplates + podManagementPolicy: OrderedReady.
---
## 나쁜 예시 3: fluent-bit을 Deployment로 배포
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: fluent-bit
spec:
replicas: 10
template:
spec:
containers:
- name: fluent-bit
image: fluent/fluent-bit:latest
volumeMounts:
- { name: varlog, mountPath: /var/log }
volumes:
- name: varlog
hostPath: { path: /var/log }
```
**문제:** Deployment는 Pod 배치를 스케줄러에 맡김 → 어떤 노드에는 0 Pod(로그 유실), 어떤 노드에는 2 Pod(중복 수집). 노드 수가 바뀌면 수동으로 replicas 조정. 해결: DaemonSet.
---
## 나쁜 예시 4: flyway를 앱 startup에 포함
```yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth
spec:
template:
spec:
initContainers:
- name: flyway-migrate
image: registry.example.com/flyway:1.24.3
args: ["migrate"]
containers:
- name: auth
image: registry.example.com/auth:1.24.3
```
**문제:** Deployment scale-up 때마다 모든 새 Pod가 migration 시도 → DB lock 경합. migration 실패가 앱 부팅 실패로 섞여 debug 불가. rollback 시 downgrade migration 제어 불가. 해결: 독립 Job + ArgoCD PostSync hook.
---
## 나쁜 예시 5: CronJob에 `concurrencyPolicy`와 `startingDeadlineSeconds` 누락
```yaml
apiVersion: batch/v1
kind: CronJob
metadata:
name: backup
spec:
schedule: "*/5 * * * *"
jobTemplate:
spec:
template:
spec:
restartPolicy: OnFailure
containers:
- name: backup
image: registry.example.com/backup:1.0.0
```
**문제:**
1. `concurrencyPolicy` 미지정 → 기본 `Allow` → 이전 backup이 오래 걸리면 중복 실행, PVC lock 충돌
2. `startingDeadlineSeconds` 미지정 → 노드 장애 후 수십 개 missed Job이 한꺼번에 생성
3. history limit 미지정 → 완료 Job이 무한 누적
해결: 모든 prod CronJob에 `concurrencyPolicy: Forbid` + `startingDeadlineSeconds: <short>` + history limit.
---
## 나쁜 예시 6: StatefulSet에 `persistentVolumeClaimRetentionPolicy` 미지정 (prod)
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: vault
spec:
serviceName: vault
replicas: 3
# persistentVolumeClaimRetentionPolicy missing
volumeClaimTemplates:
- metadata: { name: data }
spec:
accessModes: [ "ReadWriteOnce" ]
storageClassName: longhorn-replicated
resources:
requests: { storage: 20Gi }
```
**문제:** 명시가 없으면 기본값 (`{whenDeleted: Retain, whenScaled: Retain}`)이 적용되어 "동작은 맞지만" 의도가 코드에 드러나지 않는다. 팀원이 `{Delete, Delete}`인지 추측. 1000-서비스 스케일에서는 모든 StatefulSet이 이 필드를 **명시**해야 정책이 audit 가능. 해결: 항상 명시.
+248
View File
@@ -0,0 +1,248 @@
# Ingress / Traefik 운영
dev 환경은 K3s packaged Traefik 을 그대로 유지한다. 단 **`/var/lib/rancher/k3s/server/manifests/traefik.yaml` 는 수정하지 않는다.** 운영 설정은 `k8s/overlays/dev/platform/traefik/``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 관리 |
## 설계 원칙
- 앱은 `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 환경에서 직접 접속하려면 운영자가 아래를 별도로 맞춰야 한다.
```text
<ingress-ip> project.com
<ingress-ip> keycloak.dev.example.com
```
예: Traefik `LoadBalancer` IP 중 하나가 `10.208.141.123` 이면 로컬 `/etc/hosts` 에 두 host 를 추가한다. dev overlay 는 현재 외부 ACME 발급 대신 `dev-selfsigned` ClusterIssuer 를 사용하므로 브라우저에서는 인증서 경고를 허용하거나 해당 인증서를 로컬 trust store 에 등록해야 한다. 공인 DNS 가 Traefik 진입점으로 향하고 ACME 인증서가 Ready 가 되면 이 임시 조치는 제거한다.
Chrome 에서 계속 실패하면 먼저 boundary 를 나눈다.
| 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 가 없다는 뜻이다. |
### 브라우저 검증 순서
dev ForwardAuth 를 브라우저에서 직접 확인할 때는 아래 순서로 진행한다. 중간 단계를 건너뛰면 "Chrome 이 안 된다" 만 보이고 어느 boundary 가 깨졌는지 알기 어렵다.
#### 1. Traefik 진입 IP 확인
```bash
kubectl -n kube-system get svc traefik \
-o jsonpath='{.status.loadBalancer.ingress[*].ip}{"\n"}'
```
예상 예시:
```text
10.208.141.123 10.208.141.14
```
이 문서의 예시는 `10.208.141.123` 을 사용한다. 실제 클러스터에서 나온 IP 중 하나를 선택한다.
#### 2. curl 로 클러스터 경로 먼저 확인
브라우저를 열기 전에 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) 섹션을 참고.
+31
View File
@@ -0,0 +1,31 @@
# NetworkPolicy 매트릭스
단일 namespace(`mnt`) 내부에서도 서비스 간 트래픽을 **최소권한** 으로 제한한다. baseline 은 모든 Pod 의 ingress/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) |
## 작성 규칙
- 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 으로 시작하지 않도록).
## 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` 에 함께 들어 있다.
+83
View File
@@ -0,0 +1,83 @@
# 운영 / 검증
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) 참고.
```bash
# dev — 비밀번호를 프롬프트에서 무음 입력 (bash history 에 안 남음)
bash k8s/scripts/bin/bootstrap.sh dev
# teardown — 대화형 y/N
bash k8s/scripts/bin/teardown.sh dev
```
## 스크립트 구조
`k8s/scripts/``bin / ci / lib / tasks` 4 축:
| 디렉토리 | 역할 |
|---|---|
| `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` |
모든 쉘 스크립트는 `set -Eeuo pipefail` + `IFS=$'\n\t'` + `trap_cleanup` 으로 공통 에러 처리. root token / registry BasicAuth 같은 민감 값은 **stdin 파이프** 로만 전달하고 stdout 에 찍지 않는다.
## 검증 (validate.sh)
```bash
bash k8s/scripts/ci/validate.sh
```
3 단계:
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
```
## 환경별 배포
현재 `dev` overlay 만 완성. `staging` / `prod` 는 의도적으로 비어 있고 추후 확장 예정. validate.sh 는 `kustomization.yaml` 이 없는 환경을 자동 스킵한다 — 빈 overlay 가 CI 를 빨갛게 만들지 않기 위함.
### 계획된 환경별 차등
| 리소스 | 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 |
prod 승격 시 필수 작업:
- Vault storage `file``raft` + KMS auto-unseal
- Postgres backup CronJob (Velero / pgBackRest)
- cert-manager ClusterIssuer 로 TLS 전환
- 이미지 tag → digest pin
+256
View File
@@ -0,0 +1,256 @@
# Security Hardening
운영 절차가 아닌 **정책 / 거버넌스** 영역. guide.md 가 *"오늘 oncall 이 따라할 절차"* 라면 이 문서는 *"이 클러스터를 책임진다면 알아야 할 보안 결정"* 이다.
## 목차
1. [etcd encryption at rest](#1-etcd-encryption-at-rest)
2. [Vault 운영자 토큰 관리](#2-vault-운영자-토큰-관리)
3. [bash history 에 비밀번호 남기지 않기](#3-bash-history-에-비밀번호-남기지-않기)
---
## 1. etcd encryption at rest
VSO 가 만드는 K8s Secret 은 기본적으로 **etcd 에 base64 로만 저장된다 (평문과 동일)**. 운영 전 반드시 암호화를 켠다.
### K3s 방식 (권장 — 학습/소규모)
최초 설치 시:
```bash
# /etc/rancher/k3s/config.yaml
write-kubeconfig-mode: "0644"
secrets-encryption: true
# 또는 설치 커맨드
curl -sfL https://get.k3s.io | sh -s - server --secrets-encryption
```
K3s 가 AES-CBC 키를 자동 생성해서 `/var/lib/rancher/k3s/server/cred/encryption-config.json` 에 저장한다.
이미 돌고 있는 클러스터에서 켜는 경우:
```bash
sudo vim /etc/rancher/k3s/config.yaml # secrets-encryption: true 추가
sudo systemctl restart k3s
# 기존 Secret 을 즉시 재암호화 (없으면 새 Secret 부터 적용)
sudo k3s secrets-encrypt prepare
sudo systemctl restart k3s
sudo k3s secrets-encrypt rotate
sudo systemctl restart k3s
sudo k3s secrets-encrypt reencrypt
```
확인:
```bash
sudo k3s secrets-encrypt status
# Encryption Status: Enabled
# Current Rotation Stage: start
# Server Encryption Hashes: All hashes match
```
### 표준 Kubernetes 방식 (kubeadm 등)
1. `/etc/kubernetes/encryption.yaml` 생성:
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- aescbc:
keys:
- name: key-2026-04-21
secret: <head -c 32 /dev/urandom | base64>
- identity: {}
```
2. kube-apiserver manifest 에 플래그 추가:
```yaml
spec:
containers:
- command:
- kube-apiserver
- --encryption-provider-config=/etc/kubernetes/encryption.yaml
volumeMounts:
- name: encryption-config
mountPath: /etc/kubernetes/encryption.yaml
readOnly: true
```
3. 기존 Secret 재암호화:
```bash
kubectl get secrets --all-namespaces -o json \
| kubectl replace -f -
```
### KMS provider (프로덕션 권장)
`aescbc` 대신 KMS plugin (Vault transit / AWS KMS / GCP KMS) 사용. circular dependency (Vault 도 K8s Secret 에 의존) 회피를 위해 Vault transit 은 **별도 provider Vault** 를 띄워야 한다. 현 프로젝트는 단일 Vault 이므로 KMS 는 추후 작업.
### 검증
Secret 이 암호화됐는지 확인:
```bash
# K3s
sudo k3s kubectl -n mnt get secret auth-server-db -o yaml \
| grep -A1 "data:"
# etcd 에 직접 접근해서 암호화 확인 (K3s)
sudo ETCDCTL_API=3 etcdctl \
--endpoints=https://127.0.0.1:2379 \
--cacert=/var/lib/rancher/k3s/server/tls/etcd/server-ca.crt \
--cert=/var/lib/rancher/k3s/server/tls/etcd/client.crt \
--key=/var/lib/rancher/k3s/server/tls/etcd/client.key \
get /registry/secrets/mnt/auth-server-db
# 출력이 'k8s:enc:aescbc:v1:...' 로 시작하면 암호화됨
```
---
## 2. Vault 운영자 토큰 관리
root token 은 **비상시 (rekey / generate-root / 전체 복구) 전용**으로만 사용한다. 평시 작업은 개인별 계정 + `vault-admin` policy 로 수행한다.
### 초기 설정
부트스트랩 완료 후 한 번만:
```bash
REPO_ROOT="$(pwd)" \
VAULT_ADMIN_USERNAME='alice' \
VAULT_ADMIN_PASSWORD='<초기 비밀번호>' \
bash k8s/scripts/tasks/vault-setup-admin.sh
```
이 스크립트가 수행:
- `userpass` auth method 활성화 (idempotent)
- `vault-admin` policy 작성 (`secret/*`, `auth/*`, `sys/mounts/*`, `sys/audit/*` 등 관리 권한)
- `${VAULT_ADMIN_USERNAME}` 계정 생성 (token TTL 기본 8h / max 24h)
### 운영자 로그인 — root token 사용 중단
```bash
# port-forward 로 vault CLI 접근
kubectl -n mnt port-forward svc/vault 8200:8200 &
export VAULT_ADDR=http://127.0.0.1:8200
# 로그인 (로그인 시 발급되는 토큰은 8h 후 자동 만료)
vault login -method=userpass username=alice
# Password (will be hidden): <입력>
# Token is displayed and automatically cached in ~/.vault-token
```
첫 로그인 후 비밀번호 변경:
```bash
vault write auth/userpass/users/alice/password password='<새 비밀번호>'
```
### root token 처리
`vault-init-keys.json` 에 있는 root token 은:
1. **즉시 오프라인 금고 (1Password Team / 하드웨어 보안 금고 / 봉인 봉투) 로 이동**
2. 원본 파일에서 `root_token` 필드 삭제 (unseal keys 는 재부팅 시 필요하므로 유지)
3. root token 이 필요하면:
```bash
# 기존 root token 유효하면 재사용
vault login <root-token>
# 분실/만료됐으면 재발급 (unseal key 쿼럼 필요)
vault operator generate-root -init
# 응답의 nonce 저장, unseal key 보유자들이 otp 로 제출
vault operator generate-root -nonce=<nonce> -otp=<your-otp> <unseal-key-1>
vault operator generate-root -nonce=<nonce> -otp=<your-otp> <unseal-key-2>
vault operator generate-root -nonce=<nonce> -otp=<your-otp> <unseal-key-3>
# 마지막 응답에 Encoded Token 이 나옴 → otp 로 decode
vault operator generate-root -decode=<encoded> -otp=<your-otp>
# 새 root token 확보 후 기존 것 revoke:
vault token revoke <old-root-token>
```
### 운영자 계정 추가 / 제거
```bash
# 추가
REPO_ROOT="$(pwd)" \
VAULT_ADMIN_USERNAME='bob' \
VAULT_ADMIN_PASSWORD='<임시 pw>' \
bash k8s/scripts/tasks/vault-setup-admin.sh
# 제거
vault delete auth/userpass/users/bob
```
### 권한 분리 (추후 확장)
`vault-admin` 은 전권 정책이다. 실무에서는 역할별 분리 권장:
| 역할 | policy 이름 | 권한 범위 |
|---|---|---|
| 인프라 admin | `vault-admin` | 현재 정의된 전권 (rekey 제외) |
| 앱 팀 (read) | `secret-readonly` | `secret/data/*` read 전용 |
| 앱 팀 (write) | `secret-writer` | 팀별 경로 제한 (`secret/data/auth-server/*` 등) |
| 감사자 | `audit-reader` | `sys/audit/*` read + Vault audit log 접근 |
각 policy 를 만들고 userpass user 생성 시 `token_policies=<policy-name>` 로 바인딩한다.
### 감사 로그 활성화 (추후)
Vault 자체 감사 로그는 기본 비활성. 운영에서는 반드시 활성화:
```bash
# file 방식
vault audit enable file file_path=/vault/logs/audit.log
# socket 방식 (중앙집중 수집)
vault audit enable socket address=loki-syslog.monitoring.svc:514 socket_type=tcp
```
`statefulset.yaml` 의 volumeMounts 에 `/vault/logs` 를 추가해야 파일 방식 사용 가능. 별도 작업.
---
## 3. bash history 에 비밀번호 남기지 않기
`VAR=value command` 형태로 env var 를 명령줄에 직접 적으면 **그대로 `~/.bash_history` 에 저장** 된다. 대응:
### 권장 — 대화형 입력
```bash
bash k8s/scripts/bin/bootstrap.sh dev
# 프롬프트에서 무음 입력 (echo 안 됨)
```
본 프로젝트의 모든 시크릿 seed 스크립트 (`bootstrap.sh`, `tasks/vault-seed-apps.sh`, `tasks/vault-setup-admin.sh`) 는 env var 가 비어 있으면 TTY 에서 자동으로 `read -r -s` 프롬프트로 전환한다.
### 비대화 (CI) 실행 시
어쩔 수 없이 env var 를 넣어야 할 때:
```bash
# 이번 명령만 history 에 안 남기기
HISTFILE=/dev/null \
POSTGRES_SUPERUSER_PASSWORD='...' \
KEYCLOAK_DB_PASSWORD='...' \
AUTH_SERVER_DB_PASSWORD='...' \
KEYCLOAK_ADMIN_PASSWORD='...' \
MINIO_ROOT_PASSWORD='...' \
bash k8s/scripts/bin/bootstrap.sh dev
# 또는 세션 전체 history 비활성화
set +o history
POSTGRES_SUPERUSER_PASSWORD='...' bash ...
set -o history
```
> **Tip**: `HISTCONTROL=ignorespace` 가 설정된 쉘이면 **명령 앞에 공백 1 칸** 넣어도 저장되지 않는다. 다만 쉘마다 설정이 다르니 `HISTFILE=/dev/null` 이 가장 확실.
+275
View File
@@ -0,0 +1,275 @@
# STYLE.md — 인프라 문서 공용 규약 (Single Source of Truth)
이 문서는 `docs/standards/infra/**``docs/examples/infra/**` 에 등장하는 모든 라벨, 네이밍, 포트, 이미지, 리소스 관례의 **정규(normative)** 정의다. 다른 모든 문서의 YAML 조각은 예시이며, 여기 규약과 충돌할 경우 **이 문서가 우선한다.** AI 에이전트가 매니페스트를 생성할 때 관례가 문서 간 표류하는 것을 방지하려는 목적이다.
---
## 1. 라벨 (Labels)
### 1.1 Kubernetes well-known labels (`app.kubernetes.io/*`)
공식 well-known set. 이 namespace 아래에는 아래 6개 외에 임의 키를 **추가하지 않는다.**
| 키 | 의미 | 예시 |
| --- | --- | --- |
| `app.kubernetes.io/name` | 애플리케이션 이름 | `auth-server` |
| `app.kubernetes.io/instance` | 인스턴스 (환경/리전 포함 가능) | `auth-server-prod`, `auth-server` |
| `app.kubernetes.io/version` | semver 또는 release tag | `1.24.0` |
| `app.kubernetes.io/component` | 역할 | `api`, `worker`, `migration`, `database` |
| `app.kubernetes.io/part-of` | 상위 시스템 | `auth-platform` |
| `app.kubernetes.io/managed-by` | 배포 도구 | `kustomize`, `argocd`, `helm` |
### 1.2 조직 커스텀 라벨 (`example.com/*`)
`example.com/` namespace 는 문서 전용 플레이스홀더다. 실제 조직은 자사 도메인(e.g., `acme.corp/`)으로 치환한다.
| 키 | 허용 값 |
| --- | --- |
| `example.com/environment` | `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` |
| `example.com/tier` | `0` (critical) \| `1` \| `2` \| `3` (best-effort) |
### 1.3 규칙
1. 모든 워크로드(Deployment / StatefulSet / DaemonSet / Job / CronJob)에는 위 6개 `app.kubernetes.io/*` 라벨 + `example.com/environment` + `example.com/owner-team` 이 **필수**다.
2. `app.kubernetes.io/environment` 라벨은 **사용 금지**. 공식 well-known set 에 없으며, 환경 라벨은 조직 namespace 아래에 둔다.
3. Selector (`spec.selector.matchLabels`)에는 **`app.kubernetes.io/name``app.kubernetes.io/instance` 만** 사용한다. 이유: selector 는 immutable 이고, `version` / `component` 이외 라벨은 릴리즈마다 바뀌기 때문에 selector 에 포함하면 rollout 이 막힌다.
4. 라벨 값은 DNS-1123 subdomain 또는 label 규칙을 따른다: 소문자 알파벳, 숫자, `-`, `.`, 최대 63자. 공백/대문자/언더스코어 금지.
5. 라벨은 metadata 의 최상위 `labels:` 와 Pod template 의 `spec.template.metadata.labels:`**동일하게** 복제한다(선택자 일치 보장).
```yaml
metadata:
name: auth-server
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
app.kubernetes.io/version: "1.24.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
example.com/environment: prod
example.com/owner-team: auth-platform
spec:
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
```
---
## 2. 네이밍 (Naming)
### 2.1 Namespace
1. 기본 스키마: `<env>-<domain>-<service>` — 예: `prod-auth-keycloak`, `staging-billing-api`.
2. 단일 서비스 네임스페이스에 여러 컴포넌트가 있으면 서비스 이름까지만 사용한다: `prod-auth` 네임스페이스 안에 Keycloak, PostgreSQL, Flyway Job 이 공존.
3. `default` 네임스페이스는 **금지**. `kube-*` 는 Kubernetes 예약.
4. 클러스터 공통 플랫폼 컴포넌트는 별도 접두어: `platform-vault`, `platform-cert-manager`, `platform-monitoring`.
### 2.2 리소스 이름
케밥-케이스, 소문자. Service / ServiceAccount / Secret / ConfigMap 이름은 관련 워크로드 이름을 접두어로 공유한다.
| 리소스 | 규약 | 예시 |
| --- | --- | --- |
| Deployment / StatefulSet | `<app>` | `auth-server` |
| Service (ClusterIP) | `<app>` (Deployment와 동일) | `auth-server` |
| Headless Service (StatefulSet peer 통신용) | `<app>-headless` (옆에 일반 ClusterIP `<app>` 병행) | `keycloak-headless`, `keycloak` |
| ServiceAccount | `<app>-sa` | `auth-server-sa` |
| Secret (앱 소유) | `<app>-<purpose>` | `auth-server-db`, `auth-server-oidc` |
| ConfigMap | `<app>-<purpose>` | `auth-server-config`, `auth-server-runtime` |
| PDB | `<app>-pdb` | `auth-server-pdb` |
| HPA | `<app>-hpa` | `auth-server-hpa` |
| NetworkPolicy | `<app>-<direction>-<peer>` | `auth-server-egress-db`, `auth-server-ingress-traefik` |
| Job (일회성) | `<app>-<action>-<timestamp-or-version>` | `auth-server-migrate-1-24-0` |
| CronJob | `<app>-<action>` | `auth-server-session-cleanup` |
---
## 3. 포트 (Ports)
### 3.1 이름 규약
모든 containerPort / servicePort 에는 `name` 필드가 **필수**다. 아래 이름은 예약어로 취급한다.
| name | 용도 | 관행 포트 |
| --- | --- | --- |
| `http` | HTTP 앱 트래픽 | 8080 |
| `https` | HTTPS 직접 종료 | 8443 |
| `grpc` | gRPC | 9090 또는 앱별 지정 |
| `metrics` | Prometheus scrape | 9090 (kube-prometheus 관행). 컴포넌트가 이미 9090 을 쓰면 9100 |
| `health` | 별도 헬스/관리 포트 | Keycloak Quarkus 관리 포트 9000 등 |
| `admin` | 관리 UI | 컴포넌트별 |
| `cluster` | 내부 peer / 레플리케이션 | Vault 8201, Postgres 5432, etcd 2380 |
### 3.2 규칙
1. `targetPort` 는 number 대신 **이름 참조**를 권장: `targetPort: http`. 이유: 컨테이너가 바인드 포트를 바꿔도 Service 쪽 조정이 필요 없다.
2. `metrics` 포트는 **외부 노출 금지**. ClusterIP 만 쓰며 NetworkPolicy 로 Prometheus 네임스페이스에서만 ingress 허용.
3. `health`, `admin` 포트는 Ingress 에 붙이지 않는다. NetworkPolicy 로 접근 대역을 제한한다.
Container ports 스탠자 (Deployment/Pod spec 내부):
```
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 9090
protocol: TCP
- name: health
containerPort: 9000
protocol: TCP
```
Service 정의 (targetPort 는 이름 참조):
```yaml
apiVersion: v1
kind: Service
metadata:
name: auth-server
spec:
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
ports:
- name: http
port: 80
targetPort: http
```
---
## 4. 이미지 (Images)
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 이슈).
4. `imagePullPolicy`:
- digest 사용 시 `IfNotPresent` (이미지 콘텐츠는 immutable)
- 뮤터블 태그 사용 시 `Always`
5. 레지스트리: 조직 내부 미러가 우선한다. 예: `registry.example.com/<ns>/<app>`. Docker Hub 직접 pull 금지 (rate limit + 공급망 리스크).
6. SHA256 digest 로 pin 한 이미지는 CI 파이프라인에서 cosign 서명 검증(선택)과 `imagePullSecrets` digest 검증에 연결한다.
```yaml
containers:
- name: app
image: registry.example.com/auth-platform/auth-server@sha256:9f0b2c4d8e7a1b3c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c
imagePullPolicy: IfNotPresent
```
---
## 5. 리소스 (Resources)
### 5.1 필수 필드
1. 모든 컨테이너는 `resources.requests.cpu`, `resources.requests.memory`, `resources.limits.memory`**반드시** 설정한다.
2. `resources.limits.cpu` 는 **선택**이다. 레이턴시 민감 워크로드에만 설정한다. 이유: CFS throttling 으로 인한 p99 tail-latency 악화를 회피하려는 Tim Hockin / Google SRE 가이던스.
### 5.2 QoS 클래스
1. `Guaranteed` — latency-critical (Keycloak, Vault, Postgres 등): `requests == limits`, CPU limit 도 설정.
2. `Burstable` — 일반 stateless 앱: `requests < limits` 또는 CPU limit 생략.
3. `BestEffort`**금지**. requests/limits 를 생략한 워크로드는 PR 에서 블록.
### 5.3 기본 가이드라인 (1000-서비스 스케일 기준 출발점)
| 워크로드 | CPU req | Memory req / limit |
| --- | --- | --- |
| 일반 stateless API | 100m | 128256Mi |
| 무거운 JVM (Keycloak, Elasticsearch) | 500m1 | 12Gi (req == limit) |
| 배경 worker | 250m | 512Mi1Gi |
| 전환성(transient) Job (Flyway) | 100m | 128Mi |
실제 값은 부하 테스트 / VPA 권고 결과로 조정한다.
---
## 6. 보안 기준선 (Security baselines — 모든 Pod)
아래 블록은 **모든** Pod 의 최소 baseline 이다. 이걸 내린 설정은 security-hardening.md 의 예외 절차를 거쳐야 한다.
```yaml
spec:
securityContext:
runAsNonRoot: true
runAsUser: 10001 # 앱별 고정 UID, 루트(0) 금지
runAsGroup: 10001
fsGroup: 10001
seccompProfile:
type: RuntimeDefault
containers:
- name: app
image: registry.example.com/auth-platform/auth-server@sha256:...
imagePullPolicy: IfNotPresent
securityContext:
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop: [ALL]
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
```
---
## 7. PDB, Job, Deployment 기타
1. **PDB**: 1.27+ 에서 `spec.unhealthyPodEvictionPolicy: AlwaysAllow` **필수**. 기본값 `IfHealthyBudget` 은 노드 drain 중 복구 불가능한 Pod 가 evict 되지 못해 업그레이드가 멈추는 원인이 된다.
2. **Job / CronJob**:
- `spec.ttlSecondsAfterFinished: 86400` (24h) 기본. 민감 로그가 남는 경우 `3600` (1h).
- `spec.backoffLimit` 명시 (기본 6). 크리티컬 마이그레이션(Flyway)은 `0` 또는 `1` 로 줄여 재시도 폭주 방지.
- CronJob 은 `spec.concurrencyPolicy: Forbid` 를 기본값으로 둔다(중복 실행 금지).
3. **Deployment**:
- `spec.revisionHistoryLimit: 5` (기본 10 은 너무 많음 — etcd 부하).
- `spec.progressDeadlineSeconds: 600` 명시.
- `spec.strategy.rollingUpdate.maxUnavailable: 0` + `maxSurge: 25%` 가 안전한 기본.
```yaml
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: auth-server-pdb
spec:
minAvailable: 2
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server-prod
```
---
## 8. 문서 내 예시 규약
1. 모든 YAML 예시는 ```` ```yaml ```` 펜스로 감싼다. 다른 언어 펜스 금지.
2. 한 파일에 여러 리소스가 등장하면 `---` separator 를 **명시적으로** 추가한다.
3. 예시는 원칙적으로 `kubectl apply -f` 로 바로 적용 가능한 완전체여야 한다. 지면상 생략할 때는 주석으로 표기: `# ... (full spec omitted for brevity)`.
4. 나쁜 예시(안티패턴)는 반드시 `## 나쁜 예시`, `## ❌`, 또는 `## bad example` 헤더 아래에 둔다. CI 검증 스크립트가 이 헤더 규약으로 나쁜 예시 블록을 제외한다. 헤더 없이 안티패턴을 노출하면 검증기가 정당한 예시로 오인해 lint 규칙 위반을 일으킨다.
5. 네임스페이스, 이미지 레지스트리, 도메인 이름은 `example.com`, `registry.example.com` 플레이스홀더를 사용한다. 실제 조직 도메인은 overlays 에서만 등장한다.
---
## 검증 (Validation)
이 문서의 규약은 CI 에서 기계 검증된다.
- 실행: `k8s/scripts/ci/validate-docs.sh`
- Lint 설정 위치: `.kube-linter.yaml` (repo root)
- 목표 스코어:
- syntax 에러: **0**
- schema 에러: **0**
- lint warning: **≤ 5**
syntax 또는 schema 에러가 있으면 PR 은 머지 불가. lint warning 이 임계치를 넘으면 리뷰어가 수정 또는 예외 주석을 요구한다.
@@ -0,0 +1,297 @@
# infra architecture / environments 기준
## 목적
이 문서는 1000+ 서비스 규모의 K3s 기반 production 클러스터에서
- 환경을 어떻게 나눌지
- namespace / label / selector를 어떻게 고정할지
- K3s 기본 컴포넌트와 GitOps source of truth를 어떻게 구분할지
- cross-cluster / multi-region / DR(RPO·RTO)을 어떻게 문서화할지
를 먼저 고정한다.
이 문서의 목표:
- dev / staging / prod 환경 분리를 **label·namespace·selector 레벨에서** 일관되게 만든다
- 서비스별 리소스 소유권(팀·도메인·컴포넌트)을 label로 쿼리 가능하게 한다
- K3s packaged component와 사용자 AddOn을 혼동하지 않는다
- 멀티 서버에서 `manifests/` 디렉터리를 source-of-truth로 쓰는 사고를 원천 차단한다
- 이후 storage / secrets / ingress / workload / observability 표준의 전제 조건을 고정한다
## 공식 의미 (근거)
- Kubernetes well-known label set (공식, SIG-Apps 공인): `app.kubernetes.io/{name,instance,version,component,part-of,managed-by}` — 총 6개. `environment`는 포함되지 **않는다** (`https://kubernetes.io/docs/concepts/overview/working-with-objects/common-labels/`).
- `app.kubernetes.io/*` 외의 운영 차원(environment, team, tier, region 등)은 **자체 도메인 네임스페이스**(`example.com/*`)를 붙여 선언해야 한다.
- K3s는 `coredns`, `traefik`, `local-storage`, `metrics-server`를 packaged component로 제공한다.
- `/var/lib/rancher/k3s/server/manifests` 아래 파일은 서버 시작 시와 파일 변경 시 자동 적용된다(AddOn auto-deploy).
- packaged component manifest는 K3s가 재기록하므로 직접 수정 금지.
- 멀티 서버 K3s는 AddOn 파일을 자동 동기화하지 않는다.
- Kustomize v5+부터는 `labels:` 필드(기본 `includeSelectors: false`)가 `commonLabels`보다 안전한 기본이다. `commonLabels`는 항상 `selector.matchLabels`에 주입되며, Deployment/StatefulSet의 selector는 **immutable**이므로 운영 중 label 추가만으로 apply가 실패한다.
- GitOps 기본 apply 방식은 **Server-Side Apply** (`kubectl apply --server-side --field-manager=...`)다. CI/ArgoCD/Flux 모두 SSA 기본.
## 기본 규칙
### 1. 환경은 명시적으로 분리하고 **label + namespace 양쪽에** 박는다
기본 환경:
- `dev`
- `staging`
- `prod`
필요 시 `sandbox` / `canary` / `dr`을 추가할 수 있으나 dev/staging/prod 의미를 흐리지 않는다.
각 리소스는 두 곳에 동시에 환경이 드러나야 한다.
- `metadata.namespace` — 물리적 격리
- `metadata.labels["example.com/environment"]` — 쿼리·정책용 (well-known label에는 환경이 없으므로 **자체 도메인 label** 사용)
### 2. 환경 간 혼합 배포 전면 금지
하나의 namespace / hostname / PVC / Secret / TLS cert scope 안에서 서로 다른 환경 리소스가 섞이지 않는다.
금지:
- `auth-dev`, `auth-prod`가 같은 namespace 공유
- dev와 prod가 같은 ingress host (`auth.example.com`) 공유
- staging과 prod가 같은 PostgreSQL schema / S3 bucket / Vault mount 공유
- NetworkPolicy / ResourceQuota / LimitRange가 환경 경계를 걸치지 않음
### 3. namespace 전략은 “환경 prefix + 서비스 이름” 고정
1000+ 서비스 스케일에서 초기에 하나의 포맷을 박는다. 본 표준 권장은:
```
<env>-<domain>-<service>
```
예:
- `prod-identity-auth`
- `prod-identity-keycloak`
- `staging-identity-auth`
- `dev-identity-auth`
- `prod-platform-ingress-nginx`
- `prod-data-postgres-identity`
이유:
- `kubectl -n prod-*` 와일드카드 RBAC / 모니터링 쿼리가 쉬움
- `prod-` prefix로 PodSecurity admission (`pod-security.kubernetes.io/enforce=restricted`)을 one-shot으로 강제 가능
- `default` namespace는 production workload 배포 전면 금지
### 4. `app.kubernetes.io/*` 6종은 전 리소스 필수
모든 워크로드·서비스·ingress·PVC·ConfigMap·Secret에 아래 6개가 반드시 붙는다.
- `app.kubernetes.io/name` — 애플리케이션 이름 (예: `auth`)
- `app.kubernetes.io/instance` — 인스턴스 (예: `auth-prod`)
- `app.kubernetes.io/version` — semver 또는 image tag
- `app.kubernetes.io/component` — 역할 (예: `api`, `worker`, `database`)
- `app.kubernetes.io/part-of` — 상위 도메인 (예: `identity-platform`)
- `app.kubernetes.io/managed-by` — 관리 도구 (예: `kustomize`, `argocd`, `flux`)
### 5. 운영 차원 label은 **자체 도메인**으로 선언
well-known label 6종으로 표현되지 않는 축은 다음 키로 고정한다.
- `example.com/environment``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`
- `example.com/cost-center` — FinOps tag
- `example.com/slo-tier``tier-1|tier-2|tier-3`
금지:
- `app.kubernetes.io/environment` 사용 (well-known set에 없음)
- 도메인 없는 커스텀 키 (`environment: prod` 같은 top-level key)
### 6. selector에 들어가는 label은 **불변 3종만**
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/managed-by` (툴 교체 시 drift)
- `example.com/environment` (overlay에서 주입되면 selector immutable 위반)
### 7. K3s packaged component는 “기본 제공”일 뿐 “무조건 사용”이 아니다
다음 컴포넌트는 클러스터 bootstrap 초기에 유지/비활성 결정을 박는다.
- `traefik`
- `servicelb`
- `local-storage`
- `metrics-server`
- `coredns` (교체는 특수 케이스)
기본:
- 무엇을 끄는지 Git에 기록
- packaged manifest 직접 수정 금지 — `--disable` 플래그 또는 `HelmChartConfig`
- prod 1000-서비스 스케일에서는 traefik / servicelb 모두 disable 후 **ingress-nginx DaemonSet + MetalLB/외부 LB** 조합이 일반적
### 8. `/var/lib/rancher/k3s/server/manifests`는 source of truth 아님
이 디렉터리는 AddOn auto-deploy 경로다.
멀티 서버 환경에서 자동 동기화가 **안 되므로**, Git이 source of truth고 이 디렉터리는 apply sink에 지나지 않는다.
기본:
- Git repo의 `k8s/` 디렉터리가 SoT
- CI/ArgoCD/Flux가 `kubectl apply --server-side`로 push
- 서버별 scp / vim 절대 금지
- 멀티 서버 bootstrap AddOn도 Git 관리(예: `k8s/bootstrap/*`를 첫 서버에만 배치)
### 9. GitOps apply는 Server-Side Apply가 기본
```
kubectl apply --server-side --field-manager=<ci-id> -k <overlay>
kubectl diff --server-side -k <overlay>
```
이유:
- multi-controller 환경(ArgoCD + HPA + VPA + operator)에서 ownership 충돌을 `managedFields`로 명시적 해결
- `last-applied-configuration` annotation 2MB 한계 회피
- 3-way merge 실패로 인한 silent drift 제거
### 10. base는 환경 중립, overlay는 환경 차이만
이후 `kustomize.md`에서 상세히 다룬다. 이 문서에서는 원칙만 박는다.
- `k8s/base/` — 공통 shape, 환경-agnostic
- `k8s/overlays/{dev,staging,prod}/` — patches / images / replicas / resources / labels
overlay는 base를 재작성하지 않는다. overlay diff가 100줄을 넘으면 base 설계 실패 신호다.
### 11. `app/managing/plugins` 책임 분리
`k8s/base/` 하위는 다음 3축으로 고정한다.
- `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)
이 축은 **소유 팀이 다르다**는 가정 위에 있다. 각 축은 독립된 Git owner (CODEOWNERS)를 가진다.
### 12. 상태 저장 / 외부 공개 범위를 architecture 단계에서 분류
모든 서비스는 아래 2축으로 초기 분류한다.
| 축 | 값 |
|-----------------|-----------------------------------------------------------------|
| workload 성격 | `stateless` / `stateful` / `job` / `cronjob` / `daemonset` |
| 공개 범위 | `public` / `internal-only` / `operator-only` / `cluster-only` |
예:
- `auth-server` — stateless / public
- `keycloak` — stateless(앱) + stateful(외부 DB) / public (관리 포트는 internal-only)
- `vault` — stateful / operator-only (+ cluster-only service endpoint)
- `minio-tenant` — stateful / internal-only
- `postgres-identity` — stateful / cluster-only
- `fluent-bit` — daemonset / cluster-only
- `flyway-migrate` — job / cluster-only
### 13. SLO·RPO·RTO를 환경 문서에서 먼저 박는다
환경 분리가 의미 있으려면 각 환경의 목표를 숫자로 고정해야 한다. 서비스 tier별로 아래 항목을 환경 문서에서 표로 둔다.
| tier | availability SLO | RPO | RTO | backup 주기 | multi-AZ | PDB minAvailable |
|--------|------------------|------|------|-------------|----------|------------------|
| tier-1 | 99.95% | 5m | 15m | 15m | required | 50% |
| tier-2 | 99.9% | 1h | 1h | 1h | required | 1 |
| tier-3 | 99.5% | 24h | 4h | 24h | optional | 0 |
tier는 `example.com/slo-tier` label로 리소스마다 붙는다.
### 14. 멀티 서버 K3s는 critical config를 Git에서 통일
K3s multi-server에서는 아래가 모든 서버에서 동일해야 한다(불일치 시 `critical configuration value mismatch`로 join 실패).
- `cluster-cidr`, `service-cidr`, `cluster-dns`, `cluster-domain`
- `disable` 플래그 세트
- `flannel-backend` / CNI 관련
- `embedded-registry` 활성화 여부
기본:
- `/etc/rancher/k3s/config.yaml` Git 관리
- 서버별 ad-hoc 수정 금지
- 신규 서버 조인 전 `config.yaml` diff 확인
### 15. 공개 범위별 ingress host 패턴 고정
- public: `<service>.example.com`
- internal: `<service>.internal.example.com`
- operator: `<service>.ops.example.com` (mTLS + SSO 필수)
- cluster-only: ingress 없음, ClusterIP + NetworkPolicy로만 접근
### 16. 네이밍 규칙 정리 (요약)
- namespace: `<env>-<domain>-<service>`
- Deployment/StatefulSet 이름: `<service>` (namespace로 환경 구분, 이름에 env 중복 금지)
- Service 이름: Deployment 이름과 동일 (headless면 `-headless` suffix)
- PVC 이름: `<service>-<purpose>-<ordinal>` (StatefulSet volumeClaimTemplate은 자동)
- Kustomize overlay 디렉터리: `overlays/<env>/<region>/` (멀티 region 시)
## 추천 디렉터리 구조
```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
```
## 프로젝트 기준 요약
- 환경 3종(`dev`/`staging`/`prod`) + namespace prefix 고정
- well-known `app.kubernetes.io/*` 6개 + 자체 도메인 운영 label 필수
- `app.kubernetes.io/environment` 사용 금지, `example.com/environment`로 대체
- selector에는 불변 3종만
- K3s packaged component는 초기에 disable 여부 결정, 직접 수정 금지
- `manifests/`는 apply sink, Git이 SoT
- `kubectl apply --server-side` GitOps 기본
- SLO / RPO / RTO 표가 환경 문서의 일부
+241
View File
@@ -0,0 +1,241 @@
# backup / restore 기준
## 목적
이 문서는 K3s 및 일반 Kubernetes 인프라에서
- 무엇을 백업해야 하는지
- 어떤 도구/방식으로 백업할지 (Velero / CSI snapshot / pgBackRest / WAL-G / CNPG Barman)
- RPO / RTO를 어떻게 선언할지
- 복구 단위와 절차를 어떻게 표준화할지
- restore drill을 어떻게 운영할지
를 먼저 고정한다.
이 문서의 목표는 다음과 같다.
- "PVC가 있으니 백업도 된 것"이라는 착각을 제거한다
- "K3s etcd snapshot이 있으니 DB/PVC도 복구된다"는 오해를 제거한다
- 선언형 원본, 제어 평면, 상태 저장소를 서로 다른 백업 대상으로 구분한다
- 컴포넌트별 복구 전략과 도구를 먼저 정하고 YAML을 쓰게 한다
- 실제 장애 시 복구 절차를 재현 가능하게 만든다
## 공식 의미
- K3s etcd snapshot은 **클러스터 API 상태**(namespaces, secrets encryption key, RBAC, CRD instance 등)만 백업한다. **PVC의 데이터 내용은 백업하지 않는다.**
- K3s snapshot에는 cluster CA 인증서/개인키와 secrets encryption 관련 데이터가 포함될 수 있다.
- 새 호스트로 K3s snapshot을 복구할 때는 snapshot 당시 사용한 server token이 필요하다.
- Velero는 CNCF 표준 K8s 백업 도구로 `Backup` / `Schedule` / `Restore` CRD와 object storage 백엔드(S3 / MinIO / GCS / Azure Blob)를 사용한다.
- Velero file-level backup: File System Backup (FSB, kopia/restic) — 모든 CSI/비-CSI 볼륨의 파일 내용을 복제.
- Velero volume-level backup: CSI snapshot — CSI driver가 지원하는 경우 블록 수준 snapshot.
- VolumeSnapshotClass의 `deletionPolicy: Retain`이면 VolumeSnapshot 삭제 후에도 VolumeSnapshotContent(클라우드 snapshot)는 남는다.
- PostgreSQL의 기본 physical backup 도구로는 `pg_basebackup`(standalone) 외에 **pgBackRest** 또는 **WAL-G**가 사실상 표준이다. CloudNativePG operator는 내장으로 **Barman Cloud**를 사용한다.
- `pg_dump`는 logical export이며 정기 production 전체 백업의 기본 도구로는 보통 적합하지 않다.
- MinIO `mc mirror`는 현재 객체만 동기화하며 버전 이력/전체 메타데이터 보존에는 적합하지 않다.
- MinIO bucket replication은 versioning을 전제로 하고, DR 상황에서 `resync`를 지원한다.
## RPO / RTO 선언
모든 백업 대상은 아래 세 줄을 runbook에 먼저 적는다.
- **RPO (Recovery Point Objective)**: 허용 가능한 데이터 손실 시간
- **RTO (Recovery Time Objective)**: 허용 가능한 복구 시간
- **Retention**: 백업 보존 기간
이 세 값이 비어 있으면 도구/스케줄을 선택할 수 없다.
기본 tier 예시:
| Tier | RPO | RTO | Retention | 대표 도구 |
|---|---|---|---|---|
| gold | 5분 | 30분 | 30일 | CNPG continuous WAL + CSI snap 매일 |
| silver | 1시간 | 2시간 | 14일 | Velero hourly + CSI snap |
| bronze | 24시간 | 24시간 | 90일 | Velero daily FSB |
| archive | 24시간 | 72시간 | 7년 | Velero weekly → Glacier / cold bucket |
## 기본 규칙
### 1. 백업 대상은 세 층으로 분리
1. **선언형 원본 (Git)**: Kustomize base/overlay, Helm values, Argo CD Application, 운영 문서/runbook, 스크립트
2. **제어 평면 (K8s API state)**: K3s etcd snapshot / Velero API object backup
3. **상태 저장 데이터 (data plane)**: PVC / VolumeSnapshot, PostgreSQL 물리 백업, Vault raft snapshot, MinIO object data
이 셋을 하나의 방식으로 뭉뚱그리지 않는다. 특히 **K3s etcd snapshot은 (3)을 커버하지 않는다.**
### 2. Git은 배포 원본 백업이지 런타임 상태 백업이 아니다
Git/Kustomize는 source of truth지만 runtime DB state, Vault secret state, MinIO object data, K3s cluster membership state를 복원해 주지 않는다. Git 백업만으로 운영 복구가 된다고 판단하지 않는다.
### 3. K3s etcd snapshot은 제어 평면 전용 백업
K3s etcd snapshot은 API server의 선언 상태만 백업한다. PVC 안의 파일 내용은 포함하지 않는다.
기본:
- scheduled etcd snapshot 사용 (예: 6시간마다)
- local retention + S3/off-node retention 병행
- snapshot에는 secrets encryption key와 CA private key가 포함될 수 있으므로 민감 정보로 취급
- 저장 위치 암호화, 접근 통제, 보존 기간 통제, chain of custody 확인
- 새 호스트 복구용 server token을 별도 위치에 보관 (같은 곳에 두면 동시 유출 위험)
### 4. 상태 저장 데이터 백업은 Velero가 표준
Kubernetes 수준에서 PVC / 네임스페이스 / CRD를 함께 백업/복구하려면 **Velero**를 기본 도구로 둔다.
기본 구성:
- `BackupStorageLocation`: off-cluster S3 또는 cluster 외부 MinIO (같은 cluster 안 MinIO에 백업하지 않는다)
- `VolumeSnapshotLocation`: CSI driver 대응
- `Schedule` CRD로 cron 기반 정기 백업
- selector(`labelSelector`, `includedNamespaces`)로 tier별 스케줄 분리
- TTL로 retention 관리
### 5. Velero FSB(kopia/restic) vs CSI snapshot 선택
- **CSI snapshot**: 볼륨 수준 crash-consistent, 빠름, CSI driver 지원 필요. DB처럼 큰 볼륨에 적합. 클라우드 snapshot cost 고려.
- **File System Backup (FSB, kopia/restic)**: 파일 수준, 모든 볼륨에서 동작, 암호화/중복제거, 느림. local-path / hostPath / 비-CSI 볼륨에 적합.
운영 기본:
- CSI snapshot이 가능한 볼륨은 CSI snapshot 우선
- 크지 않은 설정/아카이브 볼륨은 FSB 허용
- DB 볼륨은 CSI snapshot이어도 app-consistent hook(pre/post backup) 필요
### 6. 오프-클러스터 백업 저장소 필수
백업을 **같은 K8s 클러스터 안**의 MinIO/S3에 두지 않는다. cluster 장애 = 백업 동시 소실이다.
기본:
- 별도 리전/별도 account의 S3-호환 object storage
- bucket versioning 활성화
- object-lock / WORM (규제 필요 시)
- 접근은 IRSA / Workload Identity / 최소권한 IAM
### 7. VolumeSnapshotClass `deletionPolicy`는 정책에 맞춘다
운영 gold tier 데이터의 VolumeSnapshotClass는 `deletionPolicy: Retain`을 기본으로 둔다.
이렇게 하면 K8s에서 VolumeSnapshot object가 지워져도 CSI driver의 실제 snapshot(VolumeSnapshotContent)은 남아서 사고 복구 여지를 준다.
### 8. PostgreSQL은 logical / physical / continuous를 구분
- `pg_dump` — logical export. 선택적 export, schema 비교, 마이그레이션 준비용. 프로덕션 전체 복구 기본값으로 두지 않는다.
- `pg_basebackup` — standalone base backup. 소규모/단순 케이스에 적합하지만 WAL archiving을 직접 구성해야 한다.
- **pgBackRest / WAL-G** — 프로덕션 표준. incremental / differential backup, parallel restore, retention, PITR, S3 업로드를 내장.
- **CloudNativePG (CNPG)** — K8s-native Postgres operator. 내장 **Barman Cloud**로 object storage에 WAL + base backup을 지속 업로드. `Backup` / `ScheduledBackup` CRD 제공.
### 9. K8s 위 Postgres 운영 기본은 CloudNativePG
2026 기준 K8s 상에서 Postgres를 운영한다면 **CloudNativePG (CNPG)**를 기본 후보로 둔다 (CNCF sandbox).
이유:
- `Cluster` CRD로 primary + standby 자동 관리, failover, rolling upgrade
- `backup` 섹션에서 Barman Cloud 기반 continuous archiving을 선언만 하면 동작
- `Backup` (on-demand), `ScheduledBackup` (cron), PITR restore가 `Cluster.spec.bootstrap.recovery`로 표준화
- Prometheus `PodMonitor` 내장
대안:
- **Zalando postgres-operator** — 오래된 생태계, Spilo 기반
- **Crunchy PGO** — 상용 지원 강점, pgBackRest 내장
manual StatefulSet + sidecar는 1000개 서비스 규모에서는 권장하지 않는다.
### 10. Keycloak DB는 애플리케이션과 분리된 DB 전략을 따른다
Keycloak이 외부 PostgreSQL을 사용하면 Keycloak 복구는 애플리케이션 Pod 복구보다 DB 백업 전략에 크게 의존한다. Keycloak server manifest만 백업해서는 충분하지 않다.
### 11. Vault는 storage mode에 따라 백업 방식을 다르게 본다
- integrated storage (raft) → `vault operator raft snapshot save` 기본
- external storage (Consul 등) → 해당 백엔드 백업 전략
- dev mode → 운영 대상 아님
Vault snapshot 복구 테스트는 격리된 네트워크/환경에서 수행한다 (live credential revoke, 원치 않는 cluster 간 통신, 데이터 일관성 훼손 방지).
### 12. MinIO는 PVC snapshot만으로 충분하다고 보지 않는다
object store는 단순 PV 파일 복사 관점보다 object versioning / replication / resync 포함 전략으로 본다.
기본:
- bucket versioning enabled
- 소스/대상 cluster replication configured
- DR 시 `mc replicate resync` 절차 문서화
- `mc mirror`는 현재 객체 동기화 용도로만 제한 (버전 이력 보존 안 됨)
- 스토리지 layer snapshot은 보조 수단
### 13. stateless workload는 데이터보다 재현성을 백업
다음은 기본적으로 런타임 파일 백업 대상이 아니다.
- auth-server, ingress-controller, stateless test-server
- 외부 DB 사용 Keycloak 서버 자체
복구 핵심:
- Git / Kustomize / Helm values
- Config / Secret source (Vault Secrets Operator 기준)
- 이미지 digest
- 운영 문서
### 14. migration-flyway는 산출물이 아닌 migration source를 백업
Flyway Job 자체나 container 파일시스템/PVC는 backup 대상이 아니다. 중요한 것은:
- migration script (Git)
- migration ordering + schema history table 상태 (DB 백업으로 포함)
- Flyway 실행 이력 (CI/CD 로그, Argo Rollout 기록)
### 15. 모든 백업은 "주기 + 보존기간 + 저장 위치 + 암호화 + 무결성 검증 + 복구 테스트"를 갖춘다
파일만 남기고 정책이 없는 것을 백업 전략으로 보지 않는다. 최소 메타데이터:
- Schedule cron 또는 RPO
- Retention TTL
- 저장 위치 (버킷, prefix, 리전)
- 암호화 방식 (SSE-S3 / SSE-KMS / client-side)
- 무결성 검증 (checksum, Velero `backup describe`의 errors)
- 복구 테스트 cadence + 마지막 성공 일자
### 16. restore drill은 표준 운영 절차
복구 가능한지 확인하지 않은 백업은 신뢰하지 않는다.
기본 cadence:
- 제어 평면 (K3s etcd / Velero): 분기별 1회
- DB physical restore + PITR: 월 1회
- Vault raft restore: 분기별 1회
- MinIO replication resync: 반기별 1회
기록 항목:
- 실행 일자
- 실행자
- 대상 snapshot/backup ID
- 실제 RTO / 확인된 RPO
- 발견된 issue
- 다음 drill 예정일
최근 90일 내 성공 기록이 없는 백업은 "신뢰할 수 있는 백업"으로 보지 않는다.
### 17. 복구 단위는 컴포넌트별로 다르게 정의
| 컴포넌트 | 복구 단위 | 기본 도구 |
|---|---|---|
| K3s control plane | cluster snapshot | k3s etcd-snapshot |
| K8s API object (namespace 단위) | Velero Backup | Velero |
| PostgreSQL cluster | DB cluster 전체 + PITR | CNPG Backup / pgBackRest |
| PostgreSQL single database | logical dump | `pg_dump` (보조) |
| Vault | raft snapshot | `vault operator raft snapshot` |
| MinIO | bucket / object / site | mc replication + resync |
| stateless apps | namespace/service redeploy | Argo CD + Git |
| PVC 일반 | VolumeSnapshot / Velero FSB | Velero + CSI |
모든 것을 "서비스 단위" 또는 "PVC 단위" 하나로만 보지 않는다.
### 18. 백업 구성은 Git으로 관리되고 GitOps sync된다
Velero `Schedule`, `BackupStorageLocation`, `VolumeSnapshotClass`, CNPG `ScheduledBackup`은 Argo CD / Flux로 동기화한다. kubectl 수동 편집 금지.
### 19. 백업과 복구는 다른 문서와 연결
다음 문서와 항상 연결한다.
- `storage-pvc.md` (PVC tier ↔ snapshot class)
- `db-and-migration.md` (DB 복구 전략)
- `operations-runbook-upgrade-rollback.md` (장애 시 절차)
- `config-and-secrets.md` (Vault 백업)
## 현재 스택 기본 권장안
- **K3s control plane**: etcd scheduled snapshot (6h) + S3 off-node 보관, server token 별도 안전 보관
- **PostgreSQL**: CloudNativePG + Barman Cloud (continuous WAL + daily base backup), RPO 5분
- **Vault**: integrated storage → raft snapshot 매일, 격리 환경에서 분기 1회 drill
- **MinIO**: bucket versioning + 별도 region으로 replication, mc resync runbook
- **PVC 일반**: Velero Schedule (tier별 분리) + CSI VolumeSnapshot
- **auth-server / ingress-controller / stateless**: Git + Argo CD 재현
- **migration-flyway**: migration source는 Git, DB 상태는 CNPG 백업에 포함
## 프로젝트 기준 요약
- 백업 대상은 선언형 원본 / 제어 평면 / 상태 저장 데이터로 분리
- K3s etcd snapshot은 PVC 데이터를 포함하지 않음 — 별도 Velero 필수
- Velero를 Kubernetes 백업 표준으로, off-cluster 저장소에 보관
- PostgreSQL은 CNPG + Barman Cloud (또는 pgBackRest / WAL-G) — `pg_dump`는 보조
- VolumeSnapshotClass `deletionPolicy`를 tier에 맞게 (운영은 Retain)
- RPO/RTO/Retention을 tier로 선언
- restore drill은 분기/월 단위 표준 cadence + 최근 성공 일자 기록
- 백업 구성은 GitOps로 관리
+223
View File
@@ -0,0 +1,223 @@
# config / secrets 기준
## 목적
이 문서는 1000+ 서비스 운영 클러스터에서
- 무엇을 ConfigMap에 두고 무엇을 Secret/Vault에 두는지
- 민감정보를 어떤 경로로 Pod에 주입하는지 (VSO / ESO / CSI / SealedSecrets / SOPS)
- Kubernetes Secret at-rest encryption을 어떻게 구성하는지
- Image registry credential은 어떻게 다루는지
를 단일 ground truth로 고정한다.
이 문서의 목표는 다음과 같다.
- 민감정보가 manifest/Git/image/log 어디에도 새지 않는다
- Vault를 single source of truth로 두고 K8s Secret은 **파생 산출물**로만 존재
- 주입 방식(envFrom/volume/CSI)과 source(VSO/ESO/Vault Injector)를 표준화
- GitOps와 비밀 관리를 구조적으로 분리
## 공식 의미 (근거)
- ConfigMap은 **비기밀** 데이터 저장용 API object. 최대 1MiB.
- Secret은 민감정보용 object. **data는 base64 encoded(암호화 아님)**. stringData는 생성 시 자동 base64.
- Secret은 기본적으로 etcd에 평문 저장(base64 decode가 암호화가 아님). Kubernetes는 **at-rest encryption을 운영에서 필수**로 권장.
- Secret 타입: `Opaque`, `kubernetes.io/tls`, `kubernetes.io/dockerconfigjson`, `kubernetes.io/service-account-token`, `bootstrap.kubernetes.io/token`, `kubernetes.io/basic-auth`, `kubernetes.io/ssh-auth`.
- **Vault Secrets Operator(VSO)**: Vault의 secret(KV v2, dynamic DB, PKI, AWS 등)을 Kubernetes Secret으로 sync하는 controller. CRD: `VaultConnection`, `VaultAuth`, `VaultStaticSecret`, `VaultDynamicSecret`, `VaultPKISecret`, `HCPVaultSecretsApp`. 앱은 그냥 K8s Secret을 `envFrom`/`volumeMounts`로 소비.
- **Vault Agent Injector**: Mutating webhook이 Pod에 sidecar/init container를 주입해 tmpfs에 비밀을 렌더링. K8s Secret을 **만들지 않는다**(Vault → file).
- **External Secrets Operator(ESO)**: AWS Secrets Manager, GCP Secret Manager, Azure Key Vault, Vault 등 **외부 provider → K8s Secret sync**. VSO와 유사하지만 멀티 provider.
- **CSI Secret Store Driver**: volume으로만 마운트(K8s Secret 미생성, optionally mirror). Azure Key Vault, AWS Secrets Manager, GCP Secret Manager, Vault provider 존재.
- **Sealed Secrets (Bitnami)**: public key로 암호화된 `SealedSecret` CRD를 Git에 커밋 → controller가 cluster private key로 복호화해 K8s Secret 생성. GitOps 친화적.
- **SOPS**: 파일 수준 암호화(age/GPG/KMS) + kustomize/Helm/Flux plugin. Git 커밋 가능.
- `EncryptionConfiguration`은 API Server `--encryption-provider-config` 플래그로 지정. providers: `identity`(평문), `aescbc`, `aesgcm`, `secretbox`, `kms` v1/v2.
- K3s는 `--secrets-encryption` 플래그로 aescbc provider 활성화.
## 기본 규칙
### 1. 분류: ConfigMap vs Secret vs Vault
#### ConfigMap
- host/port/base path
- feature flag
- timeout/retry/batch size
- 공개 가능한 application config (`application.yaml` 비기밀 부분)
- log level
- probe 관련 non-secret 설정
#### Kubernetes Secret (하지만 **Vault 파생**이 기본)
- DB password, OAuth client secret, signing key, API token
- TLS 인증서 (cert-manager가 자동 생성)
- imagePullSecret(`kubernetes.io/dockerconfigjson`)
- VSO/ESO가 sync한 Secret
#### Vault (source of truth)
- 모든 운영 credential의 1차 저장소
- DB dynamic credentials, PKI, transit encryption keys
- OIDC client secret, SMTP credential
- KV v2 path로 서비스별 격리
**원칙:** "조금이라도 민감하면 Vault/Secret 쪽". ConfigMap에는 절대 비밀 넣지 않는다. base64는 암호화가 아니다.
### 2. Kubernetes Secret at-rest encryption 필수
운영 클러스터는 API Server `--encryption-provider-config`로 Secret 자원을 암호화한다. 권장 순서: **KMS v2 > KMS v1 > aescbc > identity(금지)**.
```yaml
apiVersion: apiserver.config.k8s.io/v1
kind: EncryptionConfiguration
resources:
- resources: ["secrets"]
providers:
- kms:
apiVersion: v2
name: platform-kms
endpoint: unix:///var/run/kmsplugin/socket.sock
cachesize: 1000
timeout: 3s
- aescbc:
keys:
- name: fallback-2026-q1
secret: <32-byte base64 key>
- identity: {}
```
- KMS 소켓/플러그인은 노드 hardening 대상.
- K3s는 `curl -sfL https://get.k3s.io | INSTALL_K3S_EXEC="server --secrets-encryption" sh -` 또는 config.yaml `secrets-encryption: true`.
- 기존 Secret은 `kubectl get secrets --all-namespaces -o json | kubectl replace -f -`로 강제 재암호화.
- 키 회전은 `kube-apiserver` restart + `replace` 절차를 ADR로 고정.
### 3. Secret delivery 경로 우선순위
1. **VSO** — 운영 기본. Vault KV v2/dynamic credential → K8s Secret → envFrom/volume. 앱 코드 변경 0.
2. **ESO** — 멀티 클라우드 provider 필요 시. API는 VSO와 유사하지만 `SecretStore`/`ClusterSecretStore` + `ExternalSecret`.
3. **CSI Secret Store Driver** — K8s Secret object를 아예 만들고 싶지 않을 때(volume only). SA별 scope가 필요한 sensitive mount.
4. **Vault Agent Injector** — 앱이 template engine을 필요로 할 때(JSON/XML 포맷 렌더링). K8s Secret 없음.
5. **SealedSecrets / SOPS** — GitOps 전용 + 소규모 클러스터 + VSO 미도입 환경. Git에 encrypted blob 커밋.
6. **Plain Secret manifest** — 운영 금지. 로컬/부트스트랩 한정.
선택 기준:
- "Vault가 SoT이고 K8s Secret을 앱이 envFrom으로 소비" → VSO
- "멀티 클라우드/비-Vault provider" → ESO
- "K8s Secret 자체를 만들고 싶지 않음(audit/scope)" → CSI
- "앱이 Vault template로 renderng 필요" → Vault Agent Injector
- "Vault 없음 + Git에 커밋해야 함" → SealedSecrets/SOPS
### 4. VSO CRD 사용 표준
VSO는 Helm으로 `vault-secrets-operator` namespace에 설치되어 있다고 가정한다.
- `VaultConnection` (namespace or cluster) — Vault address, CA bundle, TLS skipVerify=false
- `VaultAuth` — auth method(kubernetes, jwt, approle). kubernetes auth 기본.
- `VaultStaticSecret` — KV v2 secret → K8s Secret
- `VaultDynamicSecret` — Postgres/MySQL/AWS dynamic credentials
- `VaultPKISecret` — PKI engine → `kubernetes.io/tls` Secret
- `HCPVaultSecretsApp` — HCP Vault Secrets 소비
모든 CRD는 같은 namespace 안에서 선언하고, 결과 Secret의 이름은 서비스명 규칙을 따른다.
### 5. Vault path 규칙 + auth policy
- KV v2 path: `kv/data/<team>/<service>/<env>/<component>` (예: `kv/data/identity/auth-server/prod/db`)
- Vault role은 namespace + service account로 제한:
```
bound_service_account_names=auth-server
bound_service_account_namespaces=auth-prod
```
- Vault policy는 `path "kv/data/identity/auth-server/prod/*" { capabilities = ["read"] }` 수준으로 scope.
- dynamic credential TTL은 pod 수명과 맞춘다(예: Postgres role 24h, auto-renew).
### 6. 주입 방식: envFrom vs volume
- **envFrom** — 전체 Secret의 key를 env로 투사. 간단, 12-factor 친화. 하지만 프로세스 env는 sub-process 상속, `/proc/<pid>/environ` 노출 위험.
- **volume** — 파일로 마운트(`/var/run/secrets/<name>`). 권장 in-memory(`readOnly: true`). 민감 key는 volume 우선.
- **envFrom + volume 혼합** 허용(db env는 env, signing key는 volume).
- **subPath**는 사용 금지(Secret 업데이트가 자동 반영 안 됨).
### 7. 한 Pod 내에서도 필요한 컨테이너에만 주입
- sidecar(metrics, proxy)에는 secret 전달 금지.
- Pod `volumes`로 선언하더라도 각 컨테이너 `volumeMounts`는 필요한 컨테이너에만.
### 8. Secret/ConfigMap naming
- 패턴: `<service>-<purpose>` (`auth-server-db`, `auth-server-oidc-client`, `keycloak-db`).
- 금지: `common-*`, `shared-*`, `global-*` (스코프가 불분명하고 권한 팽창 원인).
### 9. immutable Secret/ConfigMap
- 변경 빈도 낮은 `kubernetes.io/tls`, 앱 release-tied config는 `immutable: true` 검토.
- immutable이면 수정 불가 → 삭제 후 재생성 + rollout 필요. VSO가 갱신하는 Secret은 immutable 금지.
### 10. Kustomize generator 사용 기준
- `configMapGenerator` — 비기밀 설정에 허용. hash suffix로 rollout 트리거.
- `secretGenerator` — **운영 금지**. 로컬/테스트/부트스트랩 한정.
- 운영은 VSO/ESO/SealedSecrets 경로.
### 11. Image pull secret
- 타입: `kubernetes.io/dockerconfigjson`.
- 구조:
```json
{
"auths": {
"registry.example.com": {
"username": "ci-bot",
"password": "<token>",
"auth": "<base64(username:password)>"
}
}
}
```
- SA의 `imagePullSecrets`에 연결 → Deployment마다 반복 선언 불필요.
- Registry credential 자체도 VSO로 Vault → `kubernetes.io/dockerconfigjson` Secret sync(VSO `VaultStaticSecret.destination.type: kubernetes.io/dockerconfigjson`).
### 12. image 지정: digest pin 기본
- mutable tag(`latest`, `main`, `dev`)는 `imagePullPolicy: Always` + staging 환경에만.
- 운영은 `image: registry.example.com/auth-server@sha256:<digest>` 고정. `imagePullPolicy: IfNotPresent` 충분.
- digest는 CI가 release 시 생성하고 GitOps manifest(ArgoCD)에 커밋.
- Kyverno/Gatekeeper로 namespace `auth-prod`의 Pod image가 `@sha256:`를 포함하도록 enforce.
### 13. Secret 접근 RBAC
- `secrets` 리소스의 `list`/`watch`는 controller(VSO, cert-manager, argo-cd)에만 허용.
- 일반 workload는 `get` + `resourceNames` 배열로 제한.
- 같은 namespace에서 Pod 생성 권한은 Secret 간접 접근이 될 수 있음을 전제로 RBAC 설계(namespace 분리).
### 14. 민감정보 로깅/에러 보호
- 앱은 비밀을 평문 로그, 예외 메시지, telemetry attribute, debug endpoint에 포함 금지.
- Exception handler는 `password`, `token`, `secret`, `authorization` 포함 필드 자동 redact.
- APM/Logging pipeline에도 scrub rule 추가.
### 15. Secret 회전
- dynamic credential: VSO `VaultDynamicSecret`이 TTL 전에 자동 renew/rotate + Pod rollout trigger(`rolloutRestartTargets`).
- static credential: VSO `refreshAfter` + Vault rotate cron + `rolloutRestartTargets`로 Deployment 자동 rolling.
- TLS cert: cert-manager가 `renewBefore`에 맞춰 회전. Pod는 `reloader` annotation 또는 webhook으로 rollout.
### 16. 설정 타입과 도메인 타입 분리
- `@ConfigurationProperties` / `application.yaml`은 설정 계약.
- 도메인 Value Object는 config에서 복사하되 config 타입을 도메인에 노출하지 않는다.
- 테스트에서는 config를 직접 주입할 수 있어야 한다(포트 바인딩, spring profile).
### 17. 환경별 overlay
- `base/` — 공통 ConfigMap/Service/Deployment/RBAC
- `overlays/{dev,staging,prod}/` — 환경별 patch(`replicas`, `resources`, `image digest`, `ingress host`)
- Secret은 **overlay에 plain 저장 금지**. VSO CRD도 prod overlay에서 Vault mount path만 override.
### 18. 현재 스택 기본 권장안
#### auth-server / test-server / keycloak
- ConfigMap: `application.yaml` 비기밀
- Secret 경로: VSO `VaultStaticSecret`(OIDC client) + `VaultDynamicSecret`(Postgres role)
- 주입: envFrom(DB creds) + volume(signing key 파일)
#### migration-flyway
- short-lived Job
- SA token automount false
- VSO `VaultDynamicSecret`이 migration 전용 Postgres role을 짧은 TTL로 발급
#### vault
- Vault server 자체의 unseal key는 cluster 밖(HSM/KMS/cloud KMS auto-unseal)
- bootstrap token은 `vault-bootstrap` namespace에 at-rest encrypted Secret으로 저장, 사용 후 삭제
#### registry
- `kubernetes.io/dockerconfigjson` Secret은 VSO로 Vault KV에서 sync
- namespace SA `imagePullSecrets`에 연결
## 프로젝트 기준 요약
- ConfigMap = 비기밀, Secret = 민감정보, Vault = source of truth
- Kubernetes Secret at-rest encryption(KMS 우선, aescbc 최소) 필수
- Secret delivery 우선순위: VSO > ESO > CSI > Vault Agent Injector > SealedSecrets/SOPS
- 운영 Secret generator/plain Secret manifest 금지
- image는 digest pin + private registry, `kubernetes.io/dockerconfigjson` Secret은 SA imagePullSecrets 연결
- Secret 주입은 필요한 컨테이너/필요한 key만, env보다 volume 우선
- RBAC은 namespace Role + `resourceNames` + list/watch controller 전용
- 회전은 VSO/cert-manager + rolloutRestart 자동화
+305
View File
@@ -0,0 +1,305 @@
# db / migration 기준
## 목적
이 문서는 Kubernetes 상의 PostgreSQL과 Flyway를 기준으로
- 데이터베이스를 어떻게 나눌지
- 어떤 operator / 도구로 운영할지 (CNPG, Zalando, Crunchy, self-managed StatefulSet)
- migration을 어디서 어떻게 실행할지
- migration과 배포(Helm / Argo CD)의 순서를 어떻게 보장할지
- validate / migrate / rollback / backup / PITR을 어떤 순서로 볼지
- zero-downtime을 위한 expand-migrate-contract를 어떻게 적용할지
를 먼저 고정한다.
이 문서의 목표는 다음과 같다.
- app rollout과 schema 변경을 분리한다
- Keycloak DB와 auth-server DB 경계를 먼저 고정한다
- Flyway를 앱 시작 로직에 숨기지 않는다
- PostgreSQL backup/restore 전략과 migration 전략을 함께 본다
- 1000+ 서비스 규모에서 일관된 migration Job 표준을 만든다
## 공식 의미
- `pg_dump`는 logical export다. 정기 production 전체 백업 기본값으로는 보통 적합하지 않다.
- `pg_basebackup`은 실행 중인 PostgreSQL cluster의 base backup을 만들며 PITR/standby 시작점으로 쓴다.
- PostgreSQL PITR은 base backup + WAL archiving 결합이다.
- 운영 표준 물리 백업 도구: **pgBackRest**, **WAL-G**. 또는 operator-native (CNPG Barman Cloud, Crunchy PGO).
- PostgreSQL의 **대부분 DDL은 트랜잭션 내에서 실행 가능**하지만, `CREATE INDEX CONCURRENTLY`, `REINDEX CONCURRENTLY`, `ALTER TYPE ... ADD VALUE`, `VACUUM`은 트랜잭션 밖에서만 실행된다.
- CloudNativePG operator는 CNCF Sandbox 프로젝트로 K8s-native Postgres 운영 표준 후보다.
- Flyway `validate`는 적용된 migration과 로컬 migration 사이의 이름/타입/checksum 차이, 로컬에 없는 적용 버전, 아직 적용되지 않은 로컬 버전을 검증한다.
- Flyway `validateOnMigrate` 기본값은 `true`, `cleanDisabled` 기본값은 `true` (Flyway 9+).
- Flyway `migrate`는 schema history table을 자동 생성하고 최신 migration까지 적용한다.
- Flyway Community(OSS)는 **undo(U__) migration을 지원하지 않는다.** Undo는 Teams/Enterprise 전용이다.
- Flyway는 migration 실행 시 schema history table에 advisory lock을 걸어 동시 실행을 방지한다.
## RPO / RTO
모든 DB는 다음을 runbook에 먼저 적는다.
- RPO / RTO / Retention
- 복구 목표 (cluster restore / PITR / standby seed)
- 운영 tier (gold / silver / bronze)
이 값들이 없으면 backup 도구 선택이 되지 않는다. `backup-restore.md` 참조.
## 기본 규칙
### 1. DB 경계는 애플리케이션 경계보다 먼저 고정
다음을 명시적으로 정한다.
- Keycloak DB와 auth-server DB를 **물리적 cluster**로 분리할지, 같은 cluster 내 **logical DB / schema**로 분리할지
- test-server가 DB를 가지는지
- migration 소유권이 누구에게 있는지 (보통 서비스 팀)
기본:
- 인증 critical data (Keycloak)와 앱 data (auth-server)는 **cluster 분리 권장**
- 같은 cluster를 쓰더라도 database / role / schema ownership을 섞지 않음
- 한 migration tool/job이 여러 서비스 schema를 동시에 소유하지 않음
### 2. K8s 위 Postgres 운영 기본은 operator
1000+ 서비스 규모에서 self-managed StatefulSet은 운영 부담이 너무 크다. Operator를 기본 후보로 둔다.
우선순위 (2026 기준):
1. **CloudNativePG (CNPG)** — CNCF Sandbox, K8s-native, Barman Cloud 내장, `Cluster` / `Backup` / `ScheduledBackup` CRD
2. **Crunchy PGO** — 상용 지원, pgBackRest 내장
3. **Zalando postgres-operator** — Spilo 기반, 레거시 환경
operator를 쓰면 자동으로 얻는 것:
- primary/standby 구성 + failover
- rolling minor upgrade
- WAL archiving + continuous backup
- pg_basebackup, PITR, replica re-clone
- PodMonitor 연동
### 3. migration은 앱 startup에 숨기지 않는다
Flyway migration은 **독립 실행 단계**다.
기본:
- `validate` → (필요시 `info`) → `migrate` → app rollout
기본 금지:
- app container 시작 시 자동 migration (Spring Boot `spring.flyway.enabled=true` + `@SpringBootApplication` 부팅 시 migrate)
- readiness/liveness와 migration 실패를 섞는 구조
- "서버가 뜨면 알아서 schema를 맞춘다" 방식
### 4. Flyway 실행 기본값은 Kubernetes Job
운영 환경에서 Flyway는 다음 중 하나로만 실행한다.
- Kubernetes Job (권장)
- CI/CD 명시 단계
- 운영자 명시 실행 절차
장기 실행 Deployment에 넣지 않는다. Flyway 예시는 `examples/infra/flyway.md` 참조.
### 5. migration Job은 배포 흐름 안에서 app보다 먼저 실행
migration을 app보다 **선행**시키는 것은 manifest 메타데이터로 선언한다.
패턴 A — Helm hook:
```yaml
metadata:
annotations:
"helm.sh/hook": "pre-upgrade,pre-install"
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
```
패턴 B — Argo CD sync wave + hook:
```yaml
metadata:
annotations:
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
```
기본:
- migration은 sync wave가 app보다 **작은** 값 (먼저 실행)
- app Deployment는 wave `0` 또는 그 이상
- Helm과 Argo CD를 혼용하는 경우 **한 쪽으로 통일** (둘 다 hook을 걸면 순서가 꼬인다)
### 6. migration Job 안전 설정
모든 migration Job은 다음을 명시한다.
- `parallelism: 1` — 병렬 실행 금지 (Flyway advisory lock이 막아주지만, Job 수준에서도 명시)
- `completions: 1`
- `backoffLimit: 0` 또는 작은 값 (1~2) — 실패 시 무한 재시도 금지
- `activeDeadlineSeconds` — 타임아웃 (예: 1800)
- `ttlSecondsAfterFinished` — 완료 후 자동 정리 (예: 86400)
- `restartPolicy: Never`
- 이미지는 **digest pinning** (`flyway/flyway@sha256:...`)
- `resources.requests/limits` 명시
- `securityContext` restricted PSA 준수
### 7. validate를 먼저, migrate를 나중에
운영 절차 기본 순서:
1. `flyway info` (pending migration 확인)
2. `flyway validate`
3. `flyway migrate`
4. `flyway info` (결과 확인)
5. app rollout
`validateOnMigrate=true` 기본값이 있더라도, 운영 runbook에서는 validate 단계를 **분리 Job** 또는 **initContainer**로 분리한다. `examples/infra/flyway.md` 참조.
### 8. migration source는 Git이 source of truth
중요한 것은 아래다.
- versioned migration script (`V__`)
- repeatable migration script (`R__`)
- migration ordering
- schema history table 상태
기본 금지:
- 운영 서버에서 migration 파일 수동 수정
- 적용된 migration 파일을 사후 편집 (checksum mismatch)
- Flyway schema history table을 사람이 직접 UPDATE/DELETE
### 9. Flyway undo(U__)는 쓰지 않는다
Flyway Community(OSS)는 **U__ 파일을 지원하지 않는다**. Teams/Enterprise에서만 `undo` 명령이 동작한다.
기본:
- undo migration 파일을 만들지 않음
- rollback은 forward-only migration + PITR로 수행
- 운영 기본은 "다음 migration으로 앞으로 수정"
### 10. DB backup 전략과 migration 전략을 같이 본다
schema 변경이 production에 들어간다면, 같은 변경 계획 안에 아래가 같이 있어야 한다.
- rollback 가능 여부
- 변경 직전 backup 시점 (예: on-demand CNPG `Backup` 실행)
- restore 단위 (전체 cluster / logical DB)
- PITR 필요 여부 + targetTime 후보
- migration 실패 시 중단 지점 (어느 V__에서 멈췄는지)
### 11. PostgreSQL 운영 기본 백업은 continuous physical backup
운영 기본 복구 목표가 cluster-level restore / PITR / standby seed 중 하나면 continuous WAL archiving + base backup이 기본이다.
도구 선택:
- K8s + CNPG → Barman Cloud (내장)
- K8s + Crunchy → pgBackRest (내장)
- 자체 운영 → pgBackRest 또는 WAL-G
`pg_dump`는 다음 용도로 제한:
- 선택적 logical export
- 로컬/테스트 seed
- 일부 schema/table 보존
- migration 검증용 비교 데이터
### 12. PITR 필요 여부를 초기에 결정
다음 질문에 "예"면 PITR을 우선 검토한다.
- 잘못된 migration/DDL을 특정 시점 직전으로 되돌려야 하는가
- 운영 데이터 손실 허용 시간이 짧은가 (RPO < 1h)
- 인증 관련 데이터 정합성이 중요한가
### 13. schema ownership은 서비스별로 분리
기본:
- auth-server schema는 auth-server 팀이 소유
- keycloak schema는 keycloak이 소유
- 공용 schema 남발 금지
- "편해서" 하나의 migration 프로젝트로 통합 관리 금지
### 14. Flyway history table 전략을 먼저 고정
초기에 결정:
- `flyway.table` (기본 `flyway_schema_history`)
- `flyway.defaultSchema`
- `flyway.schemas`
- `flyway.createSchemas`
- 필요 시 `flyway.initSql`
기본:
- history table을 service별 schema에 배치 (예: `auth_server.flyway_schema_history`)
- 여러 서비스의 history table을 하나의 schema에 몰지 않음
### 15. baseline / repair는 예외 절차
baseline과 repair는 정상 운영 흐름이 아니다.
허용 예:
- legacy DB를 처음 Flyway 관리로 편입 (baseline)
- 의도적 migration 수정 후 공식 절차로 checksum 회복 (repair)
- history corruption 복구 (repair)
기본 금지:
- CI/CD에서 습관적 baseline/repair
- validate 오류를 없애기 위해 무분별하게 repair
### 16. migration은 forward-only를 기본값으로
운영 기본값:
- 새 migration으로 앞으로 수정
- rollback용 SQL을 미리 기대하지 않음
- 실패 시 restore/PITR 또는 다음 migration으로 교정
### 17. Keycloak DB와 auth-server DB는 따로 본다
둘 다 PostgreSQL을 써도 운영 기준은 별도로 둔다.
- migration 파이프라인 분리
- backup/restore 영향도 분리
- schema/table ownership 분리
- 버전 업그레이드 절차 분리
- Keycloak은 자체 migration을 내장하므로 **Flyway로 관리하지 않는다**
### 18. test-server는 DB를 기본 전제로 두지 않는다
test-server가 DB 연결이 없으면 migration 대상 아님, DB secret 불필요, rollout 절차도 DB 의존 없이 단순화된다.
### 19. destructive migration은 expand → migrate → contract
Zero-downtime을 위한 3단계 릴리즈:
1. **Expand** — 새 컬럼/테이블 추가 (NULL 허용 또는 default 값 있음). 기존 앱 호환.
2. **Migrate** — 앱을 새 스키마 기준으로 배포 + 데이터 backfill.
3. **Contract** — 기존 컬럼/테이블/제약 제거. 한 릴리즈 이상 뒤.
각 단계는 **별도 릴리즈**로 나간다. 같은 릴리즈에서 expand와 contract를 같이 하지 않는다.
인증/권한/토큰 관련 테이블은 특히 보수적으로.
### 20. DB 변경은 애플리케이션 호환성 윈도우를 고려
migration 문서는 다음을 포함한다.
- 이전 앱 버전과 호환 여부
- 새 앱 버전과 호환 여부
- 중간 배포 구간에서 허용되는 상태 (N-1 ↔ N 동시 운영 가능 여부)
- 롤백 시 DB가 이미 바뀐 상태일 때의 대응
### 21. 대용량 / long-running DDL은 트랜잭션 밖에서
Postgres에서 다음은 트랜잭션 밖에서 실행해야 한다.
- `CREATE INDEX CONCURRENTLY`
- `REINDEX CONCURRENTLY`
- `ALTER TYPE ... ADD VALUE` (Postgres 12+에서는 트랜잭션 내에서도 제한적으로 가능)
- `VACUUM`
Flyway에서는 해당 migration 파일 상단에 다음을 적는다:
```sql
-- flyway:executeInTransaction=false
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
```
### 22. restore 테스트 없는 backup/migration 전략 금지
다음은 반드시 drill이 있어야 한다.
- PostgreSQL base backup 복구
- WAL/PITR 절차
- Flyway 적용 후 실패 시 중단 및 복구 절차
- Keycloak/auth-server 개별 DB restore 절차
## 현재 스택 기본 권장안
- **auth-server DB**: CNPG `Cluster` 3 instances, Barman Cloud, RPO 5분, Flyway Job으로 migration
- **keycloak DB**: CNPG `Cluster` 별도, Keycloak 자체 migration (Flyway 밖)
- **test-server**: DB 없음
- **migration-flyway**: Kubernetes Job (Helm/Argo hook), 앱 Deployment보다 먼저 실행
- **backup**: CNPG Barman continuous WAL + daily base backup, `pg_dump`는 보조
## 프로젝트 기준 요약
- app rollout과 migration을 분리 (app startup migration 금지)
- Postgres on K8s는 CNPG operator를 기본 후보로
- migration Job은 Helm hook 또는 Argo CD sync wave로 app보다 먼저 실행
- migration Job은 `parallelism: 1`, `backoffLimit: 0`, digest pinning, restricted PSA
- Flyway undo(U__) 파일 만들지 않음 (OSS 미지원)
- validate → migrate → app rollout 순서
- CNPG Barman Cloud (또는 pgBackRest / WAL-G) 물리 백업 + PITR, `pg_dump`는 보조
- schema ownership은 서비스별 분리
- destructive migration은 expand → migrate → contract, 여러 릴리즈에 걸쳐
- non-transactional DDL은 `-- flyway:executeInTransaction=false`
+304
View File
@@ -0,0 +1,304 @@
# Flyway 기준
## 목적
이 문서는 PostgreSQL 기반 서비스에서 Flyway를
- 어디서 실행할지 (Kubernetes Job)
- 어떤 순서로 실행할지 (validate / info / migrate / app rollout)
- 어떤 배포 흐름과 맞물릴지 (Helm hook / Argo CD sync-wave)
- config를 어떻게 공급할지 (env var + Secret via Vault Secrets Operator)
- schema history table을 어떻게 둘지
- baseline / repair / out-of-order / undo를 어떻게 다룰지
- non-transactional DDL을 어떻게 처리할지
를 먼저 고정한다.
이 문서의 목표는 다음과 같다.
- Flyway를 앱 startup 내부 로직처럼 숨기지 않는다
- validate / migrate / repair / baseline의 역할을 분리한다
- schema history table을 운영 감사 추적의 일부로 본다
- migration Job을 1000+ 서비스 규모에서 재현 가능하게 표준화한다
- DB 변경을 애플리케이션 rollout과 분리해 운영한다
## 공식 의미
- Flyway `validate`는 적용된 migration과 로컬 migration 사이의 이름/타입/checksum 차이, 로컬에 없는 적용 버전, 아직 적용되지 않은 로컬 버전을 검증한다.
- `migrate`는 schema history table이 없으면 자동 생성하고 최신 migration까지 적용한다.
- schema history table은 migration 실행 내역, checksum, 성공/실패 상태를 기록하는 audit trail이다.
- `repair`는 schema history table을 수정하는 명령이며, 실패한 migration 엔트리 제거, checksum/description/type 재정렬, missing migration 삭제 표시를 수행한다. user object는 정리하지 않는다.
- schema history table 기본 이름은 `flyway_schema_history`다.
- schema history table 위치는 `table`, `defaultSchema`, `schemas`로 제어할 수 있다.
- `createSchemas=false`일 때 history table이 들어갈 schema가 미리 준비되지 않으면 migrate가 실패할 수 있다.
- 기존 non-empty schema에 Flyway를 도입할 때 history table이 없으면 `baseline` 또는 `baselineOnMigrate`가 필요할 수 있다.
- schema history에는 `Pending`, `Success`, `Missing`, `Out of Order`, `Outdated`, `Superseded`, `Deleted` 등 상태가 기록될 수 있다.
- Flyway는 migration 실행 중 schema history table에 **advisory lock**을 걸어 동시 실행을 직렬화한다. 다중 replica Job 수준의 race를 방지한다.
- `cleanDisabled`는 Flyway 9 이후 기본 `true`. production에서는 반드시 `true`를 명시한다.
- Flyway 8.2+ 에서 `-- flyway:executeInTransaction=false` directive로 migration 파일 단위 트랜잭션 비활성화가 가능하다.
- Flyway Community(OSS)는 **undo(U__) migration을 지원하지 않는다.** Undo는 Teams/Enterprise 상용 기능이다.
- 환경변수 config 지원: `FLYWAY_URL`, `FLYWAY_USER`, `FLYWAY_PASSWORD`, `FLYWAY_LOCATIONS`, `FLYWAY_SCHEMAS`, `FLYWAY_DEFAULT_SCHEMA`, `FLYWAY_TABLE`, `FLYWAY_BASELINE_ON_MIGRATE`, `FLYWAY_VALIDATE_ON_MIGRATE`, `FLYWAY_CLEAN_DISABLED`, `FLYWAY_OUT_OF_ORDER`, 그 외 `FLYWAY_*`.
## 기본 규칙
### 1. Flyway는 앱 startup이 아니라 독립 실행 단계
운영 환경에서 Flyway는 다음 중 하나로만 실행한다.
- Kubernetes Job (권장)
- CI/CD 명시 단계
- 운영자 명시 실행 절차
기본 금지:
- 애플리케이션 startup 시 자동 migration
- Spring Boot `spring.flyway.enabled=true`로 앱 부팅 경로에 포함
- readiness/liveness와 migration 실패를 섞는 구조
### 2. 기본 순서는 info → validate → migrate → info → app rollout
운영 기본 순서:
1. `flyway info` (pending 확인)
2. `flyway validate`
3. `flyway migrate`
4. `flyway info` (결과 확인)
5. 애플리케이션 rollout
`validateOnMigrate=true`가 기본값이지만, 운영 절차상 validate를 **분리 initContainer** 또는 **사전 단계**로 둔다.
### 3. 배포 흐름 안에서 app보다 먼저 실행 — 두 가지 패턴
**패턴 A: Helm hook**
```yaml
annotations:
"helm.sh/hook": "pre-upgrade,pre-install"
"helm.sh/hook-weight": "-10"
"helm.sh/hook-delete-policy": "before-hook-creation,hook-succeeded"
```
**패턴 B: Argo CD sync-wave**
```yaml
annotations:
argocd.argoproj.io/sync-wave: "-1"
argocd.argoproj.io/hook: Sync
argocd.argoproj.io/hook-delete-policy: BeforeHookCreation
```
기본:
- 두 패턴을 혼용하지 않는다 (Argo CD가 Helm chart를 렌더링할 때 Helm hook을 일반 리소스로 취급해 순서가 꼬임)
- 배포 도구에 맞춰 한쪽만 사용
### 4. migration Job 안전 설정 체크리스트
Job manifest에 반드시 다음이 있어야 한다.
- `parallelism: 1`, `completions: 1`
- `backoffLimit: 0` 또는 작은 값 (1~2)
- `activeDeadlineSeconds` (권장 1800 = 30분, 대형 migration은 더 길게)
- `ttlSecondsAfterFinished` (권장 86400 = 1일)
- `restartPolicy: Never`
- 이미지 digest pinning (`flyway/flyway@sha256:...`)
- `imagePullPolicy: IfNotPresent`
- `resources.requests/limits`
- `securityContext`: `runAsNonRoot: true`, `readOnlyRootFilesystem: true`, `capabilities: drop: [ALL]`
- Pod-level `seccompProfile: RuntimeDefault`
- `fsGroup` 명시 (필요 시)
### 5. config는 환경변수 + Secret
Flyway CLI는 `FLYWAY_*` 환경변수를 읽는다. Secret은 Vault Secrets Operator(VSO) 또는 External Secrets Operator를 통해 클러스터에 동기화된 `Secret`에서 주입한다.
필수 env var:
- `FLYWAY_URL``jdbc:postgresql://host:5432/db`
- `FLYWAY_USER`
- `FLYWAY_PASSWORD` — Secret에서 주입
- `FLYWAY_LOCATIONS``filesystem:/flyway/sql`
운영 권장 env var:
- `FLYWAY_SCHEMAS` — 대상 schema
- `FLYWAY_DEFAULT_SCHEMA` — history table 위치
- `FLYWAY_TABLE` — 기본 `flyway_schema_history`
- `FLYWAY_VALIDATE_ON_MIGRATE=true`
- `FLYWAY_BASELINE_ON_MIGRATE=false` (운영 기본값)
- `FLYWAY_CLEAN_DISABLED=true` (production 필수)
- `FLYWAY_OUT_OF_ORDER=false`
- `FLYWAY_MIXED=false`
### 6. `cleanDisabled=true`는 production 필수
`flyway clean`은 모든 object를 drop 하는 파괴적 명령이다.
- production: `FLYWAY_CLEAN_DISABLED=true` 반드시 명시 (Flyway 9+ 기본값이지만 명시적으로 선언)
- dev/test: 필요 시 `false` 허용, 단 접근 권한 분리
### 7. migration SQL은 ConfigMap 또는 이미지 레이어로
옵션:
- **ConfigMap**: 서비스 manifest와 함께 Argo CD로 관리. small/medium migration set에 적합. ConfigMap 1MiB 제한 주의.
- **이미지 레이어**: 서비스 repo에서 migration SQL을 Docker image로 빌드하고 Flyway image와 합쳐 사용. 대규모 migration set에 적합.
기본:
- 두 방식 모두 Git이 source of truth
- 운영 서버에서 `kubectl edit configmap`으로 migration 편집 금지
### 8. schema history table은 운영 감사 추적의 일부
수동 UPDATE / DELETE 금지. 위치는 명시적으로 결정.
기본:
- service별 schema를 `FLYWAY_DEFAULT_SCHEMA`로 지정 (예: `auth_server`)
- history table 이름은 기본값 `flyway_schema_history` 유지
- 여러 서비스의 history table을 하나의 schema에 몰지 않음
### 9. `createSchemas=false`면 history schema를 사전 준비
`createSchemas=false`를 쓰면 history table이 들어갈 schema를 별도 준비해야 한다.
기본:
- `FLYWAY_INIT_SQL``CREATE SCHEMA IF NOT EXISTS` 지시 가능
- 또는 CNPG `Cluster.bootstrap.initdb.postInitSQL`에서 schema 사전 생성
- 생성 책임이 누구인지 문서화
### 10. baseline은 예외 절차
허용 예:
- legacy DB를 처음 Flyway 관리로 편입
- 기존 non-empty schema를 Flyway에 편입할 때
기본 금지:
- 새 프로젝트인데 baseline부터 쓰기
- 운영 배포 파이프라인에서 습관적으로 baseline 사용
### 11. `baselineOnMigrate`는 기본값 아님
`baselineOnMigrate=true`는 도입/전환 시 편의를 줄 수 있지만, 운영 기본값으로 두지 않는다.
이유:
- 예상치 못한 기존 schema를 "정상 상태"처럼 받아들일 수 있다
- 실수 탐지력이 떨어진다
`FLYWAY_BASELINE_ON_MIGRATE=false`로 명시.
### 12. `repair`는 예외 절차
허용 예:
- 의도적으로 migration 파일을 수정했고 checksum 정렬이 필요
- missing migration을 문서화된 절차로 정리
- failed repeatable migration 이후 history 정리
기본 금지:
- validate 오류가 나면 원인 분석 없이 바로 repair
- CI/CD에서 습관적으로 repair 실행
### 13. `repair`는 user object를 고쳐주지 않는다
repair는 schema history table만 정리한다. 실패한 migration이 남긴 DB object 정리, 불완전한 DDL/DML 정리는 별도 절차로 수행해야 한다.
### 14. 적용된 migration 파일은 수정 금지
이유:
- checksum mismatch
- 재현 불가
- 환경 간 drift
대응:
- 새 migration으로 교정
- 정말 예외적인 수정만 공식 repair 절차와 함께 수행
### 15. out-of-order는 기본 금지
Out-of-order migration은 전체 migration history를 다시 실행할 때 다른 결과를 만들 수 있다.
기본:
- `FLYWAY_OUT_OF_ORDER=false`
- 뒤늦게 빠진 migration을 넣는 방식을 기본값으로 두지 않음
- 예외 허용 시 영향 범위 검토 문서 필수
### 16. repeatable migration(R__)은 목적 제한
Repeatable migration은 다음 용도에 제한한다.
- view 정의
- function / procedure
- trigger 재생성
- reference / static data refresh
기본 금지:
- 순서가 중요한 핵심 schema change를 repeatable로 남발
- versioned migration 대신 repeatable로 대체
### 17. Undo(U__) migration은 만들지 않는다
Flyway Community(OSS)는 undo를 지원하지 않는다.
- U__ 파일을 repo에 두지 않음 (오해 유발)
- rollback은 forward-only 새 migration + PITR로 대응
### 18. locations는 environment별로 흔들지 않는다
`migrate``repair`는 같은 `locations` 전제를 가져야 한다.
기본:
- env마다 location 구조가 달라지지 않게 유지
- 운영과 개발에서 전혀 다른 migration set을 쓰지 않음
- env별 변수는 `placeholders`(`FLYWAY_PLACEHOLDERS_*`)로 분리
### 19. migration은 서비스 소유권 단위로 분리
기본:
- auth-server는 auth-server migration set
- keycloak은 keycloak 고유 migration (사실 Keycloak은 내부 migration을 사용하므로 Flyway 대상이 아님)
- 공용 migration 프로젝트 금지
### 20. migration naming / versioning
기본:
- versioned: `V<N>__<snake_case>.sql`, N은 증가하는 정수 또는 점표기(예: `V12__`, `V1.2.3__`)
- repeatable: `R__<snake_case>.sql`
- 이름은 변경 의도를 드러나게 작성
예:
- `V42__add_refresh_token_audit_columns.sql`
- `R__refresh_user_views.sql`
### 21. destructive change는 expand → migrate → contract
`db-and-migration.md` #19 참조. Flyway 입장에서 각 단계는 **별도 릴리즈**의 versioned migration으로 나간다.
### 22. non-transactional DDL은 `executeInTransaction=false`
Postgres에서 트랜잭션 밖 실행이 필요한 DDL:
- `CREATE INDEX CONCURRENTLY`
- `REINDEX CONCURRENTLY`
- `ALTER TYPE ... ADD VALUE`
- `VACUUM`
migration 파일 상단:
```sql
-- flyway:executeInTransaction=false
CREATE INDEX CONCURRENTLY idx_users_email ON users(email);
```
기본:
- 이런 DDL은 **전용 migration 파일**로 분리 (다른 statement와 섞지 않음)
- runtime 추정치 주석
- low-traffic window로 배포 일정 조정
### 23. rollback은 Flyway 명령에 기대지 않는다
운영 기본 rollback:
- 새 migration으로 수정
- PostgreSQL PITR (CNPG bootstrap.recovery)
- 애플리케이션 버전 rollback + DB 호환 윈도우 유지 (expand-contract의 효과)
rollout undo가 DB schema rollback을 대신하지 않는다.
### 24. 현재 스택 기준 기본 권장안
- **auth-server**
- Flyway Job (Helm hook 또는 Argo sync-wave)
- `FLYWAY_DEFAULT_SCHEMA=auth_server`
- validate → migrate → app rollout
- digest pinning
- **keycloak**
- Keycloak 자체 migration 사용, Flyway 대상 아님
- **test-server**
- DB가 없으면 Flyway 대상 아님
- **운영 절차**
- repair/baseline은 예외 승인 절차
- applied migration 수정 금지
- CLEAN_DISABLED=true 필수
## 프로젝트 기준 요약
- Flyway는 독립 실행 단계 (Kubernetes Job)
- info → validate → migrate → info → app rollout
- 배포 흐름 내 순서는 Helm hook 또는 Argo CD sync-wave 중 하나로 통일
- Job: `parallelism: 1`, `backoffLimit: 0`, `ttlSecondsAfterFinished`, digest pinning, restricted PSA
- config는 `FLYWAY_*` env var + Secret (VSO / ESO)
- `FLYWAY_CLEAN_DISABLED=true` 필수, `FLYWAY_BASELINE_ON_MIGRATE=false`, `FLYWAY_OUT_OF_ORDER=false`
- schema history table 위치를 `FLYWAY_DEFAULT_SCHEMA`로 명시
- baseline / repair / out-of-order는 예외 절차
- applied migration 수정 금지
- Undo(U__) 파일 만들지 않음 (OSS 미지원)
- non-transactional DDL은 `-- flyway:executeInTransaction=false`로 파일 단위 분리
- service별 migration ownership 분리
- destructive migration은 expand → migrate → contract
+224
View File
@@ -0,0 +1,224 @@
# K3s-specific 기준
## 목적
이 문서는 일반 Kubernetes 표준과 **분리**해서, K3s 운영에서만 발생하는 특수성을 고정한다.
목표:
- K3s packaged component (`coredns`, `traefik`, `local-storage`, `metrics-server`, `servicelb`)를 일반 manifest처럼 관리하는 실수를 막는다
- `/var/lib/rancher/k3s/server/manifests`를 source-of-truth로 쓰는 실수를 막는다
- 멀티 server HA 환경에서 `critical configuration value mismatch` join 실패를 예방한다
- embedded registry mirror(Spegel)의 네트워크·버전 게이트를 정확히 이해한다
- 1000+ 서비스 prod 스케일에서 K3s의 어떤 기능을 켜고 어떤 기능을 외부로 뺄지 기준을 박는다
## 공식 의미 (근거 URL 포함)
- K3s packaged component: `coredns`, `traefik`, `local-storage`, `metrics-server` (매니페스트 파일 기반) + `servicelb`(매니페스트 없이 `--disable`만 가능).
- AddOn auto-deploy: `/var/lib/rancher/k3s/server/manifests` 하위 파일은 server 시작 시 + 파일 변경 시 자동 apply. packaged component는 K3s가 재기록하므로 직접 수정 금지.
- multi-server 유저 AddOn은 서버 간 자동 동기화되지 **않는다**.
- K3s 설정: `/etc/rancher/k3s/config.yaml` + `/etc/rancher/k3s/config.yaml.d/*.yaml` drop-in.
- critical 값 (cluster-cidr / service-cidr / cluster-dns / cluster-domain / disable 세트 / CNI / embedded-registry 활성화)이 서버 간 불일치면 `critical configuration value mismatch` join 실패.
- packaged Helm component(`traefik` 등) 커스터마이징은 `HelmChartConfig` (apiVersion `helm.cattle.io/v1`).
- K3s 기본 local storage는 Rancher Local Path Provisioner (`local-path` StorageClass, node-local, not replicated).
- **embedded registry mirror (Spegel)**: 기본 비활성. 활성화 시 노드 간 TCP 5001 (p2p gossip) + TCP 6443 (registry + supervisor)이 reachable해야 한다. 출처: `https://docs.k3s.io/installation/registry-mirror` — "all nodes must be able to reach each other via their internal IP addresses, on TCP ports 5001 and 6443".
- K3s 이미지 import: `/var/lib/rancher/k3s/agent/images/*.tar{,.zst,.gz}`.
- K3s는 기본적으로 network policy enforcer (kube-router 기반)를 포함한다. 외부 CNI(Cilium 등) 사용 시 `--disable-network-policy` + `--flannel-backend=none` 조합 필요.
## 기본 규칙
### 1. K3s 전용 규칙은 별도 문서로 유지
일반 Kubernetes 표준 문서에 K3s 특수성을 흩뿌리지 않는다. 분리 범주:
- packaged component
- AddOn auto-deploy
- config.yaml / config.yaml.d
- local-path provisioner
- embedded registry mirror
- critical server flags
- Helm component customization
### 2. packaged component는 “편의 기능”, 직접 수정 절대 금지
관리 대상:
- `coredns`
- `traefik`
- `local-storage`
- `metrics-server`
- `servicelb` (manifest 없음, flag로만 제어)
금지:
- `/var/lib/rancher/k3s/server/manifests/traefik.yaml` 직접 edit
- packaged manifest를 Git SoT로 관리
- 재시작 후 overwrite되는 파일에 운영 커스터마이징 저장
### 3. packaged component 유지/비활성은 cluster bootstrap 때 박는다
1000-서비스 prod 스케일에서 현재 기준:
| component | prod 기본 | 이유 |
|----------------|-----------|-------------------------------------------------------------|
| `traefik` | disable | ingress-nginx / Envoy Gateway로 교체. Traefik은 dev만. |
| `servicelb` | disable | MetalLB L2/BGP 또는 외부 LB. klipper는 노드 80/443 점유. |
| `local-storage`| disable | Longhorn / Ceph RBD / CSI. node-local은 DR 불가. |
| `metrics-server`| keep | HPA + `kubectl top` 전제. 대체 pipeline 준비되면 교체 가능. |
| `coredns` | keep | 교체는 특수 케이스. node-local dns cache는 별도로 추가. |
| network policy | 상황별 | Cilium 도입 시 disable. 기본 kube-router 유지도 가능. |
### 4. server critical config는 Git에서 단일 파일로 관리
`/etc/rancher/k3s/config.yaml`이 Git의 inventory repo (Ansible / Fleet / CI)에서 push된다.
서버별 ad-hoc 수정 금지. critical 값 mismatch는 **join 실패**로 직결된다.
일치해야 하는 값:
- `cluster-cidr`, `service-cidr`, `cluster-dns`, `cluster-domain`
- `disable` 세트
- `flannel-backend` / `disable-network-policy`
- `embedded-registry` 활성화 여부
- `datastore-endpoint` (etcd / external DB)
### 5. CLI argument보다 config file 우선
재현성 / diff / multi-node 동기화를 위해 server/agent 플래그는 모두 `config.yaml`로.
`/etc/rancher/k3s/config.yaml.d/*.yaml` drop-in은 역할별 파일 분리(예: `10-networking.yaml`, `20-audit.yaml`)에 사용.
### 6. `/var/lib/rancher/k3s/server/manifests`는 apply sink, SoT 아님
- 운영 SoT = Git (+ Kustomize / ArgoCD / Flux)
- 이 디렉터리는 bootstrap addon에만 한정 (예: `k3s-addons-disabled.yaml` placeholder)
- 서버별로 다른 파일을 두고 "알아서 맞겠지"는 금지
- `.skip` 파일은 **임시** 비활성화 용. 장기 disable은 `--disable` 플래그로.
### 7. multi-server user AddOn은 Git push, 로컬 scp 금지
K3s는 user AddOn을 서버 간 동기화하지 않는다. 멀티 server 환경에서 AddOn을 쓰려면:
- GitOps 컨트롤러(ArgoCD/Flux)가 apply
- 또는 Ansible/Fleet이 단일 server 노드에만 drop
- 또는 완전히 포기하고 `kubectl apply`로만 관리 (권장)
### 8. packaged Helm component 커스터마이징은 `HelmChartConfig`
traefik 유지가 불가피할 때:
```yaml
apiVersion: helm.cattle.io/v1
kind: HelmChartConfig
metadata:
name: traefik
namespace: kube-system
spec:
valuesContent: |-
<override values>
```
- `metadata.name` / `namespace`는 대응 `HelmChart`와 반드시 일치
- 민감 값은 `valuesSecrets`로 Secret 참조 (valuesContent에 하드코딩 금지)
- HelmChartConfig 자체는 Git 관리
### 9. local-path provisioner는 dev/test 한정
Rancher Local Path Provisioner = node-local hostPath. 특성:
- ReadWriteOnce only
- 노드 장애 시 데이터 접근 불가
- 백업/DR 불가 (StorageClass 레벨 스냅샷 없음)
- binding mode = WaitForFirstConsumer (Pod가 뜰 때 PV 생성)
기준:
- dev/test StatefulSet의 PVC 기본값으로만 허용
- prod의 DB / Vault / MinIO / Kafka / etcd backup target에 절대 사용 금지
- prod storage는 **Longhorn (K3s 권장) / Ceph RBD / 외부 CSI** 중 택1
### 10. metrics-server는 유지 기본값
HPA v2 metrics, `kubectl top`, VPA, kube-state-metrics 연동 모두가 전제. disable 시 Prometheus Adapter 등 대체 pipeline을 먼저 준비한 뒤에만 꺼야 한다.
### 11. traefik / servicelb는 포트 점유 + 노드 노출 전략을 같이 본다
- `servicelb` (klipper) = 모든 노드가 80/443 HostPort로 열림. prod에서는 거의 항상 disable + MetalLB 또는 외부 LB.
- `traefik` 유지 시 IngressClass / Middleware / EntryPoint 세 레이어가 전부 K3s 관리. prod에서는 disable + `ingress-nginx` DaemonSet 또는 Envoy Gateway Deployment.
### 12. network policy controller 충돌
- 기본: K3s 내장 kube-router 기반 enforcer
- Cilium / Calico 도입 시: `--flannel-backend=none` + `--disable-network-policy` + `--disable=servicelb`
- 도입 계획은 클러스터 bootstrap 결정 사항 (리빌드 없이 swap 불가에 가까움)
### 13. embedded registry mirror (Spegel): 명시적 opt-in + 네트워크 요구사항
- 기본 **비활성**
- 활성화 방법: `/etc/rancher/k3s/config.yaml``embedded-registry: true` + `registries.yaml`에 mirror 설정
- **네트워크 요구사항** (공식): 모든 노드가 서로 **TCP 5001 (p2p gossip) + TCP 6443 (local registry + supervisor)**에 도달 가능해야 한다. firewall / security group에서 해당 포트 오픈 필수.
- 활성화 대상:
- airgap / 반-airgap 환경
- 이미지 pull bottleneck이 심한 대규모 배포
- external registry 의존을 낮춰야 하는 환경
- 클러스터 범위 기능이므로 **모든 server/agent에 동일 적용**
### 14. 이미지 import / airgap 전략
- 평상시: registry pull (internal mirror 선호)
- airgap: `/var/lib/rancher/k3s/agent/images/*.tar{,.zst,.gz}` 사용, import 절차를 runbook에 명시
- 이미지 import는 agent startup 때만 로드됨 → 런타임 교체는 re-push 필요
### 15. K3s version gating을 항상 확인
다음 기능은 버전에 따라 동작/옵션이 바뀌므로, 업그레이드 전 CHANGELOG 확인 필수:
- embedded registry mirror (Spegel)
- image pre-import
- `HelmChartConfig` schema
- `disable-helm-controller` 동작
- etcd snapshot / S3 backup 옵션
### 16. K3s-specific 예외는 component 문서보다 먼저 확정
이 문서에서 박고 내려가야 하는 결정:
- traefik 유지/비활성
- servicelb 유지/비활성
- local-storage 유지 범위 (env별)
- metrics-server 유지
- network policy controller 선택
- embedded registry mirror 사용 여부
그 다음에 keycloak / vault / minio / ingress / storage 문서로 내려간다.
### 17. `kubectl apply --server-side` 기본 사용
K3s도 SSA 지원. ArgoCD / Flux / CI 모두 `--server-side --field-manager=<id>` 기본. last-applied-configuration annotation 2MB 한계 회피 + multi-controller ownership 명시.
### 18. etcd snapshot은 K3s 고유 메커니즘 사용
- embedded etcd면 `k3s etcd-snapshot` CLI 또는 `--etcd-snapshot-*` config
- S3 업로드 설정은 `/etc/rancher/k3s/config.yaml`에 선언
- 외부 datastore(PostgreSQL/MySQL) 사용 시 backup은 해당 DB 레이어에서 따로
## 현재 스택 기본 권장안 (prod)
- `traefik`: disable, ingress-nginx + cert-manager로 교체
- `servicelb`: disable, MetalLB (L2 또는 BGP)로 교체
- `local-storage`: disable (prod), dev/staging 만 유지. Longhorn으로 교체
- `metrics-server`: keep (HPA 전제)
- `network policy`: 현 단계 kube-router 유지, Cilium 도입은 별 RFC
- `embedded registry mirror`: off (현재 airgap 아님), 옵션으로 남김
- `etcd snapshot`: S3 업로드 활성, 6시간 주기, 72시간 retention
- `HelmChartConfig`: traefik 유지 경로를 쓰지 않으므로 현재 미사용
- apply 방식: `kubectl apply --server-side --field-manager=argocd`
## 프로젝트 기준 요약
- K3s 전용 규칙은 별도 문서
- packaged component 직접 수정 금지 (HelmChartConfig / disable만)
- `manifests/`는 SoT 아님
- critical config는 Git 단일 파일, 서버 간 동일
- local-path는 dev/test만
- embedded registry mirror는 TCP 5001 + 6443 reachability가 전제
- prod에서 traefik/servicelb/local-storage 전부 disable이 기본
- Server-Side Apply가 GitOps 기본
+227
View File
@@ -0,0 +1,227 @@
# Keycloak 기준
## 목적
이 문서는 Kubernetes 환경에서 Keycloak 26+ (Quarkus distribution)을 1000+ 서비스의 ID 브로커로 운영하기 위한 기준을 고정한다.
- 빌드/실행 두 단계(`kc.sh build``kc.sh start --optimized`)를 전제한다
- Hostname v2, proxy-headers, management port 9000, Infinispan 캐시를 명시한다
- 단일 Deployment 수제 배포 대신 **Keycloak Operator**를 1차 권장 경로로 둔다
- DB / 캐시 / probe / Ingress / RealmImport 를 YAML이 아닌 "설계 결정"으로 먼저 고정한다
- auth-server(도메인 위임)와 Keycloak(IdP)의 ownership 경계를 분리한다
## 공식 의미 (Keycloak 26+ 기준)
- 운영 실행 방식은 **두 단계**다. `kc.sh build`가 Quarkus augmentation을 수행해 optimized 이미지를 만들고, `kc.sh start --optimized`가 그 이미지를 기동한다. 빌드 시 configuration은 런타임에 변경 불가능하다.
- **`--proxy` 옵션은 v24에서 deprecated, v26에서 제거되었다.** 대체는 `--proxy-headers=xforwarded` 또는 `--proxy-headers=forwarded`다.
- **Hostname v2**가 기본값이며 `--hostname`은 full URL을 받는다. v24+ 이후 `hostname-url`, `hostname-path`, `hostname-port`는 제거되었다. admin 전용 주소는 `--hostname-admin`으로 지정한다.
- `--hostname-strict`의 production 기본값은 `true`다. `--hostname-backchannel-dynamic`은 기본 `false`다.
- HTTPS 종료를 Ingress/LB가 하면 Keycloak은 `KC_HTTP_ENABLED=true`로 HTTP를 수신한다.
- DB는 `KC_DB=postgres`, `KC_DB_URL`**JDBC URL**(`jdbc:postgresql://host:5432/db`) 형식이다.
- **Management interface는 기본 포트 `9000`**에서 제공되고, `/health`, `/health/started`, `/health/ready`, `/health/live`, `/metrics`를 호스팅한다. Pod probe와 Prometheus scrape는 모두 9000 대상이다.
- Production cache type 기본은 `ispn`(Infinispan distributed). **cache-stack 기본값이 `kubernetes`(DNS_PING)에서 v25부터 `jdbc-ping`으로 바뀌었다.** Operator가 관리하는 StatefulSet은 Raft-less 클러스터링을 jdbc-ping으로 수행한다.
- Operator가 생성하는 워크로드는 **StatefulSet**이다 (pod ordering이 Infinispan discovery와 맞물린다). 사용자 수제 YAML에서도 Operator 경로가 1차 권장이다.
- `KeycloakRealmImport` CR은 Keycloak server가 준비된 후 realm JSON을 server side로 import하는 1회성 Job을 생성한다.
## 기본 규칙
### 1. 운영 실행은 `start --optimized` 두 단계
빌드 단계에서 feature/db/health/metrics를 굽고, 실행 단계에서 runtime config만 주입한다.
기본:
- Dockerfile에서 `RUN /opt/keycloak/bin/kc.sh build`로 optimized 이미지 생성
- 컨테이너 CMD는 `kc.sh start --optimized`
- runtime-only config: hostname, DB URL/credential, log level
기본 금지:
- `start-dev` 운영 사용
- `start` 단독 실행(build 없이 매 기동마다 augmentation)
### 2. `--proxy-headers` 사용, `--proxy` 금지
Keycloak 26에서 `--proxy`는 제거되었다.
기본:
- HTTPS 종료 proxy 뒤: `KC_PROXY_HEADERS=xforwarded` (nginx, Traefik, ingress-nginx 등)
- RFC 7239 지원 proxy: `KC_PROXY_HEADERS=forwarded`
- proxy가 Host / X-Forwarded-* 를 **덮어쓰도록** 고정
기본 금지:
- `KC_PROXY=edge|reencrypt|passthrough` 등 legacy 옵션
### 3. Hostname v2: full URL로 고정
기본:
- `KC_HOSTNAME=https://auth.example.com` (full URL)
- Admin Console 분리: `KC_HOSTNAME_ADMIN=https://admin-auth.example.com`
- `KC_HOSTNAME_STRICT=true` (production 기본 유지)
- `KC_HOSTNAME_BACKCHANNEL_DYNAMIC=false` (기본값; 다중 cluster federation일 때만 true 검토)
기본 금지:
- 제거된 옵션 사용: `KC_HOSTNAME_URL`, `KC_HOSTNAME_PATH`, `KC_HOSTNAME_PORT`
- hostname 없이 요청 헤더에서 해석되도록 방치
### 4. HTTPS는 Ingress/LB에서 종료, Pod는 HTTP
Pod 내부에서 TLS 재암호화가 필요 없으면 Pod는 HTTP로 수신한다.
기본:
- `KC_HTTP_ENABLED=true`, `KC_HTTP_PORT=8080`
- Ingress가 TLS 종료 + proxy-header 주입
- passthrough TLS가 필요한 보안 요구가 있을 때만 `KC_HTTPS_*` 경로 채택
### 5. DB는 외부 PostgreSQL + JDBC URL
기본:
- `KC_DB=postgres`
- `KC_DB_URL=jdbc:postgresql://keycloak-db-rw:5432/keycloak` (CloudNativePG `-rw` RW endpoint 권장)
- `KC_DB_USERNAME`, `KC_DB_PASSWORD` → Secret `secretKeyRef`
- Keycloak schema와 auth-server schema는 **다른 DB 또는 다른 database**로 분리
기본 금지:
- 내장 H2 (`dev-file`, `dev-mem`) 운영
- root/superuser credential 사용
- Keycloak DB에 auth-server migration 수행
### 6. Management port 9000은 외부 비공개
기본:
- Pod containerPort 9000 (`KC_HTTP_MANAGEMENT_PORT=9000`)
- Service에 9000 expose하되 Ingress 대상 제외
- probe는 9000 대상: `/health/started`, `/health/ready`, `/health/live`
- Prometheus scrape는 내부 scraper가 9000/`/metrics`에 직접 접근
### 7. Probe timing은 Keycloak 기동 특성에 맞춘다
Keycloak은 JVM + Quarkus + Infinispan + DB migration으로 cold start가 30~120초다.
기본:
- `startupProbe`: `/health/started`, `periodSeconds: 5`, `failureThreshold: 60` → 최대 5분 유예
- `readinessProbe`: `/health/ready`, `periodSeconds: 10`, `failureThreshold: 3`
- `livenessProbe`: `/health/live`, `periodSeconds: 30`, `failureThreshold: 3`, `initialDelaySeconds: 60`
### 8. Cache: Infinispan + 버전별 stack 기본값 인지
v25+ 기본 stack은 **`jdbc-ping`**이다. DB를 discovery 매체로 쓰므로 headless service / ServiceAccount RBAC가 필요 없다.
기본:
- Operator 관리 클러스터: `KC_CACHE=ispn`, `KC_CACHE_STACK=jdbc-ping` (명시)
- 수제 StatefulSet에서 headless service 경유 discovery를 쓰려면 `KC_CACHE_STACK=kubernetes` (DNS_PING) 선택
- local mode 운영 금지 (`KC_CACHE=local`은 single replica 테스트 전용)
### 9. Operator 경로를 1차 권장으로
1000+ 서비스 규모에서 realm import, CR 기반 롤아웃, cache stack 자동 설정, StatefulSet 관리를 Operator가 담당한다.
기본:
- `Keycloak` CR + `KeycloakRealmImport` CR 조합
- OLM(OperatorHub) 또는 공식 manifest 설치
- 수제 StatefulSet 유지보수는 Operator 기능이 부족할 때만 허용
### 10. 공개 경로 최소화
Ingress에 허용하는 기본 경로:
- `/realms/` — OIDC / SAML endpoint
- `/resources/` — Keycloak theme / JS
- `/.well-known/` — OIDC discovery, JWKS
- `/js/` — Keycloak adapter JS (필요 시)
기본 금지:
- `/admin/` 외부 공개 (별도 admin host 경유)
- `/metrics`, `/health*` 외부 공개
- `/` 전체 wildcard 공개
### 11. Admin Console은 별도 host로 분리
Admin 접근은 일반 SSO host와 다른 경로로 둔다.
기본:
- `KC_HOSTNAME_ADMIN=https://admin-auth.example.com`
- Admin host는 사내 IP 화이트리스트 / VPN / OIDC forward-auth로 추가 보호
- production에서 `/admin/` 을 SSO 공용 host에 노출 금지
### 12. Realm은 `KeycloakRealmImport` CR로 선언적 관리
기본:
- realm JSON은 Git에 보관
- `KeycloakRealmImport` CR이 Job을 생성해 server-side import
- secret이 들어가는 identity provider client secret은 Vault에서 주입
기본 금지:
- Admin REST / kcadm.sh를 CI/CD pipeline이 직접 호출해 상태 변경
- realm export 파일을 Pod 내부 파일로 배포
### 13. High Availability: replicas ≥ 2 + PDB + topologySpread
Operator는 `instances` 필드로 replica를 제어한다.
기본:
- `instances: 3` (odd quorum 아님 — cache replication 안정성)
- `PodDisruptionBudget minAvailable: 2`
- `topologySpreadConstraints`로 node/zone 분산
### 14. Sticky session은 성능 최적화 옵션
Infinispan이 session을 복제하므로 필수는 아니지만, login flow 중간 redirect 지연을 줄인다.
기본:
- Ingress controller에서 `AUTH_SESSION_ID` cookie affinity
- Service `sessionAffinity: ClientIP`는 2차 선택지
### 15. Security context: Restricted PSS 준수
기본:
- `runAsNonRoot: true`, `runAsUser: 1000`
- `readOnlyRootFilesystem: true` (Keycloak은 `/opt/keycloak/data` 만 writable 요구; emptyDir 마운트)
- `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`
- `seccompProfile: RuntimeDefault`
### 16. Resource 요청은 JVM 특성 반영
기본 단일 replica:
- requests: `cpu: 500m`, `memory: 1Gi`
- limits: `cpu: 2`, `memory: 2Gi`
- JVM: `JAVA_OPTS_APPEND=-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50`
login throughput 요구가 높으면 replica 수평 확장 우선 (JVM heap 수직 확장 2차).
### 17. Observability
기본:
- `KC_METRICS_ENABLED=true`, `KC_HEALTH_ENABLED=true`
- `ServiceMonitor` 또는 `PodMonitor`로 9000/`/metrics` scrape
- event metric (login failure, token issuance)은 필요한 것만 활성화 (high cardinality 방지)
### 18. DB credential / admin credential은 Vault 경유
기본:
- `KC_DB_PASSWORD`: VSO `VaultDynamicSecret`(postgres dynamic role) 또는 `VaultStaticSecret` → K8s Secret 동기화
- Bootstrap admin (`KEYCLOAK_ADMIN`, `KEYCLOAK_ADMIN_PASSWORD`): 최초 기동 후 제거, 실 운영 admin은 realm-managed
기본 금지:
- Secret을 Git에 평문 저장
- 환경변수 default value로 credential 하드코딩
### 19. 현재 스택 기본 권장안
- 배포: Keycloak Operator + `Keycloak` CR + `KeycloakRealmImport` CR
- 워크로드: StatefulSet (Operator 생성)
- Service: ClusterIP (9000, 8080)
- Ingress: SSO host + Admin host 분리
- DB: CloudNativePG PostgreSQL cluster + Vault dynamic secret
- Cache: `ispn` + `jdbc-ping`
- Probe: 9000 management port
- Replicas: 3 + PDB + topologySpread
## 프로젝트 기준 요약
- Keycloak 26+ Quarkus distribution, `start --optimized` 두 단계
- `--proxy-headers` 사용, `--proxy` 금지
- Hostname v2 full URL, admin host 분리, strict=true 유지
- DB: 외부 PostgreSQL, JDBC URL, Vault credential
- Management port 9000 내부 전용, probe / metrics 대상
- Infinispan `ispn` + `jdbc-ping` (v25+)
- Operator 경로 1차 권장 (CR로 realm import 포함)
- Admin Console 별도 host, 공개 경로는 `/realms/`, `/resources/`, `/.well-known/`
- Replicas ≥ 2 + PDB + topologySpread + Restricted PSS
+325
View File
@@ -0,0 +1,325 @@
# Kustomize 기준
## 목적
Kustomize는 Kubernetes 리소스를 **template-free**로 조합하고 환경별 차이를 overlay로 표현하는 도구다.
1000+ 서비스 prod 스케일에서 기본 배포 도구로 사용하며, Helm 차트는 특정 플랫폼 컴포넌트(Prometheus Operator, cert-manager 등)에만 제한적으로 쓴다.
목표:
- base / overlay / component 세 축을 명확히 구분한다
- `commonLabels`의 selector immutability 함정을 피한다
- `kubectl apply --server-side`를 전제로 field manager ownership을 관리한다
- GitOps (ArgoCD/Flux) 또는 CI `kubectl apply -k` 어느 쪽이든 같은 원본을 쓴다
## 공식 의미 (근거)
- 공식 문서: `https://kubernetes.io/docs/tasks/manage-kubernetes-objects/kustomization/`, `https://kubectl.docs.kubernetes.io/references/kustomize/`
- `kubectl kustomize <dir>` 렌더, `kubectl apply -k <dir>` apply, `kubectl diff -k <dir>` diff.
- **Kustomize v5+ `labels:` 필드**: label을 리소스에 추가하되 **기본적으로 selector에 주입하지 않는다** (`includeSelectors: false`). 공식 문서 인용: *"A field that allows adding labels without also automatically injecting corresponding selectors. This can be used instead of the `commonLabels` field, which always adds selectors."*
- **`commonLabels`**: 모든 리소스의 `metadata.labels` + `spec.selector.matchLabels` + Pod template labels에 주입된다. Deployment/StatefulSet의 `selector.matchLabels`**immutable** 이므로, 이미 apply된 리소스에 `commonLabels`로 label을 추가하면 `field is immutable` 에러로 apply 실패.
- **`components:`** (v4+): 재사용 가능한 cross-cutting overlay 단위. `kind: Component`. resource 집합 + patch 집합을 하나의 단위로 묶어 여러 overlay에서 `components:` 키로 참조.
- `configMapGenerator` / `secretGenerator`: 이름 끝에 hash suffix가 자동으로 붙어 rollout trigger. `generatorOptions.disableNameSuffixHash: true`로 비활성 가능.
- `patches:` (v5 권장): `target:` 선택 + `patch:` inline 또는 `path:` 파일. strategic merge / JSON patch 양쪽 지원.
- `images:`: image name/tag/digest 교체.
- `replicas:`: resource별 replica 수 override.
- `namespace:` / `namePrefix:` / `nameSuffix:`: overlay에서 공통 변환.
- Server-Side Apply (`kubectl apply --server-side --field-manager=<id> -k`)가 GitOps 기본.
## 기본 규칙
### 1. Kustomize 디렉터리가 선언형 source of truth
- 렌더: `kubectl kustomize <dir>`
- diff: `kubectl diff --server-side -k <dir>`
- apply: `kubectl apply --server-side --field-manager=<ci-id> -k <dir>`
`kubectl apply -f` 단일 파일 apply는 금지 (bootstrap 예외 제외).
### 2. base는 환경 중립
허용:
- Deployment/StatefulSet/DaemonSet/Job/CronJob 기본 shape
- `app.kubernetes.io/{name,instance,component,part-of,managed-by}` (`version`은 overlay에서 image tag와 함께 주입)
- 공통 container spec (resources, probes, securityContext)
- 공통 volume mount / ConfigMap reference
금지:
- replicas 고정값 (overlay `replicas:`에서 결정)
- 환경별 host / domain / issuer 이름
- 환경별 secret / ConfigMap 이름
- 환경별 resources requests/limits
- `example.com/environment` label (overlay에서 `labels:`로 주입)
### 3. overlay는 환경 차이만, patches는 파일로 분리
overlay 한 디렉터리의 `kustomization.yaml`은 짧아야 한다. diff가 몇 백 줄을 넘으면 base 설계 실패 신호.
권장 구조:
```
overlays/prod/
kustomization.yaml
patches/
auth-replicas.yaml
auth-resources.yaml
auth-topology-spread.yaml
ingress-host.yaml
postgres-storage.yaml
```
### 4. 디렉터리 구조는 base / components / overlays 3축
```
k8s/
base/
app/units/<domain>/<service>/
managing/<job>/
plugins/<platform>/
components/
<reusable-cross-cutting>/
overlays/
<env>/[region/]
```
`components/`는 "Kustomize Components"로, 여러 overlay에서 재사용.
### 5. `commonLabels` 금지, `labels:` 사용
신규 코드에서는 `commonLabels` 사용을 금지한다.
```yaml
# DO
labels:
- pairs:
example.com/environment: prod
example.com/region: kr-main
includeSelectors: false
includeTemplates: true
```
이유:
- `commonLabels``selector.matchLabels`에 자동 주입 → live Deployment/StatefulSet apply 시 `field is immutable` 실패
- `labels:``includeSelectors: false`가 기본 → safe
- `includeTemplates: true`로 Pod template labels에는 전파되므로 관찰성은 유지
기존 `commonLabels` 사용 코드는 migration plan을 세워 교체. selector에 이미 들어간 label이 있다면 해당 리소스를 **재배포** (delete + recreate) 없이는 변경 불가.
### 6. selector에는 불변 3종만
overlay에서 selector를 건드리지 않는다. selector에 허용되는 label은:
- `app.kubernetes.io/name`
- `app.kubernetes.io/instance`
- `app.kubernetes.io/component`
이 3종은 base에서 고정. overlay가 `labels:`로 추가하는 label은 반드시 `includeSelectors: false`.
### 7. `patches:` (v5 스타일) 사용, `patchesStrategicMerge` / `patchesJson6902` 금지
```yaml
patches:
- target:
kind: Deployment
name: auth
path: patches/auth-resources.yaml
- target:
kind: Ingress
name: auth-public
patch: |-
- op: replace
path: /spec/rules/0/host
value: auth.example.com
```
이유:
- 단일 키로 strategic merge + JSON patch 양쪽 지원
- `target:` selector로 여러 리소스에 적용 가능
- 레거시 `patchesStrategicMerge` / `patchesJson6902`는 v5에서 deprecated (여전히 작동하지만 신규 사용 금지)
### 8. `components:`로 cross-cutting 재사용
multiple overlay에서 공통으로 끼워야 하는 변경(예: mTLS 활성화, sidecar 주입, monitoring label 추가)은 component로.
```
components/
with-istio-sidecar/
kustomization.yaml # kind: Component
patches/
inject-sidecar.yaml
with-service-monitor/
kustomization.yaml
service-monitor.yaml
with-pdb-tier1/
kustomization.yaml
pdb-patch.yaml
```
overlay에서:
```yaml
components:
- ../../components/with-service-monitor
- ../../components/with-pdb-tier1
```
### 9. `namePrefix` / `nameSuffix`는 꼭 필요할 때만
리소스 이름이 바뀌면 ConfigMap/Secret 참조 (`envFrom`, `volumes.configMap.name`)도 모두 바뀐다. namespace 격리가 기본이고, 같은 cluster 안에서 같은 이름 리소스를 여러 번 생성할 때만 prefix/suffix를 쓴다.
### 10. generator 기준
- `configMapGenerator`: 비민감 설정만. 기본 hash suffix로 rollout 자동 트리거.
- `secretGenerator`: 로컬/테스트/bootstrap 에만. prod secret은 External Secrets Operator / Vault Secrets Operator / SealedSecrets로 관리.
- `generatorOptions.disableNameSuffixHash: true`는 GitOps 외부 컨슈머가 이름을 하드코딩해야 할 때만 (예외).
### 11. `images:`로 image tag/digest 고정
```yaml
images:
- name: registry.example.com/auth
newTag: "1.24.3"
- name: registry.example.com/keycloak
digest: "sha256:abcd1234..."
```
- prod에서는 digest 권장 (tag는 mutable)
- CI가 overlay의 `images:` 섹션을 빌드 후 새 digest로 patch (kustomize edit set image)
### 12. `replicas:`는 overlay에서 resource별 값 주입
```yaml
replicas:
- name: auth
count: 6
- name: keycloak
count: 3
```
HPA 주도 rollout 환경에서는 `replicas:` override가 HPA와 충돌할 수 있다. HPA 활성 리소스는 base `replicas`를 HPA `minReplicas`와 일치시키고 overlay에서는 건드리지 않는다.
### 13. `kubectl apply --server-side --field-manager=<id>` 기본
- ArgoCD: field manager `argocd-controller`
- Flux: field manager `kustomize-controller`
- CI manual: field manager `ci-<pipeline-id>`
field manager 이름을 환경별로 통일해야 `managedFields` 충돌이 예측 가능해진다.
### 14. render 전 검증
CI가 아래를 순서대로 실행:
```bash
kubectl kustomize overlays/prod > /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
```
- kubeconform / kubeval: schema validation
- kyverno / OPA Gatekeeper: policy validation (post-render)
- conftest: opa policy bundle 실행
### 15. base는 overlay를 모른다
공식 원칙. base `kustomization.yaml`은 overlay에서만 의미 있는 설정(환경 host / issuer / region label)을 전제하지 않는다. 위반 시 base가 더 이상 재사용 가능한 unit이 아니다.
### 16. Kustomize를 템플릿 엔진으로 남용하지 않는다
분기 / 조건 / 반복이 필요하면:
1. 리소스 분리
2. component 도입
3. overlay 추가
4. (마지막 수단) Helm / jsonnet / cdk8s
Kustomize는 patch/overlay 도구다. Go template이 아니다.
### 17. scripts는 Kustomize 보조, 대체 아님
`scripts/render.sh`, `scripts/diff.sh`, `scripts/apply.sh`는 Kustomize 명령의 wrapper에 그치고 overlay 구조를 우회하지 않는다.
### 18. `resources:` vs `bases:` — v5에서는 `resources:` 통일
v2.1에서 `bases:``resources:`로 통합됨. 신규 파일에서 `bases:` 금지.
### 19. overlay에서 StatefulSet PVC retention 변경 주의
`persistentVolumeClaimRetentionPolicy`는 StatefulSet spec 필드 (GA 1.27). 환경별로 값이 다르면 overlay patch로 조정하되 prod는 기본 `{whenDeleted: Retain, whenScaled: Retain}` 유지.
## 추천 폴더 구조
```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/
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
```
## 프로젝트 기준 요약
- Kustomize v5 문법 기준, `commonLabels` 금지, `labels:` 사용
- `patches:` 단일 키, `target:` + `path:` 또는 `patch:` inline
- `components:`로 cross-cutting 재사용
- selector에는 불변 3종만 (name / instance / component)
- generator는 configMap만 기본, secret은 External Secrets
- `kubectl apply --server-side --field-manager=<id>` 전제
- render + schema + policy 검증을 CI에서 강제
- base / components / overlays 3축 디렉터리
- overlay diff는 짧아야 한다 (base 재작성 금지)
+238
View File
@@ -0,0 +1,238 @@
# MinIO 기준
## 목적
이 문서는 Kubernetes 환경에서 MinIO를 1000+ 서비스의 S3 호환 object storage로 운영하기 위한 기준을 고정한다.
- MinIO Operator + **Tenant CRD** (`minio.min.io/v2`)를 기본 배포 모델로 둔다
- Erasure coding 최소 요건 (`servers × volumesPerServer ≥ 4`)을 명시한다
- KES sidecar + Vault transit backend로 SSE-KMS를 구성한다
- STS + OIDC (Keycloak)로 서비스 인증을 수행한다
- 버전 관리 / object lock / replication / lifecycle rule을 운영 필수 요소로 둔다
## 공식 의미 (MinIO Operator + Tenant CRD 기준)
- MinIO Operator는 `minio.min.io/v2` API group의 **Tenant** CR을 watch하여 StatefulSet, Service, PVC, 인증서를 자동 생성한다.
- Tenant는 **namespace당 1개**를 권장한다 (namespace = 소유/정책/쿼터 경계).
- MinIO는 erasure coding을 사용한다. **`servers × volumesPerServer`는 최소 4이어야** 기동한다. EC:N parity (기본 EC:4 ~ EC:8)는 parity drive 수를 결정하며, 장애 허용 drive 수 = parity 수.
- MinIO pool은 불변이다(immutable). pool 내 servers / volumesPerServer는 Tenant 생성 후 변경 불가. 용량 확장은 **새 pool 추가**로 수행.
- **Health endpoints**:
- `/minio/health/live` — 프로세스 liveness (인증 없음)
- `/minio/health/cluster`**write quorum** 기준 (rolling update 중 false 가능, readiness 비권장)
- `/minio/health/cluster/read`**read quorum** 기준 (rolling update 허용, readiness 권장)
- **Metrics endpoints**:
- `/minio/v2/metrics/cluster` — cluster-wide (기본 Bearer token 필요)
- `/minio/v2/metrics/node` — per-node
- `/minio/v2/metrics/bucket/api/<bucket>` — bucket API metrics
- `mc admin prometheus generate` 로 scrape config + token 생성. 또는 `prometheusAuthType: public` 설정으로 unauth scrape 허용.
- **KES** (Key Encryption Service)는 별도 sidecar/Deployment로 Vault transit backend와 통신해 SSE-KMS / SSE-S3 per-object key를 발급한다.
- MinIO **service account**는 root access key의 하위 derived credential이다(IAM role 개념 아님). 앱은 service account만 사용하고 root는 bootstrap 전용.
- **Object lock**은 bucket 생성 시점에 활성화해야 하며, `GOVERNANCE` (bypass 권한자 우회 가능) vs `COMPLIANCE` (root도 우회 불가) 두 모드.
- **Site replication**은 최대 16개 MinIO 클러스터를 동기화한다 (IAM, bucket config, object 전체). **Bucket replication**은 특정 bucket만 대상.
- Console은 9090 port, API는 9000 port.
## 기본 규칙
### 1. 배포는 MinIO Operator + Tenant CR
기본:
- `kubectl apply -k "https://github.com/minio/operator?ref=v6.0.4"` 또는 Helm `minio-operator` + `tenant` chart
- Tenant CR로 pool, credential, TLS, KES, logging, monitoring 선언
- 수제 StatefulSet 운영 금지
### 2. Tenant는 namespace당 1개
기본:
- `minio-prod` namespace에 Tenant 1개
- 테넌트 간 격리가 필요하면 namespace를 복수 생성
- 동일 namespace에 다른 워크로드와 공존 금지
### 3. Erasure coding 요건: `servers × volumesPerServer ≥ 4`
기본 topology 후보:
| servers | volumesPerServer | 총 drive | 기본 EC parity | 장애 허용 drive |
|---------|------------------|----------|----------------|-----------------|
| 4 | 4 | 16 | EC:4 | 4 |
| 4 | 8 | 32 | EC:4 | 4 |
| 8 | 4 | 32 | EC:4 | 4 |
| 8 | 8 | 64 | EC:4 ~ EC:8 | 4 ~ 8 |
기본:
- 최소 `4 × 4 = 16` drive 시작 (prod)
- `MINIO_STORAGE_CLASS_STANDARD=EC:4` 이상, critical data는 `EC:8`
- parity 증가 = 용량 감소 + 신뢰성 증가
기본 금지:
- `servers × volumesPerServer < 4` → Tenant가 기동 실패
### 4. Pool은 immutable — 확장은 새 pool 추가
기본:
- 초기 pool의 `servers`, `volumesPerServer`, `volumeClaimTemplate.size`는 평생 고정
- 용량 부족 시 `spec.pools[]``pool-1`, `pool-2` 추가
- pool 간 데이터 rebalance는 `mc admin rebalance start`
### 5. StorageClass 명시 (local-path 금지)
기본:
- prod: `volumeClaimTemplate.spec.storageClassName: ceph-rbd-retain` / `ebs-gp3` / `local-volume-xfs` (명시적)
- 파일시스템은 `xfs` 권장 (MinIO는 ext4보다 xfs에 최적화)
- `reclaimPolicy: Retain` + PVC 삭제 가드 (Tenant 삭제 시 데이터 소실 방어)
기본 금지:
- k3s local-path prod 사용
- default StorageClass fallback
### 6. Credential: root는 bootstrap 전용, 앱은 service account
기본:
- `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 발급
- service account는 최소 권한 policy 바인딩
### 7. TLS는 기본 활성화
기본:
- `spec.requestAutoCert: true` → Operator가 Kubernetes CSR로 인증서 자동 발급 (MinIO 자체 CA)
- 사내 PKI 사용 시 `spec.externalCertSecret` + cert-manager Certificate
- API (9000), Console (9090), KES 전부 TLS
### 8. KES + Vault transit으로 SSE-KMS
기본:
- `spec.kes` 필드에 KES 사이드카 spec
- KES는 Vault transit engine을 key store로 사용
- bucket 생성 시 `mc encrypt set sse-kms minio-backup/critical key-id=my-app-key`
- per-object DEK를 KES에서 받아 암호화
기본 금지:
- KES 없이 SSE-S3만 사용 (master key가 MinIO 내부에만 존재 → 분실 위험)
- KES가 local filesystem key store 사용 (prod)
### 9. Versioning + Object Lock은 critical bucket 기본값
기본:
- 금융/감사 데이터: `mc version enable` + Object Lock `COMPLIANCE` 모드
- 백업 bucket: Object Lock `GOVERNANCE` + retention 30일
- 일반 app bucket: versioning만 (실수 복구)
- lifecycle rule로 오래된 버전 자동 정리 (`mc ilm add --expire-noncurrent-days 90`)
### 10. Replication: site vs bucket
기본:
- 전체 IAM/config 동기화 필요: **site replication** (`mc admin replicate add`)
- 특정 bucket만 cross-region 복제: **bucket replication** (`mc replicate add`)
- async replication 특성 인지 (RPO > 0)
- `mc mirror`는 DR 전략 아님 — 일회성 migration/sync 용도
### 11. STS + OIDC (Keycloak) 통합
기본:
- Keycloak에 `minio` client 생성 (confidential)
- MinIO 설정:
```
mc admin config set ALIAS identity_openid \
config_url="https://auth.example.com/realms/platform/.well-known/openid-configuration" \
client_id="minio" \
client_secret="..." \
claim_name="policy" \
scopes="openid,profile,email"
```
- 앱은 `AssumeRoleWithWebIdentity`로 JWT → 임시 STS credential 교환
- MinIO policy에 JWT `policy` claim으로 매핑
### 12. Health probe: read quorum을 readiness로
기본:
- `livenessProbe`: `/minio/health/live` (프로세스 생존)
- `readinessProbe`: `/minio/health/cluster/read` (read quorum) — rolling update 허용
- `startupProbe`: `/minio/health/live` + `failureThreshold` 넉넉하게
기본 금지:
- `readinessProbe`로 `/minio/health/cluster` (write quorum) 사용 → rolling update 시 전체 pod unready
### 13. Metrics: 내부 scrape 전용
기본:
- `spec.prometheus` 또는 `prometheusAuthType: public` (내부 network만)
- 또는 `mc admin prometheus generate` 로 scrape token 발급 후 `bearerTokenSecret`
- ServiceMonitor는 `/minio/v2/metrics/cluster` 대상
- 외부 Ingress 공개 금지
### 14. API Ingress — Console은 내부 전용
기본:
- API (9000): 필요한 경우 Ingress로 공개 (S3 API host: `s3.example.com`)
- Console (9090): 내부/운영자 전용, 외부 공개 금지 (별도 host + IP whitelist + OIDC forward-auth)
- Console을 공개하면 root credential UI 로그인 표면 확장
### 15. Lifecycle rule로 용량 관리
기본:
- 로그 bucket: 30~90일 expire
- tmp / cache bucket: 7일 expire
- versioning enabled bucket: noncurrent version 90일 expire
- incomplete multipart upload: 7일 abort (`mc ilm add --expire-incomplete-upload-days 7`)
### 16. 로깅
기본:
- `spec.log.audit` → bucket에 audit log 저장 또는 webhook으로 외부 전송
- stdout으로 console log → fluent-bit / Loki 수집
- audit log는 Object Lock bucket에 저장해 변조 방지
### 17. SecurityContext + Resource
기본:
- `spec.securityContext`: `runAsNonRoot: true`, `runAsUser: 1000`, `fsGroup: 1000`, `runAsGroup: 1000`
- Restricted PSS 준수
- 단일 pod resource (4 server cluster 기준):
- requests: `cpu: 500m`, `memory: 2Gi`
- limits: `cpu: 4`, `memory: 8Gi`
- 데이터 규모/동시 요청 수에 따라 조정
### 18. Anti-affinity + topologySpread
기본:
- `podAntiAffinity`: hostname 기준 required (같은 node에 MinIO pod 복수 금지)
- `topologySpreadConstraints`: zone 분산
- EC:4 + 4-zone = 1 zone 장애 허용
### 19. Console은 분리, auth는 OIDC
기본:
- Console endpoint에 `MINIO_IDENTITY_OPENID_*` OIDC 설정
- root credential UI 로그인은 break-glass 전용
- 일반 운영자는 OIDC 로그인 + group → policy 매핑
### 20. 현재 스택 기본 권장안
- 배포: MinIO Operator + Tenant CR (`minio.min.io/v2`)
- Topology: 최소 `4 × 4 = 16` drive, prod는 `8 × 4 = 32` 이상
- EC: `EC:4` 기본, critical data `EC:8`
- StorageClass: 명시적 (xfs, Retain)
- TLS: `requestAutoCert: true`
- KMS: KES sidecar + Vault transit
- 인증: root는 VSO 주입, 앱은 service account, 사용자는 Keycloak OIDC
- Health: live / cluster-read (readiness)
- Metrics: Prometheus bearer-token scrape
- Versioning + Object Lock: critical bucket 기본값
- Replication: site (전체) / bucket (부분) 구분
- Console: 내부 전용
## 프로젝트 기준 요약
- MinIO Operator + Tenant CR 기본 배포
- namespace당 Tenant 1개
- `servers × volumesPerServer ≥ 4` erasure coding 요건
- Pool immutable — 확장은 새 pool
- StorageClass 명시 + xfs 권장 + Retain
- KES + Vault transit으로 SSE-KMS
- Root credential은 Vault → VSO → Secret 경로
- 앱 접근은 service account, 사용자는 Keycloak OIDC STS
- Health: `/minio/health/live` + `/minio/health/cluster/read`
- Metrics: Bearer token scrape
- Console 외부 비공개, API만 필요 시 Ingress
- Versioning + Object Lock + Lifecycle rule로 데이터 보호
- site/bucket replication으로 DR (`mc mirror`는 DR 아님)
+170
View File
@@ -0,0 +1,170 @@
# network / ingress / TLS 기준
## 목적
이 문서는 K3s/Kubernetes(1000+ 서비스) 환경에서
- 어떤 Service 타입을 언제 쓸지
- 외부 공개는 Ingress/Gateway 어디로 할지
- TLS를 어디서 종료할지
- 인증서는 누가 발급·회전할지
- NetworkPolicy로 L3/L4 경계를 어떻게 그을지
를 단일 ground truth로 고정한다.
이 문서의 목표는 다음과 같다.
- 외부 attack surface를 최소화한다
- `ClusterIP`/`NodePort`/`LoadBalancer`/`Ingress`의 역할을 섞지 않는다
- 모든 Ingress는 cert-manager 발급 TLS + HTTPS redirect + HSTS + TLS 1.2+ 기본
- K3s 기본 Traefik을 유지하되 packaged manifest는 수정하지 않는다
- Keycloak/Vault/DB 같은 민감 컴포넌트의 노출 범위를 manifest로 증명한다
## 공식 의미 (근거)
- Service 기본 타입은 `ClusterIP`. 외부 L4 노출은 `NodePort` 또는 `LoadBalancer`, 외부 L7은 Ingress 또는 Gateway API.
- Ingress v1 API는 GA이지만 spec은 frozen 상태이고, 신규 기능(L4, traffic split, header match)은 Gateway API로 이동 중이다.
- Ingress v1은 `spec.ingressClassName` 필드로 컨트롤러를 선택한다. 이전의 `kubernetes.io/ingress.class` annotation은 deprecated이며 1.22에서 공식 deprecation 고지.
- Ingress TLS Secret은 타입이 `kubernetes.io/tls`이고 data key는 `tls.crt`, `tls.key`여야 한다. `spec.tls[].hosts``rules[].host`는 일치해야 한다.
- cert-manager는 `Issuer`/`ClusterIssuer`, `Certificate`, `CertificateRequest`, `Order`, `Challenge` CRD로 구성된다. `Certificate`가 참조하는 `secretName`에 자동으로 `kubernetes.io/tls` Secret이 생성·갱신된다.
- ACME HTTP-01은 public DNS + 80 reachable 필요. DNS-01은 wildcard(`*.example.com`) 발급에 필수이며 DNS provider API credential이 요구된다.
- Traefik v2/v3는 `IngressRoute`(CRD) + `Middleware`(CRD)로 L7 정책(redirect, HSTS, rate-limit, auth)을 체계적으로 구성한다. 기본 Ingress API도 annotation으로 일부 기능을 쓸 수 있다.
- K3s는 Traefik을 packaged component로 설치한다(`/var/lib/rancher/k3s/server/manifests/traefik.yaml`). packaged manifest 직접 수정은 재설치 시 덮어쓰인다. `HelmChartConfig`로 override한다.
- NetworkPolicy는 CNI가 지원해야 enforce된다. K3s 기본 flannel + kube-router policy controller는 v1 NetworkPolicy를 지원한다.
## 기본 규칙
### 1. 기본 Service 타입은 `ClusterIP`
- 내부 통신: `ClusterIP`
- 외부 HTTP/HTTPS: Ingress
- 외부 TCP/UDP L4: `LoadBalancer`(ServiceLB/MetalLB/클라우드 LB 전제)
- `NodePort`는 개발/bootstrap 용도 외 운영 금지. namespace `ResourceQuota.services.nodeports: 0`으로 선제 차단.
### 2. 모든 Service는 named port + `appProtocol`
```yaml
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
appProtocol: http
```
- `name: http|https|grpc|metrics`로 명명.
- `appProtocol` 명시는 Ingress controller/서비스 메시가 L7 처리를 최적화할 수 있게 한다.
- container `ports[].name`과 Service `targetPort`를 이름으로 연결해 포트 번호 drift를 방지.
### 3. Ingress는 `spec.ingressClassName: traefik` 필수
- `kubernetes.io/ingress.class` annotation은 **deprecated**. 어떤 Ingress에도 남기지 않는다.
- 컨트롤러가 여러 개인 클러스터(예: Traefik + internal-only NGINX)는 `IngressClass` 리소스를 만들어 class를 명시한다.
- 기본 클래스는 `ingressclass.kubernetes.io/is-default-class: "true"` annotation으로 한 개만 지정.
### 4. 외부 HTTPS는 cert-manager ClusterIssuer로 발급
- 운영 공인 도메인: `letsencrypt-prod` ClusterIssuer(ACME HTTP-01) 기본.
- Wildcard/internal CA: DNS-01(`letsencrypt-prod-dns`) 또는 Vault PKI issuer.
- Staging 검증: `letsencrypt-staging` ClusterIssuer로 선행 테스트 후 prod 전환.
- Ingress에는 annotation으로 issuer 지정: `cert-manager.io/cluster-issuer: letsencrypt-prod`. cert-manager가 Certificate + Secret을 자동 생성·갱신한다.
- Certificate CRD를 명시적으로 선언하는 방식도 허용(공유 Secret 재사용, 세밀한 `duration`/`renewBefore` 제어 필요 시).
### 5. HTTPS redirect + HSTS + TLS 1.2+ 기본
- 모든 외부 Ingress는 HTTP → HTTPS 영구 리다이렉트.
- HSTS: `max-age=31536000; includeSubDomains; preload` 기본.
- TLS minVersion: `VersionTLS12`(가능하면 1.3). 취약 cipher(RC4, 3DES) disable.
- Traefik에서는 `Middleware`(redirectScheme, headers) + `TLSOption` CRD로 정책을 선언. Ingress annotation 방식 예:
- `traefik.ingress.kubernetes.io/router.entrypoints: websecure`
- `traefik.ingress.kubernetes.io/router.middlewares: default-hsts@kubernetescrd,default-https-redirect@kubernetescrd`
- `traefik.ingress.kubernetes.io/router.tls: "true"`
### 6. Ingress host는 환경별로 분리, wildcard 남용 금지
- dev/staging/prod별 호스트 분리(`auth.dev.example.com`, `auth.staging.example.com`, `auth.example.com`).
- Wildcard 인증서는 플랫폼 수준 Certificate로 관리하고 서비스 Ingress가 `secretName` 재사용.
- `defaultBackend`(host 없음) 금지. host가 명시된 rule만 허용.
### 7. 외부 공개 범위 = "반드시 공개해야 하는 path"만
- Keycloak: `/realms/`, `/resources/`, `/.well-known/`만 노출. `/admin/`, `/metrics`, `/health`는 공개 금지.
- 관리 포트(Keycloak 9000, Vault 8201, Postgres 5432, Redis 6379)는 Ingress 경유 금지.
- 내부 도구(Argo CD, Grafana, Kibana)는 VPN/zero-trust proxy(예: Pomerium, cloudflared tunnel)로만 노출.
### 8. TLS 종료 위치와 내부 재암호화 정책
- 기본: Ingress(Traefik)에서 TLS 종료, 내부 Pod까지는 ClusterIP 경유 평문.
- 민감 backend(Vault, Keycloak token endpoint)는 **Ingress→Pod 재암호화** 검토. Traefik `serversTransport` + `insecureSkipVerify: false`로 backend TLS 사용.
- E2E mTLS가 필요하면 서비스 메시(Linkerd/Istio) 도입을 별도 ADR로 결정.
### 9. K3s 기본 Traefik은 유지·격리
- packaged manifest(`traefik.yaml`) 직접 수정 금지.
- 커스터마이징은 `HelmChartConfig`(`kind: HelmChartConfig` in `helm.cattle.io/v1`)로 override.
- Traefik은 `ingress-traefik` namespace에 격리, PSA `baseline`, NetworkPolicy는 80/443/8443 inbound + 모든 app namespace outbound 허용.
### 10. ServiceLB(klipper-lb) / MetalLB 결정
- 단일 노드 또는 on-prem 초기: K3s ServiceLB.
- 다중 노드 + BGP/ARP 정책이 필요: MetalLB(`kubectl get deploy -n kube-system | grep servicelb`가 없어야 함, `--disable=servicelb`로 off).
- 클라우드(EKS/GKE/AKS): cloud-provider LoadBalancer가 우선.
- 이 결정이 ADR로 고정되기 전에는 `LoadBalancer` Service를 새로 만들지 않는다.
### 11. NetworkPolicy는 namespace default-deny 기본
모든 운영 namespace는 다음 3종 + 서비스별 allow가 기본 세트다.
1. `default-deny-all` (ingress+egress)
2. `allow-dns-egress` (to `kube-system` `k8s-app=kube-dns`, 53/UDP+TCP)
3. `allow-from-ingress-traefik` (특정 app Pod만 허용)
### 12. NetworkPolicy `from`/`to` 엔트리 AND/OR 규칙
- **동일 엔트리 내 `namespaceSelector`+`podSelector`** → AND(교집합). 권장 패턴.
- **별도 엔트리로 분리** → OR(합집합). 거의 항상 버그.
- `ipBlock`은 같은 엔트리 내 `namespaceSelector`/`podSelector`와 함께 쓸 수 없다. 외부 CIDR allow는 별도 엔트리.
### 13. egress NetworkPolicy는 DNS 먼저, 서비스별 allow 나중
- `default-deny-all`만 적용하면 DNS 해석 실패로 앱이 기동 불가.
- kube-dns 53/UDP+TCP가 첫 번째 allow.
- 외부 API(OIDC issuer, SMTP, S3)는 FQDN이 아니라 IP CIDR로 나와야 v1 NetworkPolicy로 표현 가능. FQDN 기반 egress가 필요하면 Cilium `CiliumNetworkPolicy` 또는 egress gateway 검토.
### 14. Prometheus scrape는 ingress rule로 열기
- `monitoring` namespace의 Prometheus Pod만 허용.
- `namespaceSelector: kubernetes.io/metadata.name=monitoring` + `podSelector: app.kubernetes.io/name=prometheus` AND.
- 포트는 `metrics`(9090/9100 등) 전용, 앱 `http` 포트 재사용 금지.
### 15. Gateway API는 단계적 도입
- 신규 요구사항(traffic split, header routing, gRPC filter)이 Ingress v1으로 표현 불가하면 Gateway API 검토.
- 전환은 서비스 단위로 Ingress → `HTTPRoute`로 마이그레이션. `GatewayClass`/`Gateway`는 platform-team 소유.
### 16. health/metrics/admin endpoint 외부 공개 금지
- `/actuator/*`, `/debug/pprof/*`, `/admin/*`, `/metrics`는 Ingress path에 포함하지 않는다.
- 별도 Service 포트(`name: metrics`)를 만들고 NetworkPolicy로 Prometheus만 허용.
### 17. Ingress 경로 설계는 prefix + 명시 + 최소
- `pathType: Prefix` 명시(`ImplementationSpecific` 금지).
- `/`를 바로 노출하기 전 사용자 경로만 선언 가능한지 검토(Keycloak 패턴 참조).
- Path rewrite가 필요하면 Traefik `Middleware.stripPrefix`를 사용하고 annotation으로 명시.
### 18. ExternalName/headless Service는 용도에 맞춰
- `ExternalName`은 클러스터 외부 CNAME alias 용도. 인증/TLS 경계와 별개 고려.
- Headless(`clusterIP: None`)는 StatefulSet DNS, client-side LB 용도. Ingress 대상 아님.
### 19. 현재 스택 기본 권장안
#### auth-server / test-server
- Service: `ClusterIP` with named `http`, `metrics`
- Ingress: `ingressClassName: traefik`, cert-manager `letsencrypt-prod`, HSTS + HTTPS redirect
- NetworkPolicy: default-deny + dns + ingress-traefik + db + vault + prometheus
#### keycloak
- Service: `ClusterIP`, named `http`(8080), `management`(9000)
- Ingress: `/realms/`, `/resources/`, `/.well-known/`만 노출. 9000 포트는 Service로도 cluster 외부 비공개.
- Certificate: 전용(`sso.example.com`), 전용 TLS Secret
#### vault / db / migration-flyway
- Ingress 없음. ClusterIP only. 접근은 bastion + `kubectl port-forward` 또는 zero-trust proxy.
#### minio
- API/Console Ingress 분리. Console은 내부 전용, API는 필요 시 signed URL 중심.
#### ingress-traefik
- `ingress-traefik` namespace 격리, PSA `baseline`
- `Service type=LoadBalancer`(ServiceLB/MetalLB) 또는 `hostPort` 80/443만
## 프로젝트 기준 요약
- 기본 Service 타입은 `ClusterIP`, named port 필수
- 모든 Ingress는 `spec.ingressClassName: traefik`, annotation `kubernetes.io/ingress.class` 금지
- 모든 외부 HTTPS는 cert-manager ClusterIssuer 발급 + HSTS + HTTP→HTTPS redirect + TLS 1.2+
- Keycloak/Vault/DB 노출 범위는 path/host로 증명, 관리 포트 비공개
- K3s Traefik은 packaged manifest 직접 수정 금지, `HelmChartConfig` override
- NetworkPolicy default-deny + DNS allow + ingress-traefik allow 기본 세트
- `namespaceSelector`+`podSelector` AND/OR 차이를 정확히 사용
- Gateway API는 단계적 도입, 기존 Ingress 유지
@@ -0,0 +1,219 @@
# observability / health 기준
## 목적
이 문서는 1000+ 서비스가 공통으로 따르는 observability 기준선이다. metrics 수집 경로, golden signal 정의, 로그 포맷 / 수집 stack, trace 수집(OTel), health endpoint 외부 비공개 원칙, cardinality 가드를 한 파일에 고정한다.
## 공식 / 업계 근거
- **Google SRE Book (Ch.6)**: Four Golden Signals = **Latency, Traffic, Errors, Saturation**. 운영 대시보드의 기본 구성 원칙.
- **RED method (Tom Wilkie, Weaveworks)**: request-driven service에 대해 **Rate, Errors, Duration**.
- **USE method (Brendan Gregg)**: resource에 대해 **Utilization, Saturation, Errors**.
- **kube-prometheus-stack**: Prometheus Operator를 통한 `ServiceMonitor` / `PodMonitor` CRD가 primary scrape path.
- **Prometheus annotation fallback**: `prometheus.io/scrape: "true"` 등은 Operator가 없을 때만 사용.
- **OpenTelemetry**: OTLP protocol + OTel Collector (Deployment gateway + DaemonSet agent) 가 표준.
- **Log shipping canonical stacks**: Loki + Grafana Alloy (또는 Promtail) / Fluent Bit → OpenSearch. 한 플랫폼에서 둘 이상 섞지 않는다.
- `kubectl events` (1.27+ stable) — 기존 `kubectl get events`보다 sort/watch 기본 제공.
- metrics-server: HPA/VPA와 `kubectl top` 을 위한 최소 resource metric. full metrics와 분리.
## 기본 규칙
### 1. Four Golden Signals를 모든 서비스 대시보드의 골격으로
각 traffic-facing service는 최소 4개 signal을 노출한다.
- **Latency**: `request_duration_seconds` histogram (p50/p95/p99).
- **Traffic**: `requests_per_second` by method/status.
- **Errors**: `error_rate` (5xx / 전체).
- **Saturation**: resource utilization (CPU / memory / connection pool / queue depth).
SLO / alert / dashboard가 이 4개에서 시작한다.
### 2. RED는 request-driven, USE는 resource에 쓴다
- HTTP / gRPC 서비스 → **RED**.
- Node / disk / CPU / DB pool → **USE**.
- 두 방법론을 동시에 활용 가능 (golden signal은 양쪽 합집합).
### 3. ServiceMonitor / PodMonitor 를 primary scrape path로
kube-prometheus-stack을 운영하는 플랫폼에서는 `ServiceMonitor` CRD가 표준이다.
- `selector.matchLabels` 로 대상 Service 매칭.
- `namespaceSelector` 명시 (암묵적 전체 허용 금지).
- `endpoints[].port`**named port**, 숫자 port 금지.
- `interval` (기본 30s), `scrapeTimeout` (interval < interval) 명시.
- `scheme` (http/https) 명시.
- `bearerTokenSecret` / `tlsConfig` 로 인증 scrape.
- `relabelings` 로 label 위생 (pod_template_hash drop 등).
Pod에 직접 연결되는 경우 (Service가 없는 워크로드) `PodMonitor` 사용.
### 4. Annotation-based scrape 는 fallback
`prometheus.io/scrape: "true"` 계열 annotation은 Prometheus가 Operator 없이 kubernetes_sd_configs로 직접 discover하는 방식이다. ServiceMonitor 대비 label relabel / auth / tls 제어가 약하다.
- kube-prometheus-stack이 있는 환경: **사용 금지**, ServiceMonitor 통일.
- legacy / 교체 진행 중인 플랫폼: 전환 기간 동안만 사용.
지원 annotation:
- `prometheus.io/scrape: "true"`
- `prometheus.io/port: "8081"`
- `prometheus.io/path: "/metrics"`
- `prometheus.io/scheme: "http"`
### 5. metrics port는 외부 비공개, NetworkPolicy로 scraper만 허용
- `/metrics` 는 절대 Ingress 경로에 노출하지 않는다.
- metrics port는 별도 containerPort (ex: 8081, 9000).
- NetworkPolicy로 **monitoring namespace의 prometheus pod만** 해당 port에 ingress 허용.
### 6. Cardinality는 label 설계 단계에서 가드
Prometheus TSDB에서 **각 label value 조합 = 새 time series**. cardinality 폭발은 쿼리 OOM / storage 폭증의 가장 흔한 원인.
금지 label:
- `user_id`, `tenant_id` (높은 기수) — 대신 top-N aggregation 또는 별도 logging.
- `path` (path에 UUID / numeric ID 포함) — template된 route로 바꾼다 (`/users/:id`).
- `url` 전체, `request_id`, `trace_id`, `session_id`.
- timestamp, epoch value.
허용 label 예:
- `method` (GET/POST/…), `status_code` (bucketed 2xx/4xx/5xx가 더 안전), `route` (template).
규칙: **한 metric당 series 수 ≤ 10,000** 목표. 10만 넘어가면 review.
### 7. Histogram 을 p99 표현 기본값으로
- summary는 aggregatable 하지 않다 (서비스 간 p99 합산 불가).
- `histogram_quantile()` 를 위한 `_bucket` + `_count` + `_sum` 를 쓴다.
- bucket boundary는 SLO에 맞춰 튜닝 (`le: 0.05, 0.1, 0.25, 0.5, 1, 2.5, 5, 10`).
### 8. 로그는 JSON structured, stdout/stderr 로만
application log는 **JSON one-line per record**, stdout/stderr로 출력. PVC / hostPath / 컨테이너 내부 file 금지.
필수 field:
- `timestamp` (ISO 8601 RFC3339, UTC).
- `level` (`DEBUG`/`INFO`/`WARN`/`ERROR`).
- `service` (= `app.kubernetes.io/name`).
- `trace_id`, `span_id` (OTel에서 주입).
- `message`.
- `error` (object with `type`, `message`, `stacktrace` when level=ERROR).
- optional: `user_id` (hashed), `request_id`, `http_status`.
### 9. 로그 수집 stack은 한 플랫폼당 하나
canonical choice:
- **Loki + Grafana Alloy (권장)**: 낮은 storage cost, Grafana 통합.
- **Fluent Bit → OpenSearch/Elasticsearch**: full-text search 중심, 높은 storage cost.
플랫폼 하나에서 둘 다 운영하지 않는다. AI agent가 매니페스트 생성할 때 플랫폼 선택을 context에서 받아 일관되게 적용한다.
node-level: DaemonSet으로 agent 배포 → tail `/var/log/containers/*.log`.
### 10. 민감정보는 로그 금지 + 자동 masking
금지:
- access/refresh token, bearer, API key.
- DB password, connection string의 password 부분.
- Vault secret value.
- full Authorization header.
- PII (email, phone, SSN 등) 원문.
구현:
- logging framework의 structured field 에서만 쓰고 `toString()` 흐름 차단.
- 중앙 수집 파이프라인에 redaction filter 추가.
- 의심스러운 pattern은 debug 로그에서도 masking.
### 11. OpenTelemetry / OTLP를 trace / metrics 통로로
- 애플리케이션: OTel SDK로 계측, OTLP (gRPC 4317 또는 HTTP 4318) 로 export.
- 수집: **OTel Collector DaemonSet (agent)****OTel Collector Deployment (gateway)** → backend (Tempo / Jaeger / New Relic / Datadog).
- gateway에서 sampling / tail-based sampling / PII scrubbing 적용.
- app은 cluster 내부 agent endpoint만 알면 됨 (localhost:4317 → DaemonSet).
### 12. health / metrics / admin endpoint 는 외부 비공개 기본값
외부 비공개 대상:
- `/health`, `/health/*`, `/actuator/*`.
- `/metrics`.
- `/admin`, `/internal`, `/debug`.
- Keycloak management port 9000.
- Vault `/sys/*` endpoint.
외부 공개는 명시적 review 필요.
### 13. probe는 health endpoint 와 목적을 구분
- probe용 endpoint는 shallow, 빠른 응답.
- 운영자 점검용 deep health는 별도 endpoint (ex: `/ops/deep-health`), 인증 필요.
- Prometheus 가 `/metrics` 를 스크레이프하더라도 probe가 `/metrics` 를 쓰지 않는다 (cost 문제).
### 14. `kubectl events` 를 기본 event 조회 수단으로 (1.27+)
Kubernetes 1.27+ 부터 `kubectl events` 가 stable.
- `kubectl events -A --watch` — cluster-wide live view.
- `kubectl events -n <ns> --for pod/<name>` — 특정 오브젝트.
- `kubectl events --types=Warning` — 경고만.
`kubectl get events` 대비 sort-by-timestamp 기본, watch 안정적.
### 15. 알림 기준: Golden Signal 에 SLO 를 먼저 정의
- availability SLO: 99.9% / 99.95% 등.
- latency SLO: p99 < 500ms.
- error budget: (1 - SLO) × 기간.
- alert는 **burn rate** 기준 (1h/6h fast burn + 6h/3d slow burn 이중 창).
단순 "CPU > 80%" alert 는 actionable 하지 않다 (saturation은 dashboard용, 알림은 SLO 기반).
### 16. 워크로드별 기본 권장안
#### auth-server (Spring Boot)
- metrics: micrometer + prometheus registry, `/actuator/prometheus`.
- ServiceMonitor with named port `metrics` (8081).
- tracing: OTel Java agent, OTLP to DaemonSet.
- logging: logback JSON encoder → stdout.
#### keycloak
- metrics: management port 9000 `/metrics`.
- ServiceMonitor 대상, `/admin``9000` 외부 비공개.
- event metric cardinality는 `event_type` level 까지만, user / session ID 금지.
#### vault
- `/sys/metrics?format=prometheus` (token 필요) → ServiceMonitor with `bearerTokenSecret`.
- `/sys/health` 는 sealed/standby 구분해서 alert 룰 따로.
#### minio
- `/minio/v2/metrics/cluster` + `/node` + `/bucket`.
- bucket metric은 bucket 수 폭증 시 cardinality 주의.
#### db (PostgreSQL / MySQL)
- postgres_exporter / mysqld_exporter sidecar 또는 별도 Deployment.
- USE method (connection pool saturation, lock wait).
#### ingress-controller
- RED + upstream response time.
- path label은 반드시 template 화.
## 프로젝트 기준 요약
- Four Golden Signals를 dashboard 골격으로, RED/USE를 세부 방법론으로.
- ServiceMonitor / PodMonitor 를 primary scrape, annotation은 fallback.
- metrics port는 NetworkPolicy로 monitoring namespace만 허용.
- Cardinality는 label 설계에서 가드 (user_id / raw path / timestamp 금지).
- 로그는 JSON structured stdout, trace_id/span_id 포함.
- log shipping stack은 플랫폼당 하나 (Loki+Alloy 또는 Fluent Bit→OpenSearch).
- 로그에 민감정보 금지, 중앙 파이프라인 redaction.
- OpenTelemetry DaemonSet agent + Deployment gateway.
- health / metrics / admin endpoint 외부 비공개.
- `kubectl events` 를 기본 event 조회 수단으로 (1.27+).
- alert는 SLO burn rate 기반, CPU% 같은 단순 threshold 금지.
@@ -0,0 +1,301 @@
# operations / runbook / upgrade / rollback 기준
## 목적
이 문서는 1000+ 서비스를 운영하는 플랫폼에서 모든 변경이 거쳐야 하는 **runbook 규칙**을 고정한다. GitOps 원본, rolling update 파라미터 튜닝, 진보된 배포 전략 (Argo Rollouts, canary, blue/green), K3s 자동 업그레이드, node 작업(drain/cordon), rollback 의미와 경계가 대상이다.
## 공식 / 업계 근거
- Kubernetes `Deployment.spec.strategy`: `RollingUpdate` (default, maxSurge/maxUnavailable 25%/25%) 또는 `Recreate` (singleton).
- `kubectl rollout`: `status --timeout`, `history`, `undo --to-revision`, `pause`, `resume`, `restart`.
- **Argo Rollouts** (https://argoproj.github.io/argo-rollouts/): `Rollout` CRD가 Deployment의 대체제로 canary / blueGreen 지원. `AnalysisTemplate` + Prometheus metric으로 자동 승격/롤백.
- **Flagger**: Argo Rollouts의 대안, service-mesh 친화적 (Istio/Linkerd/App Mesh).
- **ArgoCD**: sync wave (`argocd.argoproj.io/sync-wave: "<int>"`), sync phase hook (`PreSync`, `Sync`, `PostSync`, `SyncFail`, `PostDelete`).
- **Flux**: `Kustomization.spec.dependsOn` 으로 순서 명시.
- `kubectl drain --ignore-daemonsets --delete-emptydir-data --grace-period=30` 가 node maintenance 표준. PDB를 존중하므로 PDB 설계가 전제.
- **K3s System Upgrade Controller** (https://docs.k3s.io/upgrades/automated): `Plan` CRD로 server-plan / agent-plan 분리, concurrency 제어, nodeSelector로 대상 제한.
- Flyway `validate`, `info`, `migrate` — application rollout 과 분리.
## 기본 규칙
### 1. Source of truth = Git 의 Kustomize / Helm overlay
운영 변경은 Git에 있는 선언형 원본에서만 시작한다.
기본 금지:
- 운영 노드에서 manifest 파일 직접 편집.
- `kubectl edit` 로 live object 수정 후 문서 없음.
- `/var/lib/rancher/k3s/server/manifests` 를 1차 원본처럼 사용.
### 2. 변경 절차는 render → diff → apply → status → post-check 로 고정
```
1. kubectl kustomize <overlay> # render
2. kubectl diff -k <overlay> # preview
3. kubectl apply -k <overlay> # apply
4. kubectl rollout status ... --timeout=10m
5. post-check (smoke test, SLO check)
```
`diff` 없는 `apply` 는 프로덕션 금지.
### 3. `rollingUpdate.maxSurge` / `maxUnavailable` 는 워크로드별 튜닝
기본값 `25% / 25%`**replica 수에 따라 틀릴 수 있다**.
- **replica 2**: default는 maxUnavailable 0, maxSurge 1 추천 → 항상 최소 2 유지 + 1 추가.
- **replica 3**: `maxSurge: 1, maxUnavailable: 0` → 가용성 우선.
- **replica 10+**: `maxSurge: 25%, maxUnavailable: 10%` → 속도와 가용성 균형.
- **latency-sensitive**: `maxUnavailable: 0` 고정.
- **cost-sensitive large fleet**: `maxSurge: 10%, maxUnavailable: 10%`.
### 4. `Recreate` 전략은 singleton / 동시성 금지 워크로드에만
- PVC ReadWriteOnce + 단일 pod 가 전제인 app (legacy MySQL single instance 등).
- Old/New 동시 실행 시 데이터 부정합이 나는 앱.
- 짧은 downtime이 허용되는 경우.
일반 stateless app은 절대 Recreate 쓰지 않는다.
### 5. `kubectl rollout` 명령 계열
- `kubectl rollout status deployment/<name> --timeout=10m`: 타임아웃 필수.
- `kubectl rollout history deployment/<name>`: revision 확인.
- `kubectl rollout undo deployment/<name> --to-revision=<N>`: 이전 revision으로 되돌림.
- `kubectl rollout pause deployment/<name>`: 롤아웃 중단 (부분 적용 뒤 관찰용).
- `kubectl rollout resume deployment/<name>`: 재개.
- `kubectl rollout restart deployment/<name>`: 이미지 변경 없이 Pod 재생성 (secret 갱신 후 등).
### 6. 진보된 배포 전략: Argo Rollouts (canary / blueGreen)
표준 `Deployment` 로는 부족한 경우 (자동화된 canary, metric-based 승격) 에는 Argo Rollouts 의 `Rollout` CRD 를 쓴다.
- **canary**: `steps:` 로 traffic %, pause, analysis 순서 기술.
- **blueGreen**: `activeService` / `previewService` 로 서비스 두 개 전환.
- **AnalysisTemplate**: Prometheus query로 success rate / p99 latency 측정 → 자동 promote or abort.
- **대안 Flagger**: Istio / Linkerd / App Mesh + Flagger `Canary` CRD. service mesh 있는 플랫폼에서 선택.
### 7. Argo Rollouts 기본 canary 스텝
```
steps:
- setWeight: 10
- pause: { duration: 2m }
- analysis: { templates: [{ templateName: success-rate }] }
- setWeight: 25
- pause: { duration: 5m }
- analysis: { templates: [...] }
- setWeight: 50
- pause: { duration: 10m }
- setWeight: 100
```
각 setWeight 사이에 pause + analysis 로 자동 abort gate.
### 8. blueGreen 은 traffic cutover 가 필요한 경우만
blueGreen은:
- schema 변경이 양립 불가해서 instant cutover가 필요.
- 외부 system 과 coordination 필요 (rollback도 instant).
일반 변경은 canary 가 우선. blueGreen 은 trade-off (리소스 2배, warm-up 부담) 때문에 default 가 아니다.
### 9. ArgoCD sync wave / hook
배포 순서는 sync wave annotation 으로 명시한다.
- `argocd.argoproj.io/sync-wave: "-2"` → CRD.
- `argocd.argoproj.io/sync-wave: "-1"` → namespace, secret store, operator.
- `argocd.argoproj.io/sync-wave: "0"` → 본 리소스 (기본).
- `argocd.argoproj.io/sync-wave: "1"` → Ingress, post-deploy job.
hook:
- `PreSync`: schema migration job.
- `Sync`: 본 리소스 (default).
- `PostSync`: smoke test Job, cache warm.
- `SyncFail`: 실패 시 알림 Job.
- `PostDelete`: 삭제 후 cleanup.
### 10. Flux Kustomization dependsOn
Flux 플랫폼에서는 `Kustomization.spec.dependsOn` 으로 순서를 명시한다.
```yaml
apiVersion: kustomize.toolkit.fluxcd.io/v1
kind: Kustomization
metadata:
name: auth-server
namespace: flux-system
spec:
interval: 5m
path: ./k8s/overlays/prod
prune: true
sourceRef:
kind: GitRepository
name: platform
dependsOn:
- name: cert-manager
- name: postgres-operator
```
### 11. node 작업 (drain / cordon) 은 PDB 존중 흐름
```
1. kubectl cordon <node>
2. kubectl drain <node> \
--ignore-daemonsets \
--delete-emptydir-data \
--grace-period=30 \
--timeout=10m
3. 작업 수행
4. kubectl uncordon <node>
```
옵션 의미:
- `--ignore-daemonsets`: DaemonSet pod는 evict 대상이 아님.
- `--delete-emptydir-data`: ephemeral 데이터 수용.
- `--grace-period=30`: preStop + terminationGracePeriod 존중.
- `--timeout=10m`: PDB 로 인한 무한 대기 차단.
**PDB 없는 critical workload** 는 drain 실패 또는 downtime 유발. PDB 설계가 선결 조건.
### 12. K3s System Upgrade Controller
K3s 자동 업그레이드는 `system-upgrade-controller``Plan` CRD 를 쓴다.
구성:
- **server-plan**: control-plane 먼저 업그레이드. `concurrency: 1`, nodeSelector: `node-role.kubernetes.io/control-plane=true`.
- **agent-plan**: agent 노드. `concurrency: 1~N` (small cluster는 1), server-plan 완료 후.
- `cordon: true`, `drain.force: true, deleteEmptydirData: true, ignoreDaemonsets: true` 표준.
- `version:` 또는 `channel:` 로 target K3s version.
- `upgrade.image: rancher/k3s-upgrade` + 버전 tag.
### 13. blue/green via two Services (수동 패턴)
Argo Rollouts 없이 간단 blue/green 이 필요하면:
- Deployment A (blue), Deployment B (green) 각각.
- Service selector 의 `version` label 만 전환 (blue → green).
- rollback = selector 를 다시 blue 로.
- canary 는 이 방식으로 구현하지 않는다 (Argo Rollouts 사용).
### 14. Rollback 은 DB rollback 이 아니다
**가장 자주 오해되는 규칙**. 반드시 내재화한다.
- `kubectl rollout undo` 는 Deployment workload 만 되돌린다.
- **DB schema 변경 / migration 은 되돌아가지 않는다**.
- rollback 설계는 **schema-forward-compatible** 로 한다:
- Expand (schema 추가 → 이전 코드도 호환) → Migrate (데이터 이전) → Contract (이전 코드용 schema 제거). Expand/Contract를 별도 릴리스로 분리.
- 긴급 상황에서도 rollout undo 로 DB 를 되돌릴 수 없다. DB 는 별도 restore 절차 (PITR, snapshot).
### 15. Flyway validate → migrate 를 application rollout 과 분리
```
1. flyway validate # checksum / 순서 확인
2. flyway info # 대기 migration 확인
3. flyway migrate # 실제 적용
4. kubectl apply -k ... # app rollout (별도 단계)
5. kubectl rollout status # 앱 기동 확인
```
application startup 안에 migration 을 숨기지 않는다 (rollout 실패와 migration 실패 섞임).
### 16. restore 와 rollout 구분
**rollout** (workload 변경 되돌리기):
- `kubectl rollout undo` 또는 이전 Git revision apply.
- Deployment / StatefulSet / DaemonSet 대상.
**restore** (상태 복구):
- K3s control plane → etcd snapshot restore.
- PostgreSQL → PITR / base backup + WAL.
- Vault → raft snapshot restore.
- MinIO → replication resync 또는 DR site cutover.
서로 다른 runbook 이다. "rollback" 이라는 한 단어로 뭉치지 않는다.
### 17. 긴급 변경도 runbook 을 벗어나지 않음
장애 대응 hot-fix 라도:
- 어떤 overlay 를 바꿨는지 commit / PR.
- 어떤 명령을 실행했는지 기록 (shell history / runbook log).
- 사후 Git 반영 (live-cluster drift 제거).
- 임시 조치의 만료 / 정리 시점 기록.
### 18. destructive 작업은 명시 승인 + 증거 보존
요구 작업:
- namespace 삭제.
- PVC 삭제.
- StatefulSet 삭제 + PVC 정리.
- K3s snapshot restore.
- Vault raft snapshot restore.
- DB restore overwrite.
- MinIO bucket purge / replication cutover.
규칙:
- 2-person approval.
- 작업 전 full snapshot 확보.
- dry-run / diff 선행.
- post-mortem 작성.
## 권장 절차 템플릿
### 일반 app 변경
1. PR 생성 + review
2. `kubectl kustomize <overlay>` → 렌더 검증
3. `kubectl diff -k <overlay>` → 변경 확인
4. `kubectl apply -k <overlay>`
5. `kubectl rollout status deployment/<name> --timeout=10m`
6. smoke test + SLO dashboard 확인
7. 결과 PR comment
### DB migration 포함 변경
1. migration SQL review
2. `flyway validate``flyway info``flyway migrate`
3. app overlay apply
4. `kubectl rollout status`
5. post-check
6. 실패 시 DB runbook 과 app rollback runbook 분리 적용
### K3s control plane upgrade
1. 해당 버전 release notes / caveat 확인
2. etcd snapshot 확보
3. `Plan` CRD apply (server-plan)
4. control-plane 업그레이드 완료 확인
5. `Plan` CRD apply (agent-plan)
6. agent 업그레이드 완료 확인
7. packaged component 영향 확인
8. 실패 시 etcd restore runbook
### node maintenance
1. `kubectl cordon <node>`
2. `kubectl drain <node> --ignore-daemonsets --delete-emptydir-data --grace-period=30 --timeout=10m`
3. 작업 수행
4. `kubectl uncordon <node>`
5. `kubectl get pods -o wide` 로 재배치 확인
## 프로젝트 기준 요약
- source of truth = Git Kustomize/Helm overlay, live-cluster 수정 금지.
- render → diff → apply → rollout status → post-check 순서 고정.
- rollingUpdate 파라미터는 워크로드별 튜닝, default 25%/25% 맹신 금지.
- Argo Rollouts 로 canary + AnalysisTemplate 자동 gate, Flagger 는 mesh 환경 대안.
- ArgoCD sync wave / hook, Flux dependsOn 으로 순서 명시.
- node 작업은 PDB 존중 drain 흐름, PDB 설계가 선결.
- K3s 업그레이드는 System Upgrade Controller `Plan` CRD (server → agent).
- blue/green 은 cutover 필요 시, canary 가 default.
- **rollback 은 DB rollback 이 아니다** — schema-forward-compatible 로 설계.
- Flyway validate/migrate 는 application rollout 과 분리.
- restore 와 rollout 은 다른 runbook.
- destructive 작업은 2-person approval + snapshot.
@@ -0,0 +1,199 @@
# resources / probes / availability 기준
## 목적
이 문서는 1000+ 서비스를 운영하는 Kubernetes 플랫폼에서 AI coding agent가 생성하는 모든 워크로드 매니페스트의 ground truth다. 모든 rule은 Google SRE / Netflix / Shopify의 실제 프로덕션 합의를 기반으로 한다.
정하는 것:
- resource requests/limits를 어떤 값으로, 어떤 QoS class로 줄지
- probe (startup / readiness / liveness) 세 축을 어떻게 분리할지
- 가용성(PDB / topologySpread / HPA)을 어떤 조합으로 구성할지
- K3s 환경에서 metrics-server 전제를 어떻게 다룰지
## 공식 / 업계 근거
- Kubernetes QoS class는 resources 값에 의해 자동 결정된다 (`Guaranteed`, `Burstable`, `BestEffort`).
- CPU는 compressible resource로 limit 초과 시 throttle된다. memory는 incompressible로 OOM kill된다.
- Tim Hockin (Google, Kubernetes co-founder) 및 다수 SRE 컨퍼런스 토크: **CPU limit는 CFS throttling을 quota 미만에서도 유발하므로 대부분의 프로덕션 워크로드에서 제거한다**. CPU request만 설정하여 노드 capacity를 공정 공유한다.
- memory limit는 OOM kill의 유일한 제어 수단이므로 반드시 설정한다.
- `topologySpreadConstraints`는 1.19+ stable. zone과 host 두 축으로 skew를 제한하는 것이 표준이다.
- `podAntiAffinity`는 legacy 대안, 현대 가이드는 topologySpreadConstraints 우선.
- HPA v2 (`autoscaling/v2`) 는 `behavior` block으로 scale up/down stabilizationWindow와 policy를 분리 제어한다.
- PodDisruptionBudget은 `maxUnavailable` 또는 `minAvailable`. 대규모 fleet에서는 `maxUnavailable` 권장 (replica scale 변화 추종).
- startup probe는 성공 전까지 liveness/readiness를 차단한다. slow boot 서비스에 필수.
- Kubernetes 1.29+ native sidecar: init container에 `restartPolicy: Always` 명시.
## 기본 규칙
### 1. QoS class는 의도적으로 선택한다
QoS class는 `resources` 값의 결과물이 아니라 **선택**이다.
- **Guaranteed**: 모든 컨테이너의 request == limit. 가장 높은 eviction 우선순위 보호.
- 적용: latency-sensitive JVM (Keycloak, auth-server critical tier), stateful 단일 인스턴스 (vault active), 단일 ReplicaSet critical path.
- **Burstable**: request < limit 또는 일부만 설정. 탄력적 CPU burst 허용.
- 적용: stateless HTTP API, worker, generic service — 기본값.
- **BestEffort**: request/limit 모두 없음. 가장 먼저 evict됨.
- 적용: 일시적 debugging pod, 무영향 experiment. 프로덕션 금지.
### 2. CPU limit anti-pattern — 기본은 CPU request only
Google SRE 및 Tim Hockin의 공식 stance는 "대부분의 워크로드에서 CPU limit를 설정하지 말 것"이다. Linux CFS의 quota 회계가 sub-period burst에서도 throttle을 유발하기 때문이다.
기본:
- **CPU**: request만 설정, limit 생략.
- **Memory**: limit 반드시 설정.
- Guaranteed를 원하면: `limits.memory == requests.memory`.
- Burstable 기본값: `limits.memory = 1.1 ~ 1.5 × requests.memory`.
예외 (CPU limit를 설정해야 하는 경우):
- multi-tenant 노드에서 noisy neighbor가 측정 가능한 손해를 유발.
- batch/cron Job에서 예산 통제가 필요.
- billing-backed 측정으로 인한 compliance 요구.
### 3. requests 값은 측정 기반으로 잡는다
- p95 cpu usage × 1.2 가 request 시작점.
- p99 memory (steady state) × 1.3 이 memory request 시작점.
- 최초 배포는 **overprovision** 으로 시작 → 1~2주 관측 후 right-sizing.
- VPA recommendation을 참고하되 자동 적용은 하지 않는다 (review 필요).
### 4. `limit`만 있고 `request`가 없는 구성 금지
Kubernetes는 request 미설정 시 limit를 request로 복사한다. 이는 암묵적 Guaranteed QoS로 귀결되며 의도와 다를 수 있다. 반드시 둘 다 명시한다.
### 5. Probe는 세 축으로 분리한다
- **startup probe**: "부팅이 끝났는가". 성공 전까지 readiness/liveness는 실행되지 않는다.
- 필수: Keycloak, Vault, JVM warm-up이 긴 서비스.
- 타이밍 규칙: `failureThreshold × periodSeconds ≥ 최악의 cold start (p99)`. 예: Keycloak `periodSeconds: 10, failureThreshold: 30` = 300s.
- **readiness probe**: "지금 트래픽을 받아도 되는가". 실패 시 Service endpoint에서 제외.
- 모든 traffic-facing 서비스 필수.
- 외부 의존성 전체 가용성을 묶지 않는다 (동시 탈락 방지).
- **liveness probe**: "재시작이 치료인가" (deadlock only).
- Default = 설정하지 않거나 readiness와 다른 가벼운 self-check.
- **잘못 설정하면 cascading restart 유발**. Kubernetes 공식 문서 명시.
### 6. readiness는 shallow, liveness는 더 shallow
readiness는 "app loop이 요청을 처리 가능한가"까지만 검사한다. DB connection pool 초기화처럼 intra-pod 조건은 OK. 외부 DB `SELECT 1` 전체 가용성 체크는 금지.
liveness는 process deadlock 감지 전용. HTTP endpoint면 `/livez` 같은 매우 가벼운 200 응답.
### 7. topologySpreadConstraints를 기본 가용성 primitive로
production multi-zone cluster에서는 **zone + host 두 축** 모두 제약한다.
```yaml
topologySpreadConstraints:
- maxSkew: 1
topologyKey: topology.kubernetes.io/zone
whenUnsatisfiable: DoNotSchedule
labelSelector:
matchLabels:
app.kubernetes.io/name: auth-server
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth-server
```
- zone `DoNotSchedule`: 프로덕션에서 zone 장애 격리에 필수.
- host `ScheduleAnyway`: 노드 부족 시 배포 불가 방지.
### 8. podAntiAffinity는 legacy로 본다
topologySpreadConstraints가 등장한 뒤 podAntiAffinity는 대부분의 use case에서 대체되었다. 신규 매니페스트는 topologySpreadConstraints를 우선 적용한다.
예외: 단순 "한 노드에 두 개 이상 금지" 규칙만 필요하고 spread 회계가 불필요한 경우.
### 9. HPA는 autoscaling/v2, behavior block 필수
`autoscaling/v1`은 더 이상 사용하지 않는다. `autoscaling/v2`를 기본으로 한다.
- `metrics:` 유형: `Resource` (cpu/memory), `Pods`, `Object`, `External`, `ContainerResource`.
- `behavior.scaleUp.stabilizationWindowSeconds: 0` (트래픽 급증에 빠르게 반응).
- `behavior.scaleDown.stabilizationWindowSeconds: 300` (flapping 방지).
- `policies` 조합: `type: Percent` (현 replica의 X%)와 `type: Pods` (절대 수) 동시 지정, `selectPolicy: Max` 또는 `Min`.
### 10. HPA 전제 조건
- resource requests가 먼저 잡혀 있어야 한다 (utilization target이 request 기준).
- startup probe가 안정화되어 있어야 한다 (scale-up 중 flapping 방지).
- 해당 워크로드가 **horizontal scale로 효과가 있는** 성격이어야 한다. stateful / DB / quorum 기반은 HPA 비대상.
- K3s metrics-server가 packaged로 배포되어 있음을 전제로 하되, availability를 runbook에서 점검한다.
### 11. PDB는 fleet 규모에 맞춰 `maxUnavailable` 우선
- replica ≥ 3: `maxUnavailable: 1` 또는 `maxUnavailable: 25%`.
- replica 대규모 (10+): `maxUnavailable: 10%` 권장 (유연성).
- replica 2: `maxUnavailable: 1`.
- replica 1: PDB 금지 (node drain을 막는다).
- quorum 기반 (etcd, vault raft, DB cluster): `minAvailable` 로 quorum 수 명시.
### 12. PDB zero disruption 금지
`maxUnavailable: 0` 또는 `minAvailable: 100%` 는 node drain / maintenance를 완전 차단한다. Kubernetes 업그레이드 자체가 불가능해진다. 명시적 예외 승인 없이 사용 금지.
### 13. init container 와 sidecar 순서 (1.29+)
- **init container**: main 전에 실행, 완료 후 종료. schema migration, secret preparation 용.
- **native sidecar (1.29+)**: init container에 `restartPolicy: Always` 명시. main과 병렬 실행, main 종료 후 종료.
- 사용: log forwarder, metrics exporter, service mesh proxy.
- `initContainers` 배열 순서가 실행 순서다.
### 14. 워크로드별 기본 권장안
#### auth-server (stateless Spring Boot)
- QoS: **Burstable**.
- CPU: request only (`500m`). Memory: request `1Gi`, limit `1.5Gi`.
- Probes: startup `/actuator/health/started` (60s), readiness `/actuator/health/readiness`, liveness `/actuator/health/liveness`.
- HPA: CPU 70%, min 3, max 20, scale-down 300s.
- PDB: `maxUnavailable: 1`.
- topologySpread: zone `DoNotSchedule`, host `ScheduleAnyway`.
#### keycloak (JVM, slow boot, latency-sensitive)
- QoS: **Guaranteed** (request == limit, memory 2Gi 고정).
- CPU: request `1`, limit `1` (Guaranteed 요구).
- Probes: startup 5분 budget (`periodSeconds: 10, failureThreshold: 30`), readiness `/health/ready` on 9000, liveness `/health/live` on 9000.
- HPA: 보통 **비대상**. 고정 replica (3)로 시작, 측정 후 검토.
- PDB: `maxUnavailable: 1`.
#### vault (raft quorum)
- QoS: **Guaranteed**.
- Probes: readiness/liveness는 raft sealed/active 상태 구분.
- HPA: 비대상.
- PDB: `minAvailable: 2` (3-node raft 기준 quorum 보존).
#### minio (erasure coded storage)
- QoS: **Guaranteed**.
- PDB: `minAvailable: N-1` (erasure set 기준).
- HPA: 비대상.
#### migration-flyway (Job)
- probe 없음 (Job은 probe 무의미).
- requests 명시, limit는 memory만.
- activeDeadlineSeconds 설정.
- HPA/PDB 비대상.
#### ingress-controller
- QoS: **Burstable** 또는 Guaranteed (tier에 따라).
- HPA 후보 (traffic 기반).
- PDB: `maxUnavailable: 1`.
## 프로젝트 기준 요약
- QoS는 의도적으로 선택. Guaranteed는 latency-sensitive JVM, Burstable은 stateless 기본.
- CPU limit 기본 제거 (throttling 회피). Memory limit 필수.
- requests/limits 함께 명시. limit만 단독 금지.
- probe 세 축 분리. startup 타이밍은 worst-case cold start 기준.
- topologySpreadConstraints zone + host 두 축으로 기본 구성.
- HPA v2 + behavior block. resource requests / startup 안정화 후 적용.
- PDB는 `maxUnavailable` 우선, replica 전략과 함께 결정.
- 1.29+ native sidecar는 init container `restartPolicy: Always`.
- K3s metrics-server는 HPA 전제로만 신뢰, full metrics는 별도 stack.
+293
View File
@@ -0,0 +1,293 @@
# infra scripts 기준
## 목적
이 문서는 1000+ 서비스 플랫폼에서 인프라 스크립트가 지켜야 할 품질 기준선이다. 스크립트는 선언형 원본 (Kustomize / Helm / ArgoCD / Flux) 을 **대체하지 않는다**. 렌더 / diff / 적용 / 백업 / 복구 / 부트스트랩을 **orchestration** 하는 얇은 레이어로 제한한다.
## 공식 / 업계 근거
- **Google Shell Style Guide**: `#!/usr/bin/env bash`, `set -e`, `main "$@"`, function-first, `local`.
- **Unofficial Bash Strict Mode (Aaron Maxwell)**: `set -euo pipefail` + `IFS=$'\n\t'` 가 사실상 표준.
- **ShellCheck** (https://www.shellcheck.net/): 정적 분석. CI에서 mandatory.
- **shfmt** (mvdan/sh): 자동 포맷터. line-length / indent 규격 강제.
- **GitOps 원칙** (Weaveworks 정의): 선언형 원본 + auto-reconcile. 스크립트는 원본을 소유하지 않는다.
## 기본 규칙
### 1. 모든 스크립트 맨 위에 strict mode
```bash
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
```
의미:
- `set -e` : 명령 실패 시 즉시 종료.
- `set -u` : unset variable 참조 시 에러.
- `set -o pipefail` : pipeline 중 하나라도 실패하면 전체 실패.
- `IFS=$'\n\t'` : 기본 IFS에서 space 제거 → 파일명 공백 sane split.
예외 금지. CI lint 에서 검사.
### 2. 정리 작업은 `trap` 으로 보장
임시 파일 / 임시 kubeconfig / port-forward / background job 은 반드시 trap EXIT 에서 정리.
```bash
TMPDIR="$(mktemp -d)"
trap 'rm -rf "${TMPDIR}"' EXIT INT TERM
```
- `EXIT`: 정상/비정상 종료 모두 잡음.
- `INT TERM`: signal 기반 종료 시에도 실행.
- trap은 setup 직후 즉시 설치.
### 3. ShellCheck + shfmt 는 CI 에서 필수
- `shellcheck -S style scripts/**/*.sh` → CI fail 시 merge 금지.
- `shfmt -i 2 -bn -ci -d scripts/` → 자동 포맷 검증.
- suppress (`# shellcheck disable=...`) 는 **줄 단위**로만, 이유 주석 필수.
- "경고 너무 많아서 꺼둔다" 금지.
### 4. 표준 `log()` 함수 (ISO 8601 timestamp + level, stderr)
```bash
log() {
local level="$1"; shift
local ts
ts="$(date -u +'%Y-%m-%dT%H:%M:%SZ')"
printf '%s [%s] %s\n' "${ts}" "${level}" "$*" >&2
}
info() { log INFO "$@"; }
warn() { log WARN "$@"; }
error() { log ERROR "$@"; }
fatal() { log FATAL "$@"; exit 1; }
```
- stdout 은 머신 판독용 결과 전용.
- stderr 로 로그 → pipeline 안전.
- UTC ISO 8601 로 tz 모호성 제거.
### 5. 엔트리포인트 `main "$@"` 패턴
```bash
usage() {
cat <<'EOF' >&2
Usage: render-diff-apply.sh [--overlay PATH] [--context NAME] [--yes]
--overlay PATH path to kustomize overlay (required)
--context NAME kube context (required)
--yes skip confirmation for apply
EOF
}
main() {
# 인자 파싱
# 환경 검증
# 함수 호출
:
}
main "$@"
```
- 엔트리포인트 스크립트는 **얇게**. 비즈니스 로직은 `lib/` 또는 `tasks/`.
- `usage()` 함수 필수.
### 6. 변수는 `local`, command substitution 은 분리
```bash
bad_pattern() {
local ctx="$(kubectl config current-context)" # local 이 exit status 가려버림
}
good_pattern() {
local ctx
ctx="$(kubectl config current-context)" # 분리 → $? 보존
}
```
ShellCheck SC2155 가 이것을 잡음.
### 7. Idempotent 를 기본값으로
- `create` 보다 `apply` / `ensure` 성격.
- `kubectl apply -k` 는 idempotent.
- `mkdir -p`, `kubectl create namespace X --dry-run=client -o yaml | kubectl apply -f -` 패턴.
- destroy 성격은 반드시 opt-in.
### 8. `kubectl diff``kubectl apply` 필수 흐름
프로덕션 적용 스크립트 기본 흐름:
```
1. kubectl kustomize <overlay> > render.yaml # render
2. kubeconform / kubectl apply --dry-run=server # validate
3. kubectl diff -k <overlay> # preview
4. confirm gate (CONFIRM=yes 또는 --yes)
5. kubectl apply -k <overlay> # apply
6. kubectl rollout status ... --timeout=10m # watch
```
### 9. `--dry-run=server` 를 validation 기본값으로
client-side dry run 은 CRD schema / admission webhook 을 평가하지 않는다. **server-side dry run** 을 쓴다:
```bash
kubectl apply -k "${OVERLAY}" --dry-run=server
```
### 10. destructive 작업은 `--yes` 또는 `CONFIRM=yes` gate
delete / prune / restore overwrite 류는 명시적 opt-in 없이 실행 금지.
```bash
if [[ "${CONFIRM:-no}" != "yes" ]]; then
fatal "destructive operation requires CONFIRM=yes"
fi
```
또는:
```bash
if [[ "${YES:-0}" -ne 1 ]]; then
warn "re-run with --yes to confirm"
exit 2
fi
```
### 11. 환경을 암묵적으로 추론하지 않는다
- 대상 overlay / namespace / context 는 **명시적 인자**로.
- `kubectl config current-context` 에 몰래 의존 금지.
- 필요한 env var 는 시작 시 `[[ -z "${FOO:-}" ]] && fatal "FOO required"` 로 검증.
### 12. JSON 파싱은 `jq` / `kubectl -o jsonpath`, 절대 regex 로 하지 않는다
```bash
# BAD
kubectl get pod foo -o yaml | grep "image:" | awk '{print $2}'
# GOOD
kubectl get pod foo -o jsonpath='{.spec.containers[0].image}'
# GOOD
kubectl get pod foo -o json | jq -r '.spec.containers[0].image'
```
kubectl/kubernetes 출력에 regex 쓰면 field 순서 / 라벨 / 버전 변화에 깨진다.
### 13. 비밀값은 로그 / stdout / 파일에 남기지 않는다
- env var / secret value 를 `set -x` 아래에서 직접 사용 금지.
- debug 모드에서는 masking:
```bash
mask_secrets() {
sed -E \
-e 's/(password=)[^ ]+/\1***/g' \
-e 's/(token=)[^ ]+/\1***/g' \
-e 's/(Authorization: Bearer )[A-Za-z0-9._-]+/\1***/g'
}
some_command --debug | mask_secrets
```
- secret 을 참조해야 하면 `--from-file` 이나 stdin pipe 로 주입, argv 금지.
### 14. 스크립트는 선언형 원본을 소유하지 않는다
**금지**:
- 대규모 heredoc YAML 생성기 (스크립트 내부에 매니페스트 숨김).
- 환경별 로직이 if/else 로만 존재.
- 스크립트만 실행해야 실제 상태를 알 수 있는 구조.
**허용**:
- `kubectl apply -k overlays/<env>` wrapping.
- Helm chart render + apply orchestration.
- backup / restore (stateful data 만 대상).
- bootstrap (namespace, secret store 설치 같은 일회성).
- smoke test.
### 15. 폴더 구조
```text
scripts/
bin/ # 엔트리포인트 (얇게)
render
diff
apply
backup-k3s
restore-k3s
lib/ # 공통 함수
common.sh # log, fatal, require_cmd, confirm
kubectl.sh # kubectl wrappers
kustomize.sh # kustomize render helpers
tasks/ # 도메인 작업
keycloak.sh
vault.sh
flyway.sh
ci/ # CI 검증 전용
lint.sh
validate.sh
```
- `bin/` 파일 이름은 동사.
- `lib/` 는 20개 내외, 잡동사니 함수 금지.
- 하나의 거대 `deploy.sh` 금지.
### 16. retry 는 함수화, 무한 루프 금지
```bash
retry() {
local max="$1"; shift
local delay="$1"; shift
local n=0
until "$@"; do
n=$((n + 1))
if (( n >= max )); then
return 1
fi
sleep "${delay}"
done
}
retry 5 3 kubectl rollout status deployment/foo --timeout=30s
```
backoff 는 선형/지수 명시, 무한 retry 금지.
### 17. quoting / array 기본값
- 모든 변수 전개는 `"${VAR}"`.
- 인자 list 는 array: `args=(--namespace foo --context bar)`.
- `"$@"` 유지.
- unquoted glob / word splitting 금지.
### 18. 출력 채널 규칙
- stdout → 머신 판독 결과 (jsonpath 결과, 렌더된 YAML 등).
- stderr → 로그, 경고, 에러, 진행 표시.
- exit code → 0 success, 1 error, 2 usage error.
pipeline 하류 도구가 stdout 을 parse 한다는 전제로 작성.
## 프로젝트 기준 요약
- strict mode `set -euo pipefail` + `IFS=$'\n\t'` 필수.
- trap EXIT INT TERM 으로 정리 보장.
- ShellCheck + shfmt CI 필수.
- ISO 8601 UTC + LEVEL 로그 함수 (stderr).
- `main "$@"` 패턴 + usage() 함수.
- local 선언과 command substitution 분리.
- idempotent 기본, destructive 는 `--yes` / `CONFIRM=yes` gate.
- `kubectl diff``apply`, `--dry-run=server` validation.
- 환경 추론 금지, overlay/namespace/context 명시.
- JSON 은 jq / jsonpath, 절대 regex 금지.
- secret 은 log / argv 에 남기지 않고 masking.
- 스크립트는 선언형 원본을 소유하지 않는 orchestration 레이어.
- `bin/ lib/ tasks/ ci/` 폴더 분리, giant deploy.sh 금지.
+183
View File
@@ -0,0 +1,183 @@
# security hardening 기준
## 목적
이 문서는 K3s/Kubernetes 기반 인프라(1000+ 서비스 규모)에서
- 어떤 Pod Security Standard(PSS) 수준을 강제할지
- Pod/ServiceAccount/RBAC/NetworkPolicy/Secret/Image supply chain을 어디까지 하드닝할지
- Platform 예외를 어떻게 선언할지
를 단일 ground truth로 고정한다.
이 문서의 목표는 다음과 같다.
- root/privileged/host namespace 사용을 기본 금지하고 예외는 manifest로 증명한다
- allow-all network/RBAC을 운영 기본값으로 두지 않는다
- Secret의 저장·접근·전송 모든 단계에서 신뢰 경계를 명시한다
- K3s production hardening(PSS, NetworkPolicy, audit, at-rest encryption)을 묶음으로 본다
## 공식 의미 (근거)
- Kubernetes 1.25부터 `PodSecurityPolicy`(PSP)는 제거되었다. 대체는 **Pod Security Admission (PSA)** + `pod-security.kubernetes.io/*` namespace label이다.
- Pod Security Standards는 `privileged`, `baseline`, `restricted` 세 프로파일이다. `restricted`는 업계 최신 hardening best practice를 반영한다.
- PSA는 `enforce`, `audit`, `warn` 세 모드를 지원하고, 각 모드마다 버전을 `latest`/`vX.Y`로 고정할 수 있다.
- `restricted` 프로파일이 강제하는 주요 필드: `runAsNonRoot=true`, `allowPrivilegeEscalation=false`, `capabilities.drop=["ALL"]`(네트워크 capability는 `NET_BIND_SERVICE`만 추가 허용), `seccompProfile.type in {RuntimeDefault, Localhost}`, host namespace/Port/Path 금지, `privileged=false`, `procMount=Default`, ephemeral volume 화이트리스트.
- NetworkPolicy는 namespace 내 매칭되는 Pod가 하나라도 있으면 그 Pod의 해당 방향 트래픽은 **정책 합집합**만 허용된다(그 외 default deny). 매칭되는 Pod가 없으면 기본은 allow-all이다.
- NetworkPolicy `from`/`to` 원소 내에서 `namespaceSelector``podSelector`**동일 엔트리** 안에 두면 AND(교집합), **별도 엔트리**로 두면 OR(합집합)로 계산된다. 이 차이가 cross-namespace 정책 버그의 1순위 원인이다.
- Secret은 기본적으로 etcd에 base64로만 저장되므로 운영 클러스터는 `EncryptionConfiguration`(aescbc/aesgcm/KMS)을 필수로 구성한다. K3s는 `--secrets-encryption` 플래그로 aescbc provider를 활성화한다.
- ServiceAccount token은 Pod에 기본 자동 마운트된다. 1.24부터는 time-bound projected token이 기본이다.
- RBAC는 additive-only이며 `Role`/`RoleBinding`(namespace) 우선, `ClusterRole`/`ClusterRoleBinding`은 예외적이다.
## 기본 규칙
### 1. 모든 application namespace는 PSA `restricted` enforce 라벨이 기본
namespace 생성 시 다음 라벨을 **enforce** 수준으로 붙인다(예외는 rule 3).
```yaml
metadata:
labels:
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: v1.29
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: v1.29
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: v1.29
```
- `enforce`: deny on violation (hard gate)
- `audit`: audit log 기록
- `warn`: kubectl 사용자에 경고
- version은 `latest` 대신 **명시 버전 pin**을 기본으로 한다. 업그레이드는 ADR로 관리한다.
- PSP는 1.25에서 제거되었으므로 어떤 manifest/차트에도 `policy/v1beta1 PodSecurityPolicy`를 남기지 않는다.
### 2. Restricted 프로파일 전 필드 계약
application Pod/Deployment는 아래 전부를 만족해야 한다. 하나라도 빠지면 PSA가 reject한다.
- `spec.automountServiceAccountToken: false` (API 호출 불필요 시)
- `spec.securityContext.runAsNonRoot: true`
- `spec.securityContext.runAsUser: <non-zero numeric>` (예: `10001`)
- `spec.securityContext.runAsGroup: <non-zero numeric>` (예: `10001`)
- `spec.securityContext.fsGroup: <numeric>` (volume 쓰기 필요 시)
- `spec.securityContext.seccompProfile.type: RuntimeDefault` (Pod 또는 컨테이너 레벨)
- 컨테이너 `securityContext`:
- `allowPrivilegeEscalation: false`
- `privileged: false`
- `readOnlyRootFilesystem: true`
- `runAsNonRoot: true`
- `capabilities.drop: ["ALL"]`
- `capabilities.add`: 비어있거나 `["NET_BIND_SERVICE"]`만 허용
- Pod spec 금지 필드: `hostNetwork`, `hostPID`, `hostIPC`, `hostUsers=false`, `hostPath` volume, `hostPort`, `ephemeralContainers`에 한해 `privileged`
### 3. Platform 예외 namespace는 ADR 문서 + 좁은 enforce
`kube-system`, `ingress-traefik`, `vault`, `cert-manager`, `vault-secrets-operator`, `monitoring` 등은 `baseline` 또는 `privileged` 프로파일이 필요할 수 있다. 예외는 다음을 manifest에 고정한다.
- namespace label은 **필요한 최저 수준**(`baseline` 우선, `privileged`는 CNI/CSI/Node-exporter에 한정)
- 예외 근거 ADR 링크 annotation: `platform.example.com/psa-exception: "ADR-0042"`
- 예외 받는 구체 필드(예: `hostNetwork`, `CAP_NET_ADMIN`)만 열고 나머지는 restricted
### 4. privileged / host namespace 기본 금지
application Pod는 `privileged: true`, `hostNetwork`, `hostPID`, `hostIPC`, `hostPath`, `hostPort`를 쓰지 않는다. 필요성이 있으면 rule 3의 platform 예외로 이관한다.
### 5. runAsNonRoot + numeric UID 강제
image가 `USER` 지시어로 non-numeric user만 지정해도 PSA는 runtime에 UID 확인이 불가능하면 reject할 수 있다. 항상 numeric UID를 명시한다(권장 범위: 1000065535). UID 0은 전 영역 금지.
### 6. seccompProfile은 RuntimeDefault 우선
Pod 레벨 `seccompProfile.type: RuntimeDefault`를 기본으로 두어 모든 컨테이너에 상속. 특정 컨테이너가 custom profile이 필요하면 `Localhost`로 개별 선언하고 profile 파일 경로를 문서화한다. kubelet `--seccomp-default=true`를 클러스터 플래그로 검토한다.
### 7. capabilities drop-first
`drop: ["ALL"]`이 기본이다. 추가 허용 화이트리스트는 `NET_BIND_SERVICE`뿐. `CAP_SYS_ADMIN`, `CAP_NET_ADMIN`, `CAP_SYS_PTRACE`, `CAP_NET_RAW`는 platform 예외에서만 허용한다.
### 8. readOnlyRootFilesystem + writable emptyDir
모든 application container는 `readOnlyRootFilesystem: true`. 쓰기 경로는 `emptyDir`(가능하면 `medium: Memory`, `sizeLimit` 명시)로 분리한다. `/tmp`, `/var/run`, 앱 cache 경로는 별도 volume mount.
### 9. ServiceAccount는 workload 1:1, 토큰 기본 비마운트
- `default` SA 사용 금지. namespace당 Deployment별 전용 SA 생성.
- `automountServiceAccountToken: false`를 Pod spec에 기본 명시.
- Kubernetes API를 호출해야 하는 Pod만 `true` + projected token volume을 명시적으로 선언.
### 10. RBAC는 namespace Role + RoleBinding 우선
- `*` resource/verb 금지.
- `secrets` 리소스는 `get` + `resourceNames` 명시. `list`/`watch`는 controller/operator에만 허용.
- `ClusterRole`/`ClusterRoleBinding`은 CRD controller, metrics scraper, admission webhook 같은 cluster-wide 컴포넌트에 한정하고 subject는 platform SA로 제한.
- `system:masters` group binding 금지.
### 11. NetworkPolicy default-deny + 명시적 allow
운영 namespace는 생성 직후 다음 3종을 배포한다.
1. default-deny-all (ingress + egress)
2. allow-dns-egress (to `kube-system``k8s-app=kube-dns`, UDP/TCP 53)
3. allow-from-ingress-controller (namespaceSelector=`ingress-traefik` + podSelector=`app.kubernetes.io/name=traefik`)
추가 allow는 서비스별 요구(예: DB, Redis, Vault, S3, OIDC endpoint)에 맞춰 **한 엔트리 = AND, 여러 엔트리 = OR** 규칙을 지켜 작성한다.
### 12. NetworkPolicy enforcement 전제 검증
- K3s 기본 CNI(flannel) + kube-router policy controller가 실제로 enforce하는지 배포 후 negative test 필수.
- Calico/Cilium 전환 시 `--disable-network-policy` + `--flannel-backend=none` 조합을 ADR로 관리.
- 운영 정책 변경 후에는 synthetic probe(`netshoot` Pod)로 deny/allow 경로를 모두 검증한다.
### 13. Secret at-rest encryption은 운영 필수
- API Server `--encryption-provider-config` 지정: `aescbc` 또는 KMS provider(권장: AWS KMS/GCP KMS/HashiCorp Vault Transit).
- K3s는 `--secrets-encryption` 플래그 활성화(aescbc). 기존 Secret은 `kubectl get secrets -A -o json | kubectl replace -f -`로 재암호화.
- etcd 백업 자체도 별도 암호화 저장.
### 14. 민감정보 delivery 경로 표준화
1순위: Vault Secrets Operator(VSO)가 Vault → K8s Secret으로 sync → envFrom/volume
2순위: External Secrets Operator(ESO) + AWS/GCP Secret Manager
3순위: CSI Secret Store Driver (volume mount only, K8s Secret 미생성)
4순위: SealedSecrets / SOPS (GitOps + encrypted-at-rest in Git)
모든 경로는 config-and-secrets 문서의 선택 기준 표를 따른다.
### 15. Image supply chain 통제
- `imagePullPolicy: Always`는 mutable tag(`latest`, `main`)에만. 운영은 **digest pin** `image: registry.example.com/auth-server@sha256:<64hex>` 을 기본으로.
- `kubernetes.io/dockerconfigjson` 타입 `imagePullSecret`은 workload SA에 `spec.imagePullSecrets`로 연결.
- Private registry만 허용: `ImagePolicyWebhook` 또는 Kyverno/Gatekeeper로 public Docker Hub deny.
- Image signing(cosign)과 SBOM 요구를 CI에서 강제, cluster-level로는 `ClusterImagePolicy` (sigstore policy-controller) 검토.
### 16. health/metrics/admin endpoint 내부 전용 기본
- `/metrics`는 Service 별도 포트(`name: metrics`) + NetworkPolicy로 `monitoring` namespace Prometheus만 허용.
- `/actuator/*`, `/admin`, `/debug/pprof`는 Ingress 경로에 노출 금지.
- Keycloak `/admin/`, `/metrics`, `/health`는 외부 공개 금지 기본값(network-ingress-tls 문서와 함께 enforce).
### 17. audit + policy together
- API Server `--audit-policy-file`로 최소한 Secret/RBAC/PodSecurity violation을 `RequestResponse` 수준으로 기록.
- PSA `audit` 라벨을 모든 namespace에 붙여 violation을 audit log로 수집.
- Kyverno 또는 Gatekeeper로 PSA 밖의 policy(resource limits, image registry, required labels)를 보완.
### 18. Pod spec 기타 하드닝 기본
- `resources.limits.cpu`, `resources.limits.memory` 필수. memory limit 없는 Pod는 OOM-Kill 전파 위험.
- `terminationGracePeriodSeconds` 명시(기본 30은 서비스별로 재조정).
- `readinessProbe` + `livenessProbe` 분리. `startupProbe`는 JVM/느린 기동 앱에 필수.
- `topologySpreadConstraints` 또는 `podAntiAffinity`로 node 단일 장애 블라스트 반경 축소.
### 19. 현재 스택 기본 권장안
#### auth-server / test-server / keycloak / migration-flyway
- namespace PSA: `restricted` enforce pinned to `v1.29`
- SA: 서비스별 1:1, `automountServiceAccountToken: false`
- Secret: VSO로 Vault → K8s Secret sync
- NetworkPolicy: default-deny + dns + ingress + db + vault + metrics-scrape
#### ingress-traefik
- namespace PSA: `baseline` (예외 ADR 기록)
- `hostNetwork` 금지(ServiceLB 또는 MetalLB 사용), hostPort는 80/443/8443만
- CAP_NET_BIND_SERVICE만 add
#### vault / vault-secrets-operator
- namespace PSA: `baseline` (Vault server IPC_LOCK 필요)
- storage PVC는 encrypted StorageClass
- unseal key는 cluster 밖(HSM/KMS auto-unseal)
#### db (Postgres/MySQL)
- namespace PSA: `restricted` (StatefulSet, fsGroup 999)
- NetworkPolicy: application SA의 Pod만 5432 허용
- backup은 별도 namespace의 Job에서 수행, 해당 Job에만 read-only secret 부여
## 프로젝트 기준 요약
- 모든 app namespace에 `pod-security.kubernetes.io/enforce: restricted` + pinned version 라벨 부착
- PSP는 제거되었으므로 어디에도 남기지 않는다
- Restricted 프로파일 전 필드 계약을 Pod/Deployment가 만족
- default-deny NetworkPolicy + DNS allow + ingress allow + metrics-scrape allow를 namespace 기본 세트로 배포
- `namespaceSelector`+`podSelector` AND/OR 차이를 정확히 사용
- Secret at-rest encryption + VSO(1순위) delivery 표준화
- 운영 이미지는 digest pin, private registry only
- audit policy + Kyverno/Gatekeeper 보완 정책과 함께 운영
+227
View File
@@ -0,0 +1,227 @@
# storage / PVC 기준
## 목적
PVC는 단순히 "데이터를 남기기 위한 옵션"이 아니라,
- 어떤 워크로드가 상태를 가지는지
- 그 상태의 수명과 복구 단위가 무엇인지
- 어떤 storage class / access mode / reclaim policy / binding mode가 필요한지
- snapshot / expansion 지원이 필요한지
를 먼저 고정한 뒤에 사용한다.
이 문서의 목표는 다음과 같다.
- 상태 저장 워크로드와 무상태 워크로드를 저장소 기준으로 명확히 구분한다
- separate PVC 남발을 막는다
- K3s 기본 local-path provisioner의 운영 사용 범위를 통제한다
- PVC lifecycle과 backup/restore 단위를 먼저 고정한다
- StorageClass / VolumeSnapshotClass / reclaim policy / binding mode를 선언적으로 명시한다
## 공식 의미 (Kubernetes 기준)
- PV는 클러스터의 저장소 리소스이며 Pod lifecycle과 독립적이다.
- PVC는 저장소에 대한 요청(size, access mode, StorageClass, volumeMode 등)이다.
- StorageClass는 동적 프로비저닝 파라미터, `reclaimPolicy`, `allowVolumeExpansion`, `volumeBindingMode`, `mountOptions`를 정의한다.
- `reclaimPolicy`는 PV 해제 시 동작을 결정한다. 동적 프로비저닝 PV의 기본값은 `Delete`다. 운영 데이터가 있으면 StorageClass에서 `Retain`을 명시한다.
- `volumeBindingMode`의 기본값은 `Immediate`이며, topology-aware / late-binding이 필요하면 `WaitForFirstConsumer`를 사용한다.
- `hostPath`는 single-node testing 전용이다. 운영 클러스터에서 사용하지 않는다.
- K3s는 Rancher Local Path Provisioner를 기본 제공해 노드 로컬 저장소를 사용할 수 있지만, RWO만 지원하고 snapshot/expansion은 지원하지 않는다.
- VolumeSnapshot / VolumeSnapshotContent / VolumeSnapshotClass는 CSI snapshot을 위한 K8s API다. `deletionPolicy: Retain` / `Delete`를 정책에 맞게 선택한다.
- StatefulSet은 `persistentVolumeClaimRetentionPolicy`로 삭제/스케일다운 시 PVC 보존 여부를 제어할 수 있다.
## 기본 규칙
### 1. PVC는 상태가 있을 때만 사용
다음 중 하나가 아니면 PVC를 붙이지 않는다.
- 재시작 후에도 유지되어야 하는 데이터가 있음
- Pod 교체와 무관하게 보존되어야 하는 파일/데이터가 있음
- 복구 대상이 되는 저장 상태가 있음
- 애플리케이션이 명시적으로 영속 저장소를 요구함
금지:
- "혹시 몰라서" PVC 추가
- 로그/캐시/임시 파일을 습관적으로 PVC에 저장
- stateless 앱에 관성적으로 PVC 부착
### 2. PVC 존재만으로 StatefulSet을 결정하지 않는다
PVC가 있다고 무조건 StatefulSet은 아니다.
먼저 묻는다.
- Pod마다 고유한 저장소가 필요한가?
- stable network identity가 필요한가?
- 순서 있는 확장/축소가 필요한가?
아니면:
- Deployment + 단일 PVC(RWO, replicas 1) 또는 Deployment + RWX PVC
도 가능하다.
### 3. separate PVC는 "데이터 수명과 복구 단위가 다를 때만"
하나의 워크로드가 여러 PVC를 가져도 되는 경우는 아래와 같다.
- 데이터 종류별 수명주기가 다름
- backup/restore 단위가 다름
- 성능 요구(StorageClass) 또는 IOPS 특성이 다름
- 보안/접근 제어 단위가 다름
- 장애 시 독립적으로 보존/삭제되어야 함
금지:
- 디렉터리 몇 개를 기계적으로 PVC로 분리
- mount path별로 습관적으로 PVC 추가
- 이유 없이 "앱 데이터/설정/로그"를 모두 개별 PVC로 분리
### 4. 기본 원칙은 "적게, 명확하게"
기본적으로는 하나의 워크로드 / 하나의 상태 저장 목적 / 하나의 PVC를 먼저 검토한다.
분리는 정당한 이유(#3)가 있을 때만 한다.
### 5. StorageClass는 항상 명시적으로 지정
PVC는 `storageClassName`을 항상 명시한다. 클러스터 default annotation에 의존하지 않는다.
기본:
- 운영 표준 StorageClass 3~5개를 미리 정의 (예: `fast-ssd-retain`, `standard-delete`, `archive-retain`, `rwx-shared`)
- 성능/복제/노드 종속성 차이가 있으면 workload별로 구분
- 각 StorageClass는 `provisioner`, `reclaimPolicy`, `volumeBindingMode`, `allowVolumeExpansion`을 모두 선언
### 6. StorageClass `volumeBindingMode` 기본값은 `WaitForFirstConsumer`
운영 표준은 `WaitForFirstConsumer`다.
이유:
- Pod가 스케줄되는 노드의 topology(zone, node-local disk, GPU affinity 등)에 맞춰 PV를 바인딩한다
- `Immediate`는 PVC 생성 즉시 PV를 바인딩하므로, 이후 Pod가 해당 노드/zone에 스케줄되지 못하는 상황이 생긴다
- K3s local-path provisioner는 노드 로컬이므로 반드시 `WaitForFirstConsumer`여야 한다
`Immediate` 허용 예외:
- 네트워크 스토리지(Ceph, NFS, S3 CSI 등)이고 topology 제약이 없는 경우
- 사전에 PV를 warm-up 해야 하는 특수 케이스
### 7. StorageClass `reclaimPolicy`는 데이터 등급에 맞춘다
동적 프로비저닝의 기본 `reclaimPolicy``Delete`다. 이는 PVC 삭제 시 PV와 데이터가 사라진다는 뜻이다.
기본:
- production stateful data (DB, object store backend, identity store 등) → `Retain`
- dev/test, ephemeral cache, rebuild-safe data → `Delete`
- `Retain`을 쓰면 PVC 삭제 후 남은 PV를 정리하는 책임이 운영자에게 생긴다. runbook에 정리 절차를 명시한다.
### 8. `allowVolumeExpansion`은 기본 `true`로 두되 축소는 불가
PVC 확장 요구는 자주 생긴다. StorageClass에서 `allowVolumeExpansion: true`를 기본으로 둔다.
주의:
- PVC 용량 축소는 K8s가 지원하지 않는다
- 파일시스템 online expansion 지원 여부는 CSI 드라이버마다 다르다
- 확장 후 Pod 재시작이 필요한 드라이버가 있다
### 9. AccessMode는 실제 요구에 맞게 고른다
기본:
- 단일 writer면 `ReadWriteOnce` (RWO)
- 동일 노드의 여러 Pod가 공유 필요시 `ReadWriteOncePod` (K8s 1.27+) 또는 RWO
- 여러 Pod/노드 동시 read/write가 진짜 필요할 때만 `ReadWriteMany` (RWX)
- 읽기 전용 공유는 `ReadOnlyMany` (ROX)
편의상 RWX를 기본값으로 두지 않는다. RWX는 NFS/CephFS 같은 별도 스토리지 백엔드를 요구한다.
### 10. K3s local-path provisioner는 운영에서 기본값 아님
K3s 기본 local-path provisioner의 하드 제약:
- RWO 전용 (RWX 불가)
- VolumeSnapshot 미지원
- VolumeExpansion 미지원
- 노드 로컬이므로 Pod가 특정 노드에 pin 됨 → 노드 장애 시 데이터 접근 불가
- backup은 노드 파일시스템에 직접 접근해야 함
기본:
- dev/test: 허용
- production: Longhorn, OpenEBS, Rook-Ceph, 또는 클라우드 CSI driver(EBS, PD, Azure Disk 등)로 교체
- 불가피하게 prod에서 local-path를 쓸 경우 `backup-restore.md`와 반드시 연동하고 노드 affinity/zone 분리를 명시
### 11. `hostPath` 직접 사용 금지
운영 PV/PVC에 `hostPath`를 사용하지 않는다.
예외:
- 학습/단일 노드 로컬 테스트
- 매우 제한된 디버깅 용도 (CSI driver 진단 등)
운영 표준으로 채택하지 않는다.
### 12. VolumeSnapshotClass를 StorageClass와 1:1로 매칭
snapshot 대상 PVC가 있는 StorageClass는 대응되는 VolumeSnapshotClass를 반드시 정의한다.
기본:
- `driver`는 StorageClass의 provisioner와 맞춤
- `deletionPolicy`는 운영 데이터면 `Retain`, ephemeral이면 `Delete`
- snapshot class는 `labels`로 RPO/retention 정책과 연결
### 13. PVC lifecycle은 workload 생성 전에 문서화
PVC를 만들기 전에 아래를 정한다.
- 누가 생성하는가 (Helm, Kustomize, Operator, manual)
- 누가 삭제하는가 (GitOps sync, 운영자 수동)
- scale down 시 어떻게 되는가
- workload 삭제 시 어떻게 되는가
- backup 대상인가 (어떤 RPO/RTO)
- restore 단위인가 (PVC / VolumeSnapshot / backup tool 별)
"삭제하면 같이 정리되겠지"를 금지한다.
### 14. StatefulSet의 PVC retention policy를 명시적으로 검토
StatefulSet을 쓰는 경우 `persistentVolumeClaimRetentionPolicy.whenDeleted` / `whenScaled`를 기본값에 두지 않는다.
기본:
- 운영 데이터: 둘 다 `Retain`
- ephemeral 데이터: 둘 다 `Delete`
- 혼용시 명시적 이유를 주석에 남김
### 15. Pod와 PVC는 같은 namespace 소유권
PVC는 Pod와 같은 namespace에서 사용된다. 스토리지도 workload의 namespace 소유권을 따라간다.
금지:
- "공용 저장소 namespace"에 무분별하게 PVC 몰아넣기
- 여러 서비스가 의미 없이 같은 PVC를 기대하는 구조
### 16. 워크로드별 기본 선택
| Workload | 기본 PVC | StorageClass | AccessMode | Snapshot |
|---|---|---|---|---|
| auth-server | 없음 | - | - | - |
| test-server | 없음 | - | - | - |
| ingress-controller | 없음 | - | - | - |
| migration-flyway (Job) | 없음 | - | - | - |
| Keycloak (external DB) | 없음 | - | - | - |
| PostgreSQL / CNPG | 필수 | fast-ssd-retain | RWO | 필수 |
| Vault (raft) | 필수 | fast-ssd-retain | RWO | 필수 |
| MinIO | 필수 | standard-retain | RWO | 보조 (replication 우선) |
### 17. 로그와 임시 파일은 PVC 기본 금지
다음은 기본적으로 PVC에 저장하지 않는다.
- application log (→ stdout + 로그 수집기)
- temp file (→ `emptyDir`)
- cache (→ `emptyDir` 또는 memory-backed)
- rendered config copy
- transient upload staging
정말 영속화가 필요하면 이유를 주석에 명시한다.
### 18. backup/restore와 반드시 연결
PVC를 허용한 워크로드는 반드시 아래와 연결한다.
- `backup-restore.md` (Velero schedule, snapshot class, RPO/RTO)
- `operations-runbook-upgrade-rollback.md` (복구 절차)
PVC가 생기면 복구 전략도 같이 생겨야 한다. 백업 없는 PVC는 merge 금지.
### 19. 파일시스템 / 블록 모드 명시
`volumeMode`는 기본 `Filesystem`이지만, DB raw block 같은 경우 `Block`을 쓸 수 있다. DB 운영이 요구하지 않으면 `Filesystem` 고정.
### 20. securityContext와 fsGroup
PVC를 쓰는 Pod는 `securityContext.fsGroup` 또는 `fsGroupChangePolicy: OnRootMismatch`를 명시해서 permission 문제를 예방한다. restricted PSA 하에서는 `runAsNonRoot: true`, `runAsUser`, `fsGroup`을 모두 설정한다.
## 프로젝트 기준 요약
- PVC는 상태가 있을 때만, separate PVC는 수명/복구 단위가 다를 때만
- StorageClass는 항상 명시, `volumeBindingMode: WaitForFirstConsumer` 기본, `reclaimPolicy`는 데이터 등급에 맞춤
- 동적 프로비저닝 기본 `reclaimPolicy=Delete`를 인지하고 운영 데이터는 `Retain` 명시
- K3s local-path는 RWO / no snapshot / no expansion — prod 기본값 아님
- VolumeSnapshotClass를 StorageClass와 매칭해서 정의
- StatefulSet PVC retention policy 명시
- 로그/임시 파일은 PVC 기본 금지
- PVC가 생기면 backup/restore 기준도 같이 만든다
+277
View File
@@ -0,0 +1,277 @@
# Vault 기준
## 목적
이 문서는 Kubernetes 환경에서 HashiCorp Vault 1.17+ 를 1000+ 서비스의 secret / PKI / dynamic credential 소스로 운영하기 위한 기준을 고정한다.
- Integrated Storage (Raft) HA + auto-unseal을 1차 권장 경로로 둔다
- 공식 Helm chart (`hashicorp/vault`) values.yaml의 핵심 필드를 명시한다
- Vault Secrets Operator (VSO) 0.8+ CRD 경로를 secret delivery 기본값으로 둔다
- Vault Agent Injector는 Kubernetes Secret을 우회하고 싶은 워크로드의 2차 경로로 둔다
- Raft snapshot / audit device / telemetry / TLS / Kubernetes auth role을 운영 필수 요소로 둔다
## 공식 의미 (Vault 1.17+ 기준)
- Vault는 **sealed** 상태로 기동한다. Shamir 수동 unseal 또는 auto-unseal (`awskms`, `gcpckms`, `azurekeyvault`, `transit`)로 unseal한다.
- **Integrated Storage (Raft)**는 공식 지원 HA backend다. 기동 시 `storage "raft"` stanza, `cluster_addr`, listener의 `cluster_address`가 모두 필요하다. `ha_storage`와 동시 선언 금지.
- Vault는 두 포트를 쓴다: **`8200` (API/client), `8201` (cluster-to-cluster Raft replication)**. Service는 8201을 반드시 expose해야 peer-to-peer Raft가 성립한다.
- `/v1/sys/health` 는 단일 endpoint로 상태 코드로 응답한다: `200` active, `429` standby (`standbyok=true`면 200), `472` DR secondary, `473` performance standby, `501` uninitialized, `503` sealed.
- **Audit device는 최소 하나 활성화해야 한다.** audit device가 전부 실패하면 Vault는 요청 처리를 멈춘다(블로킹). 여러 개 운영 권장.
- Kubernetes auth method는 ServiceAccount JWT를 TokenReview API로 검증한다. Vault 1.17+는 short-lived projected SA token(`audiences`)을 권장한다.
- VSO 0.8+는 `secrets.hashicorp.com/v1beta1` API group을 사용하고 `VaultConnection`, `VaultAuth`, `VaultStaticSecret`, `VaultDynamicSecret`, `VaultPKISecret`, `HCPAuth`, `HCPVaultSecretsApp` CRD를 제공한다.
- Vault Agent Injector는 `vault.hashicorp.com/agent-inject: "true"` 같은 Pod annotation으로 sidecar/init container를 주입해 secret을 `/vault/secrets/<name>` 파일로 렌더링한다.
- DR replication / Performance replication은 **Enterprise 기능**이다. OSS에서는 Raft snapshot restore가 복구 경로다.
- Telemetry는 `telemetry { prometheus_retention_time = "24h" disable_hostname = true }` stanza로 활성화하고 `/v1/sys/metrics?format=prometheus`에서 scrape한다.
## 기본 규칙
### 1. 배포는 공식 Helm chart (`hashicorp/vault`)
기본:
- `helm repo add hashicorp https://helm.releases.hashicorp.com`
- `server.ha.enabled=true` + `server.ha.raft.enabled=true`
- `injector.enabled` 는 secret delivery 전략에 따라 결정 (VSO만 쓰면 `false`)
- values.yaml은 Git에 보관 + Helmfile / Argo CD Application로 배포
기본 금지:
- 수제 StatefulSet으로 처음부터 조립
- `dev` 모드 운영
- chart 기본 `standalone` 모드 production 사용 (single node + file storage)
### 2. HA topology: Raft 3-node 또는 5-node
기본:
- `server.ha.replicas: 3` (과반수 장애 허용: 1 node)
- critical path면 `5`로 확장 (2 node 장애 허용)
- `server.ha.raft.setNodeId: true` (각 pod의 hostname을 node_id로 자동 주입)
- anti-affinity: hostname 기준 required, zone 기준 preferred
### 3. Raft config: listener 8200 + cluster 8201 + service_registration
`server.ha.raft.config` HCL에 최소한 아래 stanza가 필요하다.
```hcl
ui = true
listener "tcp" {
address = "[::]:8200"
cluster_address = "[::]:8201"
tls_disable = 0
tls_cert_file = "/vault/tls/tls.crt"
tls_key_file = "/vault/tls/tls.key"
}
storage "raft" {
path = "/vault/data"
node_id = "$(HOSTNAME)"
}
cluster_addr = "https://$(HOSTNAME).vault-internal:8201"
api_addr = "https://$(HOSTNAME).vault-internal:8200"
service_registration "kubernetes" {}
telemetry {
prometheus_retention_time = "24h"
disable_hostname = true
}
```
`cluster_addr`는 headless service(`vault-internal`)의 pod FQDN을 쓴다. 8201 Service expose 필수.
### 4. Auto-unseal 채택 (1차 권장)
기본:
- AWS: `seal "awskms" { region = "..." kms_key_id = "..." }`
- GCP: `seal "gcpckms" { project = "..." region = "..." key_ring = "..." crypto_key = "..." }`
- Azure: `seal "azurekeyvault" { tenant_id = "..." vault_name = "..." key_name = "..." }`
- Vault-to-Vault: `seal "transit" { address = "..." token = "..." key_name = "autounseal" mount_path = "transit/" }`
기본 금지:
- Shamir key를 CI/CD 환경변수나 Kubernetes Secret에 저장
- seal backend에 lifecycle 보호 없음 (KMS key deletion protection 필수)
### 5. Audit device는 최소 2개
Audit device 전부 실패 시 Vault가 요청을 block한다. redundancy 확보.
기본:
- `auth/kubernetes/login` 경로 포함 모든 API 감사
- `file`: `server.auditStorage.enabled: true``/vault/audit/audit.log`
- `syslog` 또는 `socket`: 중앙 로그 파이프라인 (Loki, Splunk, CloudWatch)
- `vault audit enable file file_path=/vault/audit/audit.log`
기본 금지:
- audit device 0개 운영
- audit log PVC 용량 무제한 (log rotation + sink 필수)
### 6. TLS는 end-to-end
기본:
- cert-manager Certificate로 `vault-tls` Secret 발급 (cluster issuer)
- listener에 `tls_cert_file`, `tls_key_file`, `tls_min_version = "tls13"`
- client (app, VSO, Injector)는 CA bundle trust
- Vault ↔ Storage ↔ seal backend 전 구간 TLS
### 7. Vault는 기본 내부 전용 (ClusterIP)
기본:
- Service type: ClusterIP (8200, 8201)
- Ingress 기본 금지
- 외부 관리자 접근은 VPN / bastion / port-forward / OIDC-protected admin Ingress
### 8. Probe: `/v1/sys/health` 상태코드 의미 반영
기본:
- readiness: `GET /v1/sys/health?standbyok=true&perfstandbyok=true&uninitcode=204` (uninitialized를 200으로 수용 초기 bootstrap 허용)
- liveness: `GET /v1/sys/health?standbyok=true&sealedcode=204&uninitcode=204` (sealed + uninit이어도 pod 생존)
- startup: initialDelay 10s, failureThreshold 12 (2분 유예)
기본 금지:
- `GET /` 단순 probe
- sealed 상태에서 liveness 실패 → 무한 재시작 루프
### 9. Kubernetes auth method 구성
Vault 쪽 (1회 bootstrap):
```bash
vault auth enable kubernetes
vault write auth/kubernetes/config \
token_reviewer_jwt="$(cat /var/run/secrets/kubernetes.io/serviceaccount/token)" \
kubernetes_host="https://kubernetes.default.svc.cluster.local" \
kubernetes_ca_cert=@/var/run/secrets/kubernetes.io/serviceaccount/ca.crt \
disable_iss_validation=false
```
Role은 ServiceAccount + namespace에 바인딩:
```bash
vault write auth/kubernetes/role/auth-server \
bound_service_account_names=auth-server \
bound_service_account_namespaces=auth-prod \
policies=auth-server-read \
ttl=1h \
audience=vault
```
기본 금지:
- `bound_service_account_names=*` 또는 `bound_service_account_namespaces=*`
- TTL 무한 또는 24h 이상
### 10. Secret delivery: VSO가 1차 권장
기본:
- 클러스터 전체 1개 `VaultConnection` (namespace: `vault`)
- 앱 namespace마다 `VaultAuth` (ServiceAccount 바인딩)
- 정적 KV 동기화: `VaultStaticSecret`
- 동적 DB credential: `VaultDynamicSecret`
- TLS 인증서: `VaultPKISecret`
- `destination.create: true`로 K8s Secret 자동 생성, `rolloutRestartTargets`로 consumer 재시작
### 11. Vault Agent Injector: Kubernetes Secret 우회가 필요할 때
기본 annotation set:
```yaml
vault.hashicorp.com/agent-inject: "true"
vault.hashicorp.com/role: "auth-server"
vault.hashicorp.com/agent-inject-secret-db-creds: "database/creds/auth-server"
vault.hashicorp.com/agent-inject-template-db-creds: |
{{ with secret "database/creds/auth-server" -}}
DATABASE_USERNAME={{ .Data.username }}
DATABASE_PASSWORD={{ .Data.password }}
{{- end }}
vault.hashicorp.com/agent-pre-populate-only: "true" # init-only (앱이 파일 1회 읽음)
vault.hashicorp.com/agent-inject-file-db-creds: "db.env"
```
기본:
- etcd에 민감정보를 남기고 싶지 않을 때 선택
- 앱이 파일 기반 secret 소비 가능해야 함
- 장기 실행 sidecar 대신 `agent-pre-populate-only: "true"`로 init container만 사용해 resource overhead 감소
### 12. Raft snapshot 백업은 운영 필수
기본:
- 하루 1회 `vault operator raft snapshot save` CronJob
- snapshot을 off-cluster object storage (S3, GCS, MinIO replicated bucket)에 저장
- retention 30일 이상 + 주간 / 월간 snapshot 분리
- restore 절차를 runbook으로 문서화
### 13. Telemetry + Prometheus scrape
기본:
- config: `telemetry { prometheus_retention_time = "24h" disable_hostname = true }`
- 내부 Prometheus token policy:
```
path "sys/metrics" { capabilities = ["read"] }
```
- Prometheus scrape: `/v1/sys/metrics?format=prometheus` + Bearer token (unauth-endpoint 가능하지만 권장하지 않음)
### 14. 포트 expose: 8200 + 8201 둘 다
기본:
- Pod containerPort: 8200 (api), 8201 (cluster)
- Service `vault`: ClusterIP, 8200
- Service `vault-internal`: Headless, 8200 + **8201** (Raft peer discovery 필수)
- 8201 누락 시 Raft peer-to-peer 실패, leader election 불가
### 15. Replication 경계: OSS vs Enterprise
DR replication, performance replication, namespace multi-tenancy는 **Vault Enterprise** 전용이다.
OSS 기준 복구:
- Raft snapshot restore로 state 복원
- 동일 seal backend 요구 (auto-unseal이면 KMS key 필요)
기본 금지:
- OSS에서 DR topology를 가정한 설계
- Enterprise 기능을 OSS manifest에 넣기
### 16. Security context: Restricted PSS
기본:
- `runAsNonRoot: true`, `runAsUser: 100` (vault user)
- `readOnlyRootFilesystem: true`
- `allowPrivilegeEscalation: false`, `capabilities.drop: [ALL]`, `capabilities.add: [IPC_LOCK]` (mlockall을 위함, swap 방지)
- `seccompProfile: RuntimeDefault`
### 17. Resource 요청
기본 단일 replica (Raft 3 node 클러스터 중 하나):
- requests: `cpu: 250m`, `memory: 256Mi`
- limits: `cpu: 1`, `memory: 512Mi`
대규모 PKI / dynamic secret 발급량이 많으면 `memory: 1Gi` 이상.
### 18. Token / root token 취급
기본:
- `vault operator init` 출력 root token은 1회성 bootstrap
- 초기 설정 완료 후 `vault token revoke <root-token>`
- 장기 root 필요 시 `vault operator generate-root` 절차로 ephemeral 생성
- app token은 Kubernetes auth login 경로로만 발급
- CLI history에 unseal key, root token 남기지 않음 (`HISTCONTROL=ignorespace`)
### 19. 현재 스택 기본 권장안
- 배포: Helm chart `hashicorp/vault`, `server.ha.enabled=true` + `server.ha.raft.enabled=true`
- Replicas: 3
- Storage: Integrated Storage (Raft) + dataStorage PVC + auditStorage PVC
- Unseal: auto-unseal (awskms / gcpckms / azurekeyvault / transit)
- TLS: end-to-end, cert-manager Certificate
- Service: ClusterIP 8200 + Headless 8200/8201
- Ingress: 기본 금지 (관리자 경로만 OIDC-protected 예외)
- Probe: `/v1/sys/health` status-code aware
- Audit: file + syslog 중복
- Secret delivery: VSO 1차, Injector 2차
- Backup: daily Raft snapshot → off-cluster object storage
## 프로젝트 기준 요약
- Helm chart 공식 배포, HA Raft 3-node, auto-unseal
- 8200 (client) + 8201 (cluster) Service expose 필수
- `/v1/sys/health` status-code 기반 probe
- audit device 최소 2개, 전체 실패 시 block 특성 인지
- Kubernetes auth role은 SA + namespace 단위, wildcard 금지
- VSO 1차 / Injector 2차 (`agent-pre-populate-only` init-only 선호)
- Raft snapshot daily CronJob → off-cluster 보관
- Telemetry `/v1/sys/metrics?format=prometheus` + Prometheus token policy
- DR/perf replication은 Enterprise 기능, OSS 경계 분명
- Restricted PSS + IPC_LOCK capability (mlockall)
+324
View File
@@ -0,0 +1,324 @@
# workload selection 기준
## 목적
이 문서는 각 컴포넌트를
- Deployment
- StatefulSet
- DaemonSet
- Job
- CronJob
중 무엇으로 배포할지 먼저 고정한다.
목표:
- 상태 저장 / 무상태 / 노드로컬 / 일회성 / 주기성 워크로드를 섞지 않는다
- PVC가 필요하다는 이유만으로 StatefulSet을 선택하는 실수를 막는다
- Ingress controller / CNI / CSI / 로그 shipper / node-exporter 같은 노드로컬 에이전트를 Deployment로 배포하는 실수를 막는다
- migration / bootstrap / 백업을 장기 실행 앱과 분리한다
- 1000+ 서비스 스케일에서 operator-managed 패턴이 기본인 영역(DB, Kafka, monitoring)은 operator를 기본 선택으로 문서화한다
## 공식 의미 (근거)
- **Deployment**: stateless 장기 실행. `spec.replicas` 기반 수평 확장. ReplicaSet으로 rolling update. `https://kubernetes.io/docs/concepts/workloads/controllers/deployment/`.
- **StatefulSet**: stable network identity, stable persistent storage, ordered deployment/scaling. 각 Pod는 `<name>-<ordinal>` 이름을 가지고 PVC가 `volumeClaimTemplates`로 자동 생성. `persistentVolumeClaimRetentionPolicy` (GA since 1.27) 필드: 기본값 `{whenDeleted: Retain, whenScaled: Retain}`. `podManagementPolicy` (OrderedReady / Parallel). `updateStrategy` (RollingUpdate / OnDelete).
- **DaemonSet**: 선택된 모든 노드에 정확히 한 Pod를 실행. 노드 추가/제거에 따라 자동 생성/삭제. `https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/`.
- **Job**: 한 번 실행되어 완료. `restartPolicy: Never | OnFailure`. `backoffLimit` / `activeDeadlineSeconds` / `ttlSecondsAfterFinished` / `parallelism` / `completions`.
- **CronJob**: 시간 기반 스케줄 Job. `concurrencyPolicy: Allow | Forbid | Replace`, `startingDeadlineSeconds`, `successfulJobsHistoryLimit` / `failedJobsHistoryLimit`.
- **Operator pattern**: CRD + controller. 1000-서비스 스케일에서 DB/Kafka/Redis/monitoring은 사실상 Deployment/StatefulSet을 직접 쓰지 않고 operator가 소유.
## 기본 규칙
### 1. 기본 선택 기준은 "상태 + 수명 + 배치 위치"
3축으로 먼저 분류:
1. **수명**: 장기 실행 / 일회성 / 주기성
2. **상태**: stable identity + persistent storage 필요 / 불필요
3. **배치**: 노드별 한 Pod 필요 / cluster-wide 자유 배치
매핑:
- 장기 + 무상태 + 자유 배치 → **Deployment**
- 장기 + stateful + 자유 배치 → **StatefulSet** (또는 operator)
- 장기 + 무상태 + 노드별 한 Pod → **DaemonSet**
- 일회성 → **Job**
- 주기성 → **CronJob**
### 2. Deployment는 stateless 장기 실행 기본값
조건:
- Pod identity가 교체 가능
- durable state가 외부 DB / 외부 storage / 외부 cache에 있음
- 수평 확장이 자연스러움
- Pod 이름 / 순서가 의미 없음
적용 후보:
- `auth-server`
- `test-server` (장기 실행 모드)
- 외부 DB 사용하는 `keycloak`
- 대부분의 stateless API / worker
**필수 동반 리소스 (replicas≥2인 prod 워크로드):**
- `PodDisruptionBudget` (minAvailable ≥ 50% 또는 SLO tier에 맞춘 값)
- `HorizontalPodAutoscaler` v2 (behavior 포함)
- `topologySpreadConstraints` (zone + hostname)
- `ServiceMonitor` 또는 Prometheus scrape annotation
### 3. StatefulSet은 "stable identity + storage" 모두 맞을 때만
조건 중 하나라도 강하면:
- stable network identity (DNS name per replica) 필요
- stable persistent storage per Pod 필요
- ordered rollout / termination 필요
- replica 간 peer discovery가 ordinal에 의존
적용 후보:
- `postgres` (operator 없을 때 — 있으면 CloudNativePG 같은 operator 우선)
- `minio` (MinIO Operator 가능)
- `vault` (Raft storage mode)
- `etcd` (외부)
- `kafka`, `zookeeper` (Strimzi operator 우선)
- `elasticsearch` (ECK operator 우선)
**필수 선언**:
- `serviceName` (headless Service 참조)
- `volumeClaimTemplates`
- `podManagementPolicy: OrderedReady` 기본. Parallel은 peer discovery가 순서를 요구하지 않을 때만.
- `updateStrategy: RollingUpdate` + `partition`으로 canary rollout
- `persistentVolumeClaimRetentionPolicy` 명시 (prod 기본 `{whenDeleted: Retain, whenScaled: Retain}`)
### 4. DaemonSet은 노드 전역 에이전트 전용
조건:
- 노드별 한 개만 떠야 함 (또는 특정 노드 group에 한 개)
- 노드 추가/제거에 자동 반응
- hostPath / hostNetwork / hostPID 필요한 경우 다수
적용 후보:
- 로그 shipper: `fluent-bit`, `fluentd`, `vector`
- metric exporter: `node-exporter`, `cadvisor`
- CNI agent: `calico-node`, `cilium-agent`
- CSI node plugin: `longhorn-manager`, `ceph-csi-node`
- security agent: `falco`, `tetragon`
- service mesh node proxy: `istio-cni-node`
- ingress-nginx **DaemonSet 모드** (edge 노드가 고정되고 모든 edge 노드가 80/443 HostPort / hostNetwork로 외부 노출해야 할 때)
**Ingress controller: DaemonSet vs Deployment 결정**:
- **Deployment** + `Service type=LoadBalancer` (MetalLB / 외부 LB): 기본 권장. 노드와 ingress replica 수가 분리됨. HPA 적용 가능.
- **DaemonSet** + `hostNetwork: true` / HostPort 80,443: bare-metal + 외부 LB 없이 모든 노드가 ingress가 되어야 할 때. 80/443 노드 포트 점유, HPA 불가, 노드 수 = replica 수.
**필수**:
- `tolerations`로 노드 taint (예: `node-role.kubernetes.io/control-plane`) 대응 결정
- `nodeSelector` 또는 `affinity`로 대상 노드 그룹 명시 (label 기반)
- `updateStrategy: RollingUpdate` + `maxUnavailable` 지정
- `priorityClassName: system-node-critical` (필수 인프라 에이전트)
### 5. PVC가 있다고 무조건 StatefulSet 아니다
체크리스트:
- Pod마다 고유한 storage identity 필요? 아니면 단일 PVC 공유?
- Pod 이름 / 순서가 의미 있나?
- peer discovery가 stable DNS name에 의존?
아니면:
- **단일 replica Deployment + PVC (ReadWriteOnce)** 도 유효
- **Deployment + ReadWriteMany PVC** (여러 replica가 동일 storage 공유) 도 유효 (shared cache 등)
### 6. Job은 migration / bootstrap / one-off 기본값
적용 후보:
- `flyway-migrate` / `liquibase-migrate`
- schema validation
- 초기 admin 사용자 bootstrap
- 데이터 리페어 / 정리
- 이미지 빌드 trigger
**필수**:
- `restartPolicy: Never` (실패 원인 디버깅 가능) 또는 `OnFailure` (transient 실패 재시도)
- `backoffLimit` 명시 (기본값 6은 prod에서 너무 관대할 수 있음)
- `activeDeadlineSeconds` (무한 실행 방지)
- `ttlSecondsAfterFinished` (완료 Job 자동 정리, 1000-서비스 스케일 필수)
- ServiceAccount 최소 권한
### 7. CronJob은 주기 실행 전용
적용 후보:
- etcd / DB 백업
- 정기 정리 (old PVC, old Snapshot, old Job)
- 정기 검증 / 리포트
- 비즈니스 배치 (야간 집계)
**필수**:
- `concurrencyPolicy: Forbid` 기본 (동시 실행 방지). 멱등하면 `Allow`.
- `startingDeadlineSeconds` (노드 장애로 miss 된 job 무한 누적 방지)
- `successfulJobsHistoryLimit: 3` / `failedJobsHistoryLimit: 5`
- schedule timezone 명시 (`spec.timeZone` v1.25+)
금지:
- 항상 떠 있어야 하는 서버를 CronJob으로 배포
- 본 서비스 온라인 처리를 CronJob에 의존
### 8. Stateful workload는 retention / scale-down 정책을 먼저 박는다
StatefulSet의 `persistentVolumeClaimRetentionPolicy`:
- `whenDeleted` (StatefulSet이 삭제될 때 PVC 처리): `Retain` (기본) / `Delete`
- `whenScaled` (replica 축소될 때 PVC 처리): `Retain` (기본) / `Delete`
**prod 기본**: `{whenDeleted: Retain, whenScaled: Retain}` (기본값). DB/Vault/MinIO 모두 여기서 이탈하지 않는다.
**dev/staging**: `{whenDeleted: Delete, whenScaled: Delete}` 허용 (클러스터 재생성 시 자동 정리).
### 9. 외부 DB를 쓰는 앱 서버는 stateless 우선
Pod에 durable state가 없으면 Deployment. Pod identity가 고정되어야 한다는 이유만으로 StatefulSet 선택 금지.
기준:
- `auth-server` → Deployment
- 외부 DB 사용 `keycloak` → Deployment (caching은 external Redis / Infinispan cluster)
- 외부 object store 사용 앱 → Deployment
### 10. Keycloak은 앱 레이어와 저장소를 분리
Keycloak 서버 자체는 stateless로 다룬다.
- **외부 DB (PostgreSQL)** 사용이 prod 기본
- session / cache는 Infinispan embedded 또는 remote 모드 결정 (remote 선호, replica 간 peer discovery는 Kubernetes DNS)
- **Deployment** + externalTrafficPolicy 고려
- HA replica ≥ 2 + PDB
### 11. Vault는 모드별로 다르다
- **dev mode**: 학습 전용. prod 절대 금지.
- **standalone (file storage)**: StatefulSet + PVC. replica=1. 단일 장애점.
- **HA Raft**: StatefulSet (integrated storage). replica 3 또는 5. `podManagementPolicy: Parallel` 허용.
- **HA Consul backend**: StatefulSet (Vault) + StatefulSet (Consul). operator 권장.
- **external Vault**: 클러스터 내부 서버 없음, ExternalSecrets로 참조만.
prod 권장: **HA Raft mode StatefulSet (replica 3)** 또는 **external Vault**.
### 12. MinIO는 Operator 기본 (StatefulSet은 fallback)
1000-서비스 스케일에서 MinIO는 MinIO Operator + `Tenant` CRD가 기본. tenant가 StatefulSet을 내부적으로 생성.
raw StatefulSet은 단일 node/dev 환경에서만 예외 허용.
### 13. Flyway는 Job 기본값
- 앱 startup 내부 migration 금지 (앱 부팅 실패와 migration 실패가 섞임)
- Flyway Job이 선행되고 Success 후에 Deployment rollout
- ArgoCD PostSync hook 또는 Argo Workflows로 순서 제어
- `migrate`, `validate`, `info`, `repair` 각각 독립 Job
### 14. Ingress controller 배치 결정
prod 권장:
- **ingress-nginx Deployment** + `Service type=LoadBalancer` (MetalLB L2 또는 BGP, 또는 외부 LB)
- 또는 **Envoy Gateway / Gateway API 기반 Deployment**
- HPA 가능, PDB 필수 (tier-1 로 취급)
- 복수 IngressClass (`nginx-public`, `nginx-internal`) 분리
DaemonSet 선택 조건:
- edge 노드가 고정되어 있고 hostNetwork 80/443이 필요
- 외부 LB가 없고 DNS round-robin으로 다수 노드 IP 노출
### 15. DB는 operator 기본, StatefulSet은 fallback
1000-서비스 스케일의 PostgreSQL:
- **CloudNativePG Operator** 기본 → `Cluster` CRD. operator가 StatefulSet/Service/Secret/ConfigMap/Backup 전부 관리.
- **Zalando Postgres Operator**도 대안
- raw StatefulSet은 dev / 특수 케이스에만
MySQL/MariaDB:
- **MariaDB Operator** / **mysql-operator**
Redis:
- **Spotahome redis-operator** / **Redis Enterprise Operator**
- cluster 모드면 StatefulSet, sentinel 모드면 Deployment(sentinel) + StatefulSet(redis)
### 16. Batch / 대량 병렬은 Job + `parallelism` + IndexedJob
단일 Job으로 수천 개 task 병렬 실행:
- `completionMode: Indexed` + `parallelism: N`
- 각 Pod가 `JOB_COMPLETION_INDEX` env로 자기 작업 식별
- 더 복잡한 DAG는 Argo Workflows / Tekton
### 17. workload 종류만 맞는다고 품질이 보장되지 않는다
최종 결정 전 동반 표준 확인:
- `storage-pvc.md` (StorageClass / volumeClaimTemplate / snapshot)
- `network-ingress-tls.md` (ingressClassName / TLS)
- `resources-probes-availability.md` (PDB / HPA / probe / resources)
- `backup-restore.md` (RPO / RTO / 절차)
- `security-podsecurity.md` (PSA / seccomp / capabilities)
- `observability.md` (ServiceMonitor / log shipping)
### 18. priority class / preemption 전략
- 플랫폼 에이전트 (CNI, CSI, log shipper, node-exporter): `system-node-critical`
- 클러스터 컨트롤러 (cert-manager, external-secrets, operator): `system-cluster-critical`
- 비즈니스 tier-1: 커스텀 `tier-1-critical` (value 1000000)
- 비즈니스 tier-2: `tier-2` (value 100000)
- 비즈니스 tier-3 / batch: `tier-3` (value 10000)
## 현재 스택 기본 권장안 (prod)
| 컴포넌트 | workload 종류 | operator | 비고 |
|----------------------|--------------------------------|-------------------|--------------------------------|
| `auth-server` | Deployment | — | tier-1 HPA+PDB |
| `test-server` (장기) | Deployment | — | |
| `test-server` (검증) | Job | — | ttlSecondsAfterFinished |
| `keycloak` | Deployment (외부 DB) | — | HA replicas≥2 |
| `postgres-identity` | StatefulSet (via CNPG) | CloudNativePG | replica 3 |
| `vault` | StatefulSet (Raft) | Vault Operator | replica 3 |
| `minio` | StatefulSet (via Tenant) | MinIO Operator | |
| `kafka` | StatefulSet (via Strimzi) | Strimzi | |
| `redis` | StatefulSet (via operator) | redis-operator | sentinel 또는 cluster 모드 |
| `flyway-migrate` | Job | — | ArgoCD PostSync hook |
| `postgres-backup` | CronJob | CNPG ScheduledBkp | operator가 소유 |
| `ingress-nginx` | Deployment + MetalLB | — | public / internal 분리 |
| `cert-manager` | Deployment | — | system-cluster-critical |
| `external-secrets` | Deployment | — | system-cluster-critical |
| `prometheus` | StatefulSet (via Prometheus) | Prometheus Op | |
| `fluent-bit` | DaemonSet | — | system-node-critical |
| `node-exporter` | DaemonSet | — | system-node-critical |
| `cilium-agent` | DaemonSet | Cilium Op (opt) | system-node-critical |
| `longhorn-manager` | DaemonSet | Longhorn | |
## 프로젝트 기준 요약
- stateless 장기 → Deployment (+PDB+HPA+topologySpread 필수)
- stateful + stable identity/storage → StatefulSet (또는 operator)
- 노드 전역 에이전트 → DaemonSet
- 일회성 → Job (ttlSecondsAfterFinished 필수)
- 주기성 → CronJob (concurrencyPolicy + startingDeadlineSeconds)
- PVC ≠ StatefulSet 신호 전부 아님
- DB/Kafka/Redis/Prometheus는 operator 기본
- `persistentVolumeClaimRetentionPolicy` 명시, prod는 Retain/Retain
- Flyway는 Job, 앱 startup 내부 migration 금지
- ingress controller는 Deployment+LB 기본, DaemonSet은 edge 조건에만
File diff suppressed because it is too large Load Diff
+62
View File
@@ -0,0 +1,62 @@
# Docs validation report
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)
## Summary
| Metric | Count |
|---|---|
| Files scanned | 37 |
| YAML blocks extracted | 146 |
| K8s-resource blocks | 109 |
| YAML syntax errors | 0 |
| Schema invalid/errors | 0 |
| kube-linter findings | 0 |
## Per-file
| File | Blocks | K8s | Syntax | Schema | Lint |
|---|---|---|---|---|---|
| docs/examples/infra/architecture-environments.md | 4 | 3 | 0 | 0 | 0 |
| docs/examples/infra/backup-restore.md | 9 | 7 | 0 | 0 | 0 |
| docs/examples/infra/config-and-secrets.md | 9 | 8 | 0 | 0 | 0 |
| docs/examples/infra/db-and-migration.md | 6 | 4 | 0 | 0 | 0 |
| docs/examples/infra/flyway.md | 5 | 4 | 0 | 0 | 0 |
| docs/examples/infra/k3s-specific.md | 9 | 2 | 0 | 0 | 0 |
| docs/examples/infra/keycloak.md | 7 | 5 | 0 | 0 | 0 |
| docs/examples/infra/kustomize.md | 8 | 8 | 0 | 0 | 0 |
| docs/examples/infra/minio.md | 7 | 6 | 0 | 0 | 0 |
| docs/examples/infra/network-ingress-tls.md | 8 | 8 | 0 | 0 | 0 |
| docs/examples/infra/observability-health.md | 7 | 7 | 0 | 0 | 0 |
| docs/examples/infra/operations-runbook-upgrade-rollback.md | 5 | 5 | 0 | 0 | 0 |
| docs/examples/infra/resources-probes-availability.md | 7 | 7 | 0 | 0 | 0 |
| docs/examples/infra/scripts.md | 0 | 0 | 0 | 0 | 0 |
| docs/examples/infra/security-hardening.md | 6 | 6 | 0 | 0 | 0 |
| docs/examples/infra/storage-pvc.md | 13 | 11 | 0 | 0 | 0 |
| docs/examples/infra/vault.md | 9 | 7 | 0 | 0 | 0 |
| docs/examples/infra/workload-selection.md | 6 | 6 | 0 | 0 | 0 |
| docs/standards/infra/architecture-environments.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/backup-restore.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/config-and-secrets.md | 1 | 1 | 0 | 0 | 0 |
| docs/standards/infra/db-and-migration.md | 2 | 0 | 0 | 0 | 0 |
| docs/standards/infra/flyway.md | 2 | 0 | 0 | 0 | 0 |
| docs/standards/infra/k3s-specific.md | 1 | 1 | 0 | 0 | 0 |
| docs/standards/infra/keycloak.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/kustomize.md | 5 | 0 | 0 | 0 | 0 |
| docs/standards/infra/minio.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/network-ingress-tls.md | 1 | 0 | 0 | 0 | 0 |
| docs/standards/infra/observability-health.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/operations-runbook-upgrade-rollback.md | 1 | 1 | 0 | 0 | 0 |
| docs/standards/infra/resources-probes-availability.md | 1 | 0 | 0 | 0 | 0 |
| docs/standards/infra/scripts.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/security-hardening.md | 1 | 0 | 0 | 0 | 0 |
| docs/standards/infra/storage-pvc.md | 0 | 0 | 0 | 0 | 0 |
| docs/standards/infra/STYLE.md | 5 | 2 | 0 | 0 | 0 |
| docs/standards/infra/vault.md | 1 | 0 | 0 | 0 | 0 |
| docs/standards/infra/workload-selection.md | 0 | 0 | 0 | 0 | 0 |
## Error details
_No errors found._
+98
View File
@@ -0,0 +1,98 @@
# Vault / VSO 상세
README 의 Vault·VSO 핵심 섹션을 보충한다. Kubernetes auth 초기화 명령, policy/role 매핑, VaultStaticSecret 카탈로그, dockerconfigjson `.auth` 이슈를 모은다.
## Vault 기본 정보
| 항목 | 값 |
|---|---|
| 이미지 | `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` (외부 노출 금지) |
## 설계 결정
- **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 불필요.
## RBAC
ClusterRoleBinding `vault-tokenreview-binding``system:auth-delegator`. Vault 의 Kubernetes auth method 는 클라이언트(VSO 등)가 제출한 ServiceAccount JWT 를 `TokenReview` + `SubjectAccessReview` API 로 검증한다. 이 ClusterRoleBinding 이 없으면 VSO 로그인이 `permission denied` 로 실패한다.
Vault Pod 의 ServiceAccount 는 `automountServiceAccountToken: true` (기본). Vault 는 `/var/run/secrets/kubernetes.io/serviceaccount/{token,ca.crt}` 를 읽어 `auth/kubernetes/config``token_reviewer_jwt` / `kubernetes_ca_cert` 를 채운다.
## Kubernetes auth 초기 설정
`tasks/vault-init.sh` 가 수행:
```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
```
## 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.
Executable
+901
View File
@@ -0,0 +1,901 @@
# Project-Infra 운영 가이드
아키텍처 · 폴더 구조 · 설계 결정은 [README.md](README.md) 를 먼저 읽는다. 본 문서는 **실제 배포·운영 절차** 에 집중한다. 보안 정책 / 거버넌스 (etcd 암호화, Vault 토큰 관리, bash history 보호) 는 [docs/security-hardening.md](docs/security-hardening.md) 참고.
---
## 목차
1. [사전 준비](#1-사전-준비)
2. [최초 부트스트랩 — 자동](#2-최초-부트스트랩--자동)
3. [최초 부트스트랩 — 수동 (단계별)](#3-최초-부트스트랩--수동-단계별)
4. [Traefik / Ingress 운영](#4-traefik--ingress-운영)
5. [TLS / cert-manager 적용](#5-tls--cert-manager-적용)
6. [Keycloak Operator / RealmImport 적용](#6-keycloak-operator--realmimport-적용)
7. [Vault 시크릿 관리](#7-vault-시크릿-관리)
8. [Docker Registry 사용법](#8-docker-registry-사용법)
9. [앱에서 시크릿 사용하기](#9-앱에서-시크릿-사용하기)
10. [마이그레이션 실행 (Flyway)](#10-마이그레이션-실행-flyway)
11. [Vault UI 접근](#11-vault-ui-접근)
12. [환경별 배포](#12-환경별-배포)
13. [검증 / 린트 / 스키마 체크](#13-검증--린트--스키마-체크)
14. [정리 / 롤백 (teardown)](#14-정리--롤백-teardown)
15. [트러블슈팅](#15-트러블슈팅)
---
## 1. 사전 준비
> **예상 소요**: 첫 셋업 30 분 (CLI 설치 포함). 두 번째부터는 0.
### 필요한 CLI
| 도구 | 용도 |
|---|---|
| `kubectl` | 클러스터 조작 |
| `helm` | VSO Operator 설치 |
| `jq` | JSON 파싱 (scripts 내부) |
| `kustomize` | (선택) 로컬 렌더 |
| `kubeconform` | (선택) 스키마 검증 |
| `kube-linter` | (선택) 안티패턴 린트 |
| `yq` | (선택) YAML 가공 |
> **참고**: `validate.sh``~/bin` 에 설치된 도구도 자동으로 PATH 에 추가한다.
### 필요한 환경 변수
| 변수 | 의미 |
|---|---|
| `CONFIRM=yes` | (teardown 전용) 대화형 확인 자동 yes 처리 |
| `RESET_STALE_SECRETS=yes` | (bootstrap 전용) 기존 VSO-managed Secret 삭제 후 재생성 |
| `AUTO_GENERATE=yes` | (vault-seed-apps 전용) 비대화 + env 없음 시 랜덤 비밀번호 생성 |
| `POSTGRES_SUPERUSER_PASSWORD` | (bootstrap Phase 6 비대화) Postgres superuser 비밀번호 |
| `KEYCLOAK_DB_PASSWORD` | (bootstrap Phase 6 비대화) Keycloak DB 비밀번호 |
| `AUTH_SERVER_DB_PASSWORD` | (bootstrap Phase 6 비대화) auth-server DB 비밀번호 |
| `KEYCLOAK_ADMIN_PASSWORD` | (bootstrap Phase 6 비대화) Keycloak 초기 관리자 비밀번호 |
| `MINIO_ROOT_PASSWORD` | (bootstrap Phase 6 비대화) MinIO Tenant 루트 비밀번호 |
> **주의**: env var 사용 시 [bash history 보호](docs/security-hardening.md#3-bash-history-에-비밀번호-남기지-않기) 참고. `HISTFILE=/dev/null` 접두 권장.
### 클러스터 전제
- Kubernetes 1.25+ (Pod Security Admission 사용)
- `local-path` StorageClass (K3s 기본) 또는 동등한 RWO 프로비저너
- `kubernetes.io/metadata.name` namespace 라벨이 자동으로 붙는 1.22+ 환경
- dev 클러스터의 K3s 기본 Traefik 이 `kube-system` namespace 에 존재해야 함
---
## 2. 최초 부트스트랩 — 자동
> **예상 소요**: 10~15 분 (이미지 pull 시간 포함). Phase 6 의 시크릿 입력이 가장 오래 걸림.
### 대화형 실행 (권장)
Phase 6 에서 앱 시크릿 5 개 비밀번호를 무음 입력. bash history 에 남지 않는다.
```bash
bash k8s/scripts/bin/bootstrap.sh dev
# Phase 6 진행 중:
# Postgres superuser 비밀번호: *******
# Postgres superuser 비밀번호 한 번 더: *******
# Keycloak DB 비밀번호: *******
# ... (5 개 시크릿, 각각 확인 재입력 포함)
```
### 비대화 (CI) 실행
```bash
HISTFILE=/dev/null \
POSTGRES_SUPERUSER_PASSWORD='...' \
KEYCLOAK_DB_PASSWORD='...' \
AUTH_SERVER_DB_PASSWORD='...' \
KEYCLOAK_ADMIN_PASSWORD='...' \
MINIO_ROOT_PASSWORD='...' \
bash k8s/scripts/bin/bootstrap.sh dev
# 또는 비대화 + 랜덤 생성 (운영자가 값을 몰라도 됨, Vault 에서 나중에 조회):
AUTO_GENERATE=yes bash k8s/scripts/bin/bootstrap.sh dev
```
### 실행 단계
`bin/bootstrap.sh` 가 9 단계 (Phase 0~8) 를 순서대로 실행한다:
| Phase | 내용 |
|:---:|---|
| 0 | MinIO Operator 설치 (`tasks/minio-operator-install.sh`) — Tenant CRD 선행 등록 (별도 namespace `minio-operator`) |
| 1 | Namespace + PSS 라벨 (`kubectl apply -k k8s/base/managing/namespace`) — 먼저 적용해 의존성 안정화 |
| 2 | 기존 Secret 점검 — VSO-managed K8s Secret 5 개 중 이미 존재하는 것 탐지. `RESET_STALE_SECRETS=yes` 면 삭제 |
| 3 | 인프라 리소스 배포 (`kubectl apply -k k8s/overlays/dev/`) — vault + registry + 앱 워크로드 |
| 4 | vault-0 Running 대기 — Ready 가 아니라 **Running**. Vault readiness probe 는 초기화+unseal 후에만 통과하므로 |
| 5 | Vault 초기화 (`tasks/vault-init.sh`) — init / unseal / KV v2 / k8s auth / policy × 2 / role × 2 |
| 6 | 앱 시크릿 seed (`tasks/vault-seed-apps.sh`) — 5 개 시크릿 대화형 입력 (이미 있으면 skip) |
| 7 | VSO Helm (`tasks/vso-install.sh`) — 기존 dirty 릴리즈 자동 uninstall + `helm upgrade --install --wait` |
| 8 | VSO CRDs (`kubectl apply -k k8s/overlays/dev/vso/`) |
> **참고**: 스크립트는 idempotent 다. 이미 진행된 단계는 자동 스킵된다.
> **주의**: 현재 `bootstrap.sh``mnt` namespace 자원까지만 자동 배포한다. `k8s/overlays/dev/platform/traefik/``k8s/overlays/dev/tls/` 는 namespace 가 다르거나 optional dependency 가 있어 운영자가 별도로 적용한다 (§4, §5).
>
> `k8s/overlays/dev/platform/cert-manager/`, `k8s/overlays/dev/platform/keycloak-operator/`, `k8s/overlays/dev/keycloak-realm/` 은 CRD / 외부 DNS / 인증 흐름 의존성이 있어 자동 부트스트랩 전에 선행/별도 확인이 필요하다 (§5, §6).
### 생성되는 파일
- `vault-init-keys.json`**unseal keys (5 개) + root token**. 권한 0600 으로 저장.
> **주의**: 이 파일은 **반드시 오프라인 금고 / 외부 KMS 로 이동** 하고 원본은 삭제한다. `.gitignore` 에 등록되어 있으나 실수로도 커밋하지 말 것.
### 완료 확인
```bash
kubectl -n mnt get pods
kubectl -n mnt get secrets | grep -E 'identity-postgres-superuser|keycloak-db|auth-server-db|keycloak-bootstrap-admin|minio-tenant-env'
kubectl -n mnt get vaultstaticsecret
```
기대 상태: 모든 Pod `Running 1/1`, K8s Secret 5 개 존재, VaultStaticSecret `Status: Synced`.
---
## 3. 최초 부트스트랩 — 수동 (단계별)
> **예상 소요**: 자동과 동일하나 학습 시 +20~30 분.
자동 스크립트가 중간에 실패했을 때, 또는 학습 목적으로 단계별 진행이 필요할 때.
### 3-1. MinIO Operator 설치
```bash
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/minio-operator-install.sh
```
### 3-2. 인프라 배포
```bash
kubectl apply -k k8s/base/managing/namespace # namespace 선행
kubectl apply -k k8s/overlays/dev/
```
`mnt` namespace 와 Vault / Registry / 앱 워크로드가 선언된다.
> **참고**: Registry 는 MinIO S3 자격증명 Secret 이 주입되기 전까지 대기할 수 있다. Postgres / Keycloak / auth-server 도 Vault secret 이 주입되기 전까지 `ContainerCreating` 으로 대기한다 (정상).
### 3-3. Vault Pod Running 대기
> **참고**: Vault 는 초기화 전에는 Ready 가 될 수 없으므로 Running 까지만 기다린다 (§15.10 참고).
```bash
kubectl -n mnt wait --for=jsonpath='{.status.phase}'=Running pod/vault-0 --timeout=120s
```
### 3-4. Vault 초기화
```bash
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-init.sh
```
이 스크립트가 수행하는 것:
- `vault operator init -key-shares=5 -key-threshold=3` (이미 초기화되었으면 스킵)
- `vault-init-keys.json` 생성 (권한 0600)
- Sealed 상태면 자동 unseal
- root token 으로 로그인 (stdin 파이프 — stdout 에 안 찍힘)
- `secret/` 에 KV v2 활성화 (idempotent)
- `kubernetes` auth method 활성화 + `kubernetes_ca_cert` + `token_reviewer_jwt` 설정 (idempotent)
- `vso-auth-platform` / `vso-storage` policy × 2 작성 (항상 재적용)
- `vso-auth-platform` / `vso-storage` k8s auth role × 2 작성 (항상 재적용)
### 3-5. VSO Helm 설치
```bash
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vso-install.sh
```
`helm upgrade --install --wait --timeout 5m` 으로 실행. 시작 시 기존 릴리즈가 `failed`/`pending*`/`uninstalling` 상태면 자동 uninstall 후 재설치 (§15.11). `--values``k8s/base/plugins/vso/helm/values.yaml`.
### 3-6. VSO CRDs 적용
```bash
kubectl apply -k k8s/overlays/dev/vso/
```
`VaultConnection` / `VaultAuth` × 2 가 등록된다. `VaultStaticSecret` 은 dev overlay 각 서브디렉토리 (`database/`, `keycloak/`, `storage/`) 에서 이미 함께 적용됨. VSO Operator 가 Vault KV 를 읽어 K8s Secret 을 합성.
> **주의**: 해당 Vault KV 경로에 값이 실제로 있어야 성공. 아직 없으면 VSO 가 permission denied 또는 not found 로 남음. §7 의 수동 주입 후 자동 재시도.
---
## 4. Traefik / Ingress 운영
> **예상 소요**: 30 초~1 분 (apply 만).
### 왜 별도 overlay 인가
`k8s/overlays/dev/kustomization.yaml``namespace: mnt` 를 전역으로 주입한다. 반면 K3s 기본 Traefik 은 실제로 `kube-system` 에 존재한다. 그래서 Traefik 운영 리소스는 같은 kustomization 안에 섞지 않고 `k8s/overlays/dev/platform/traefik/` 로 분리했다.
### 적용 대상
| overlay | namespace | 설명 |
|---|---|---|
| `k8s/overlays/dev/platform/traefik` | `kube-system` | `HelmChartConfig` + `Middleware` + `TLSOption` |
| `k8s/overlays/dev/` | `mnt` | `auth-server`, `keycloak` 의 app Ingress 및 app NetworkPolicy |
### Traefik 운영 overlay 적용
```bash
kubectl apply -k k8s/overlays/dev/platform/traefik
```
포함되는 것:
- `HelmChartConfig/traefik``replicas=2`, `ingressClass=traefik`, HTTP→HTTPS redirect, metrics 활성화
- `Middleware/security-headers` — HSTS, `X-Content-Type-Options`, frame deny 등 공용 헤더
- `TLSOption/modern-tls` — TLS 1.2+, strict SNI, 허용 cipher suite
### 앱 Ingress 현재 상태
| 리소스 | host | 공개 범위 |
|---|---|---|
| `auth-server` | `project.com` | `/` |
| `keycloak-public` | `keycloak.dev.example.com` | `/realms/`, `/resources/`, `/.well-known/`, `/js/` |
app Pod 는 기본 deny 상태이므로, Traefik 에서 들어오는 8080/TCP 만 NetworkPolicy 로 별도 허용한다.
### ForwardAuth variant 적용
repo 에는 component `k8s/components/forward-auth/` 가 준비되어 있고, 현재 `k8s/overlays/dev/` 가 이 component 를 직접 포함한다. 따라서 dev overlay 를 적용하면 oauth2-proxy 와 `auth-server` 보호 middleware 도 함께 렌더링된다.
적용 전제:
1. `k8s/overlays/dev/keycloak-realm/` 또는 동등한 방법으로 `platform` realm + `auth-server-ingress` client 준비 (§6)
2. redirect URI 를 `https://project.com/oauth2/callback` 로 등록
3. Vault path `secret/oauth2-proxy/forward-auth` 에 아래 key 저장 (§7)
- `client-secret`
- `cookie-secret`
4. `project.com`, `keycloak.dev.example.com` 이 실제 Traefik 진입점으로 해석
적용:
```bash
kubectl apply -k k8s/overlays/dev
```
이 overlay 가 추가하는 것:
- `oauth2-proxy` Deployment / Service
- `project.com/oauth2/` 경로용 Ingress
- `oauth2-proxy-auth` Traefik `Middleware`
- `auth-server` Ingress patch — `project.com/` 요청은 oauth2-proxy ForwardAuth 를 먼저 통과해야 함
> **주의**: 현재 dev 용 oauth2-proxy 설정은 `ssl_insecure_skip_verify=false` 이다. 따라서 `keycloak.dev.example.com` 인증서 체인이 정상이어야 로그인 흐름이 끝까지 진행된다.
> **참고**: dev 운영 완료까지 남은 항목 (DNS / ACME 인증서 / realm 적용 / negative test 등) 은 [README Limitations](README.md#limitations-honest-scope) 참고.
---
## 5. TLS / cert-manager 적용
> **예상 소요**: 설치 3~5 분 + 외부 DNS 의존 (실제 발급은 DNS 가 Traefik 진입점을 가리켜야 가능).
cert-manager 는 repo source of truth 로 편입되어 있다. dev 기준 설치 overlay 는 `k8s/overlays/dev/platform/cert-manager/` 이며, 공식 static install `v1.20.2` 를 적용한다. `ClusterIssuer` 는 CRD 등록 이후 `k8s/overlays/dev/platform/cert-manager-issuers/` 로 별도 적용한다.
### 적용
```bash
kubectl apply -k k8s/overlays/dev/platform/cert-manager
kubectl -n cert-manager rollout status deploy/cert-manager --timeout=180s
kubectl -n cert-manager rollout status deploy/cert-manager-webhook --timeout=180s
kubectl -n cert-manager rollout status deploy/cert-manager-cainjector --timeout=180s
kubectl apply -k k8s/overlays/dev/platform/cert-manager-issuers
```
> **주의**:
> - `letsencrypt-prod-clusterissuer.yaml` / `letsencrypt-staging-clusterissuer.yaml``admin@project.com` 은 실제 수신 가능한 운영 메일로 교체한다.
> - HTTP-01 은 `project.com`, `keycloak.dev.example.com` 이 Traefik 외부 진입점으로 해석되고 80/443 이 도달 가능해야 성공한다.
### 준비된 Certificate 리소스
| 파일 | secretName | host |
|---|---|---|
| `k8s/overlays/dev/tls/project-com-certificate.yaml` | `project-com-tls` | `project.com` |
| `k8s/overlays/dev/tls/keycloak-dev-certificate.yaml` | `keycloak-dev-example-com-tls` | `keycloak.dev.example.com` |
### Certificate 적용
전제:
- `cert-manager` CRD 설치 완료
- `ClusterIssuer/letsencrypt-prod` 또는 동등한 issuer 준비
- DNS 가 실제 Traefik 진입점으로 향함
```bash
kubectl apply -k k8s/overlays/dev/tls
```
### 완료 확인
```bash
kubectl -n mnt get certificate
kubectl -n mnt describe certificate project-com # Status.Conditions.Ready=True
```
> **권장**: 인증서 발급 상태를 먼저 확인한 뒤 ForwardAuth E2E 검증을 진행한다 (§4).
---
## 6. Keycloak Operator / RealmImport 적용
> **예상 소요**: 5~10 분.
Keycloak 은 권장 흐름에 맞춰 Operator 기반으로 전환한다. dev 제약상 실제 Keycloak 인스턴스와 realm/client 는 `mnt` 에 두며, Keycloak Operator 도 `mnt` 에 설치해 해당 namespace 를 watch 하게 한다.
> **참고**: `mnt` 는 default-deny egress namespace 이므로, `k8s/overlays/dev/platform/keycloak-operator/networkpolicy.yaml` 이 Operator Pod 에서 Kubernetes API 로 나가는 443/6443 만 허용한다. 이 정책이 없으면 Operator informer 가 API server 에 연결하지 못해 CrashLoopBackOff 로 떨어진다.
### 왜 필요한가
- `oauth2-proxy``auth-server-ingress` client 를 전제로 동작한다
- client / redirect URI 같은 OIDC 계약은 Git 에서 관리되어야 drift 가 줄어든다
- 표준도 Keycloak realm 을 `KeycloakRealmImport` 로 선언형 관리하라고 권장한다
### 적용 순서
**1. Keycloak Operator CRD / controller 적용**
```bash
kubectl apply -k k8s/overlays/dev/platform/keycloak-operator
kubectl -n mnt rollout status deploy/keycloak-operator --timeout=180s
```
**2. 기존 수제 Keycloak 리소스 정리**
기존 `Deployment` 기반 Keycloak 과 Operator 기반 Keycloak 이 같은 `Service/keycloak` 이름을 쓰므로, 전환 시 기존 수제 리소스를 정리한다.
```bash
kubectl -n mnt delete deployment/keycloak service/keycloak configmap/keycloak-config serviceaccount/keycloak-sa --ignore-not-found
```
**3. dev overlay 적용**
`k8s/overlays/dev/keycloak/` 는 이제 `Keycloak` CR, VSO secret 변환, Ingress, NetworkPolicy 를 포함한다.
```bash
kubectl apply -k k8s/overlays/dev
kubectl -n mnt get keycloak keycloak
kubectl -n mnt get pods -l app.kubernetes.io/instance=keycloak
```
**4. RealmImport 적용**
```bash
kubectl apply -k k8s/overlays/dev/keycloak-realm
kubectl -n mnt get keycloakrealmimport platform-realm
```
### client secret 처리
> **주의**: `auth-server-ingress` 같은 confidential client 의 secret 값 자체는 Git 에 넣지 않는다. realm/client shape 는 Git 에 두고, secret 값은 생성 후 Vault path `secret/oauth2-proxy/forward-auth` 로 넣어 oauth2-proxy 가 소비하게 한다 (§7).
---
## 7. Vault 시크릿 관리
> **예상 소요**: 회당 1~2 분 (port-forward + put).
### 애플리케이션 시크릿 저장 — 자동화됨
5 개 시크릿은 `bin/bootstrap.sh` Phase 6 에서 자동으로 seed 된다. 또는 단독 실행:
```bash
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-seed-apps.sh
```
동작:
- **이미 있는 경로는 skip** — 운영자가 회전한 값을 덮어쓰지 않음
- **대화형 입력** (TTY) — `read -r -s` 로 무음 입력 + 확인 재입력. bash history 에 남지 않음
- **비대화 + env 지정** — 해당 env var 를 사용 (`HISTFILE=/dev/null` 접두 권장)
- **비대화 + env 없음 + `AUTO_GENERATE=yes`**`openssl rand` 로 랜덤 24 자 생성
> **참고**: Vault CLI 의 `vault kv put <path> -` 모드로 **JSON stdin 전달** 이라 비밀번호가 argv / process table 어디에도 노출되지 않는다.
### 시크릿 경로 및 키
| 경로 | 키 | 용도 |
|---|---|---|
| `secret/identity-postgres/superuser` | `username` (기본 postgres), `password` | Postgres 슈퍼유저 (`POSTGRES_USER_FILE` / `POSTGRES_PASSWORD_FILE`) |
| `secret/keycloak/db` | `password` | Keycloak 의 DB 비밀번호 + initdb 가 생성하는 keycloak DB role |
| `secret/auth-server/db` | `SPRING_DATASOURCE_USERNAME` (기본 auth_server), `SPRING_DATASOURCE_PASSWORD` | Spring Boot configtree + Flyway |
| `secret/keycloak/bootstrap-admin` | `KEYCLOAK_ADMIN` (기본 admin), `KEYCLOAK_ADMIN_PASSWORD` | Keycloak 초기 관리자 계정 |
| `secret/minio/tenant-env` | `config.env` (env-file 포맷 단일 키) | MinIO Operator Tenant 루트 자격증명 |
| `secret/oauth2-proxy/forward-auth` | `client_secret`, `cookie_secret` | dev overlay 의 oauth2-proxy confidential client / session cookie |
| `secret/docker-registry/basic-auth` | `username`, `password`, `users` (htpasswd 한 줄) | Traefik Middleware 가 외부 push 시 검증 |
### 값 조회
```bash
kubectl -n mnt port-forward svc/vault 8200:8200 &
export VAULT_ADDR=http://127.0.0.1:8200
vault login -method=userpass username=alice # userpass admin 권장 — root token 사용 중단
vault kv get secret/keycloak/bootstrap-admin
# 특정 field 만:
vault kv get -field=KEYCLOAK_ADMIN_PASSWORD secret/keycloak/bootstrap-admin
```
> **참고**: userpass admin 셋업은 [docs/security-hardening.md §2](docs/security-hardening.md#2-vault-운영자-토큰-관리) 참고.
### 값 변경 (비밀번호 교체)
```bash
# 새 값으로 덮어쓰기 (vault kv put) — seed-apps.sh 의 skip 로직 우회
vault kv put secret/keycloak/db password='<새-pw>'
# 60s ~ 1h 내 VSO 가 자동으로 K8s Secret 갱신. 즉시 반영 원하면:
kubectl -n mnt delete secret keycloak-db
# VSO 가 Vault KV 를 읽어 재생성
```
> **Tip**: 강제 즉시 반영의 다른 방법 — `kubectl -n mnt annotate vaultstaticsecret <name> refresh=$(date +%s) --overwrite`.
> **주의 — MinIO 비밀번호에 `"` 금지**: MinIO 의 `config.env` 는 env-file 포맷 (`export KEY="value"`) 이라 값에 `"` 가 들어가면 파싱 깨짐. `vault-seed-apps.sh` 가 프롬프트에서 거부하며 재입력 요구한다.
### root token 회전
운영 정책 / userpass admin 셋업 절차는 [docs/security-hardening.md §2](docs/security-hardening.md#2-vault-운영자-토큰-관리) 참고.
---
## 8. Docker Registry 사용법
> **예상 소요**: push/pull 회당 < 1 분 (이미지 크기 의존).
### Push
```bash
docker login registry.project.com
# username: <DOCKER_REGISTRY_PUSH_USERNAME, 기본 registry-push>
# password: <Vault 에 저장한 값>
docker tag my-app:0.1.0 registry.project.com/my-app:0.1.0
docker push registry.project.com/my-app:0.1.0
```
외부 push 는 `registry.project.com` Ingress 로 들어오며 Traefik BasicAuth 를 통과해야 한다. BasicAuth 의 htpasswd `users` 값은 Vault path `secret/docker-registry/basic-auth` 에 저장되고 VSO 가 `docker-registry-basic-auth` Secret 으로 동기화한다.
> **주의**: 실제 워크로드 이미지는 `registry.project.com/...` 주소를 사용한다. image pull 은 Pod 내부가 아니라 노드의 kubelet/containerd 가 수행하므로, `docker-registry.mnt.svc.cluster.local` 같은 ClusterIP DNS 를 `image:` 에 쓰는 방식은 피한다.
### 내부 Service 직접 접근 (debugging)
내부 Service 는 registry Pod 자체 확인이나 클러스터 내부 HTTP 접근이 필요할 때만 사용한다.
```bash
docker tag my-app:0.1.0 docker-registry.mnt.svc.cluster.local:5000/my-app:0.1.0
docker push docker-registry.mnt.svc.cluster.local:5000/my-app:0.1.0
```
### Pull (Pod)
```yaml
apiVersion: apps/v1
kind: Deployment
spec:
template:
spec:
serviceAccountName: auth-server-sa
containers:
- name: my-app
image: registry.project.com/my-app:0.1.0
```
`auth-server-sa` / `test-server-*-sa` 는 dev overlay 에서 `docker-registry-pull-credentials``imagePullSecrets` 로 참조한다.
### 완료 확인
```bash
curl -fsS -u <push-user>:<push-pw> https://registry.project.com/v2/my-app/tags/list
# 정상 응답: {"name":"my-app","tags":["0.1.0"]}
kubectl -n mnt get secret docker-registry-pull-credentials -o jsonpath='{.type}{"\n"}'
# 정상 응답: kubernetes.io/dockerconfigjson
```
> **참고**: dev 환경에서 외부 DNS / TLS 가 아직 준비 전이면 노드의 containerd 에 이미지를 직접 import 하는 임시 우회가 필요할 수 있다. 정상 운영 (DNS + cert-manager 인증서 발급 완료) 에서는 위 push/pull 만으로 충분하다.
---
## 9. 앱에서 시크릿 사용하기
### envFrom (권장)
```yaml
spec:
containers:
- name: app
envFrom:
- secretRef:
name: auth-server-db # VSO 가 dev overlay 에서 합성
```
### 개별 key
```yaml
env:
- name: POSTGRES_PASSWORD
valueFrom:
secretKeyRef:
name: identity-postgres-superuser
key: password
```
### volume mount
```yaml
volumes:
- name: db-creds
secret:
secretName: auth-server-db
containers:
- volumeMounts:
- name: db-creds
mountPath: /etc/secrets
readOnly: true
```
---
## 10. 마이그레이션 실행 (Flyway)
> **예상 소요**: 30 초~3 분 (마이그레이션 갯수 의존).
base 에 정의된 `migration-flyway` Job 은 기본적으로는 배포되지 않는다. dev overlay 가 ArgoCD PreSync / sync-wave=-1 annotation 을 patch 하므로, GitOps 로 배포할 때는 ArgoCD 가 앱보다 먼저 Job 을 실행한다.
> **주의**: `kubectl apply -k` 로 수동 배포 시에는 Job 이 **앱과 동시에** 생성되므로 race 가능. 아래 순서대로 실행한다.
```bash
# migration 먼저
kubectl apply -k k8s/overlays/dev/auth/ -l app.kubernetes.io/component=migration
# 완료 대기
kubectl -n mnt wait --for=condition=complete job/migration-flyway --timeout=300s
# 앱 배포
kubectl apply -k k8s/overlays/dev/
```
ArgoCD 를 쓰면 이 순서가 sync-wave 로 자동화된다.
### 재실행
Flyway Job 은 `backoffLimit: 0` 으로 한 번만 실행된다. 재실행하려면:
```bash
kubectl -n mnt delete job migration-flyway
kubectl apply -k k8s/overlays/dev/auth/
```
---
## 11. Vault UI 접근
> **참고**: `service-ui` NodePort 는 보안상 제거되었다. 관리자는 port-forward 로만 접근한다.
```bash
kubectl -n mnt port-forward svc/vault 8200:8200
# 브라우저에서 http://127.0.0.1:8200/ui
# Token 입력: $(jq -r .root_token vault-init-keys.json)
```
> **권장**: root token 은 최초 설정 / 비상 복구 외에는 사용하지 않는다. 평시 접근은 개인별 userpass / OIDC auth 로 분리한다 — [docs/security-hardening.md §2](docs/security-hardening.md#2-vault-운영자-토큰-관리) 참고.
---
## 12. 환경별 배포
현재 `dev` 만 구성되어 있다.
```bash
bash k8s/scripts/bin/bootstrap.sh dev
```
> **참고**: staging / prod overlay 는 비어 있으며, 추후 다음 요소를 추가한다 — Vault storage `file``raft` 전환, Postgres backup CronJob, cert-manager ClusterIssuer + Certificate, 환경별 hostname (Keycloak / 공개 Ingress), `persistentVolumeClaimRetentionPolicy` 를 prod 는 `Retain` 유지 (dev 는 overlay 에서 `Delete` 로 patch).
환경별 차등표는 [docs/operations.md](docs/operations.md#환경별-배포) 참고.
---
## 13. 검증 / 린트 / 스키마 체크
> **예상 소요**: 1~3 분 (kubeconform 원격 스키마 조회).
```bash
bash k8s/scripts/ci/validate.sh
```
출력:
```
k8s/overlays/dev build=ok schema=ok lint=ok
k8s/overlays/dev/vso build=ok schema=ok lint=ok
모든 overlay 통과
```
- **build**: `kustomize build` (환경 중립성 / patch / labels)
- **schema**: `kubeconform -strict -ignore-missing-schemas` (K8s OpenAPI + Datree CRD catalog 원격 조회)
- **lint**: `kube-linter lint --config .kube-linter.yaml` (securityContext / 리소스 요구사항 / PSS / image tag 등)
> **참고**: `kustomization.yaml` 이 없는 overlay (`staging`, `prod`) 는 자동 스킵된다.
CI 파이프라인에서 이 스크립트를 PR 게이트로 사용한다. 실패 시 `build=fail|schema=fail|lint=fail` 로 표기되고 상세 에러가 stderr 에 출력된다.
---
## 14. 정리 / 롤백 (teardown)
> **예상 소요**: 2~5 분 (PVC 보호 finalizer 정리 + namespace 종료).
```bash
# 대화형 (y/N 확인)
bash k8s/scripts/bin/teardown.sh dev
# 비대화 (CI)
CONFIRM=yes bash k8s/scripts/bin/teardown.sh dev
# MinIO Operator 까지 제거 (기본은 유지)
TEARDOWN_MINIO_OPERATOR=yes bash k8s/scripts/bin/teardown.sh dev
```
체계적 7 단계:
| 단계 | 내용 |
|:---:|---|
| 1 | Precheck — namespace 존재 여부 + phase 확인. 일부 단계는 없으면 skip |
| 2 | VSO CRD 삭제 — `kubectl delete -k overlays/<env>/vso/` 60s timeout. 타임아웃 시 `VaultStaticSecret / VaultAuth / VaultConnection` finalizer 강제 해제 |
| 3 | VSO Helm uninstall — `helm uninstall --wait 5m` (Operator 제거) |
| 4 | 인프라 overlay 삭제 — `kubectl delete -k overlays/<env>/` 120s timeout |
| 5 | namespace 잔존 리소스 finalizer 정리 — PVC 보호 finalizer + VSO CRD + 전체 namespaced 리소스 일괄 finalizer 제거 |
| 6 | namespace 삭제 + Terminating 감지 — `kubectl delete namespace` 60s 대기 → 실패 시 `/finalize` API 호출로 강제 종료 |
| 7 | Cluster-scoped 정리 — `vault-tokenreview-binding` ClusterRoleBinding 제거. `TEARDOWN_MINIO_OPERATOR=yes` 면 MinIO Operator 도 함께 |
> **참고**: controller 없이 남은 CRD finalizer, PVC 보호 finalizer, 전체 namespaced 리소스 finalizer 를 단계별로 선제 해제해서 namespace 가 Terminating 에 걸리지 않도록 처리. 이미 Terminating 에 걸려 있어도 단계 6 에서 `/finalize` API 직접 호출로 강제 종료.
> **주의**: teardown 후에도 `vault-init-keys.json` 은 보존된다. 완전 초기화하려면 수동으로 삭제한다.
### 강제 종료의 부작용
> **주의**: 단계 6 의 `/finalize` 는 orphan 리소스 (PV / PVC 바인딩) 를 남길 수 있다.
```bash
# teardown 후 orphan PV 검사
kubectl get pv | grep -E 'Released|Failed'
# 필요시 수동 삭제
kubectl delete pv <name>
```
---
## 15. 트러블슈팅
> **참고**: 보안 정책 / 거버넌스 (etcd 암호화 / Vault root token / bash history) 는 [docs/security-hardening.md](docs/security-hardening.md) 참고.
### 15.1 Registry Pod 가 계속 `ContainerCreating`
Secret `docker-registry-minio` 또는 `docker-registry-basic-auth` 가 아직 생성되지 않은 상태일 수 있다. VSO 가 Vault KV 를 읽어서 만든다.
```bash
kubectl -n mnt describe vaultstaticsecret docker-registry-minio
kubectl -n mnt describe vaultstaticsecret docker-registry-basic-auth
kubectl -n mnt logs -l app.kubernetes.io/name=vault-secrets-operator --tail=100
```
자주 보는 에러:
- `permission denied` → Vault policy 또는 role 설정 오류. `tasks/vault-init.sh` 재실행.
- `no matching vault path` → Vault KV 에 값이 저장되지 않음. `tasks/vault-seed-apps.sh` 재실행.
### 15.2 VSO 가 Vault 에 로그인 실패
```bash
kubectl -n mnt logs -l app.kubernetes.io/name=vault-secrets-operator --tail=200 | grep -i error
```
체크 항목:
- Vault ClusterRoleBinding `vault-tokenreview-binding` 존재? `kubectl get clusterrolebinding vault-tokenreview-binding`
- Vault ServiceAccount 에 token 자동 마운트 되어 있음? (기본값 true)
- `vault auth/kubernetes/config``kubernetes_ca_cert` + `token_reviewer_jwt` 설정됨? → 없으면 `tasks/vault-init.sh` 재실행
- VaultConnection address 가 `http://vault:8200` 이고 같은 namespace 에 실제 `vault` Service 존재?
### 15.3 `vault operator init` 실패 — 이미 초기화됨
> **참고**: 정상. `tasks/vault-init.sh` 는 idempotent 하게 이 경우를 스킵하고 unseal 만 다시 수행한다. `kubectl exec vault-0 -- vault status` 로 상태 확인.
### 15.4 부트스트랩 중단 → 재시작
```bash
bash k8s/scripts/bin/bootstrap.sh dev
```
각 단계가 idempotent 이므로 그대로 다시 실행해도 된다. 이미 완료된 단계는 스킵된다.
> **참고**: Phase 6 의 시크릿 입력 시, Vault KV 에 이미 있으면 스킵되므로 비밀번호는 사용되지 않음.
### 15.5 Vault UI 가 안 열림
NodePort 는 제거되었다. port-forward 를 사용한다:
```bash
kubectl -n mnt port-forward svc/vault 8200:8200
```
`http://127.0.0.1:8200/ui` 로 접근.
### 15.6 NetworkPolicy 로 트래픽 차단 의심
```bash
# 모든 NetworkPolicy 확인
kubectl -n mnt get networkpolicy
# 특정 Pod 에 적용된 정책 확인
kubectl -n mnt describe pod <pod-name> | grep -A3 Labels
kubectl -n mnt get networkpolicy -o yaml | grep -A2 podSelector
```
임시 허용 (디버깅):
```bash
kubectl -n mnt delete networkpolicy default-deny-all
```
> **주의**: 진단 완료 후 반드시 복구 — `kubectl apply -k k8s/overlays/dev/`.
### 15.7 PodSecurity 위반 경고
`kubectl apply``Warning: would violate PodSecurity "restricted:latest": ...` 메시지가 뜨면 **어떤 Pod 의 어떤 필드** 가 위반인지 확인:
```bash
# 최근 이벤트
kubectl -n mnt get events --sort-by='.lastTimestamp' \
| grep -i 'podsecurity\|FailedCreate'
# 경고 메시지는 apply 시 stderr 로도 나옴
kubectl apply -k k8s/overlays/dev/ 2>&1 | grep -i warning
```
자주 걸리는 항목 체크리스트:
- `runAsNonRoot: true` 누락 또는 `runAsUser: 0`
- `allowPrivilegeEscalation: false` 누락
- `capabilities.drop: [ALL]` 누락
- `seccompProfile.type: RuntimeDefault` 누락
- `readOnlyRootFilesystem: true` 누락 (선택이지만 권장)
- hostPath / hostNetwork / hostPID / hostIPC 사용
- hostPorts 사용
> **참고**: base 의 모든 워크로드는 이미 Restricted 통과. 경고가 뜨는 건 보통 다음 두 가지 — VSO Operator Helm chart, MinIO Operator Helm chart. 둘 다 자기 namespace 에서 돌아가므로 `mnt` 의 PSS 와 무관. `mnt` 안의 Pod 에서 경고가 나면 매니페스트를 수정해야 함.
### 15.8 기존 K8s Secret 이 남아있을 때
VSO 는 `destination.overwrite: false` 기본값이라 **이미 존재하는 Secret 을 덮어쓰지 않는다**. Vault KV 에 새 값을 넣어도 K8s Secret 은 옛날 값을 유지.
확인:
```bash
kubectl -n mnt get secret -l 'kubernetes.io/managed-by!=Helm' \
-o custom-columns=NAME:.metadata.name,AGE:.metadata.creationTimestamp
```
해결 1 — 개별 삭제 후 VSO 재생성:
```bash
kubectl -n mnt delete secret docker-registry-minio docker-registry-basic-auth
# VSO 가 1-2 분 내 Vault KV 에서 읽어 재생성
kubectl -n mnt get vaultstaticsecret
```
해결 2 — bootstrap 재실행 시 자동 정리:
```bash
RESET_STALE_SECRETS=yes bash k8s/scripts/bin/bootstrap.sh dev
# Phase 2 에서 VSO-managed Secret 7 개 전부 삭제 → Phase 7/8 에서 VSO 재생성
```
> **주의**: 이 옵션은 **destructive**. 운영자가 명시적으로 지정했을 때만 동작.
### 15.9 namespace 가 Terminating 에 걸림
`mnt` namespace 가 `Terminating` 에서 오래 멈추면 보통 다음 셋 중 하나다.
- controller 가 이미 사라졌는데 CRD finalizer 가 남아 있음
- PVC protection finalizer 가 남아 있음
- namespaced 리소스 일부가 finalizer 때문에 삭제 완료를 못 함
> **참고**: 현재 `teardown.sh` 는 이 상황을 고려해 단계적으로 정리한다 — `VaultStaticSecret / VaultAuth / VaultConnection` finalizer 제거 → PVC 보호 finalizer 제거 → 남은 namespaced 리소스 finalizer 일괄 제거 → 마지막에 namespace `/finalize` 호출.
수동 확인:
```bash
kubectl get namespace mnt -o yaml
kubectl api-resources --verbs=list --namespaced -o name | xargs -n 1 kubectl -n mnt get --ignore-not-found
```
이미 teardown 을 사용 중이라면 대부분은 스크립트가 자동 처리한다. 수동 개입은 정말 스크립트가 실패했을 때만 한다.
### 15.10 vault-0 이 `0/1 Running` 에서 멈춤
**현상**:
```
NAME READY STATUS RESTARTS AGE
vault-0 0/1 Running 0 2m
```
계속 `0/1 Running`. `kubectl wait --for=condition=Ready` 가 timeout 으로 실패.
**원인 — 의도된 동작**:
Vault 의 readiness probe 는 `/v1/sys/health?sealedcode=503&uninitcode=503` 를 사용한다. 즉:
- **uninitialized** → HTTP 503 → readiness fail
- **sealed** → HTTP 503 → readiness fail
- **initialized + unsealed** → HTTP 200 → Ready
이건 sealed Vault 가 Service Endpoints 에서 제외되어 트래픽이 흘러가지 않도록 하는 **보안 설계**. 초기화 전에는 구조상 Ready 가 될 수 없다.
**해결 — `vault-init.sh` 실행**:
```bash
# Pod 이 Running 이면 exec 가능 → 초기화 실행 가능
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-init.sh
```
수행되는 것:
1. `vault operator init` — unseal keys + root token 생성
2. unseal 5 shares 중 3 개로 자동 unseal
3. root login → KV v2 + k8s auth + policy × 2 + role × 2
`vault-init.sh` 가 끝나고 몇 초 뒤 Pod 이 자동으로 Ready 로 전환:
```bash
kubectl -n mnt get pod vault-0
# vault-0 1/1 Running
```
**bootstrap.sh 가 Phase 4 에서 Ready 대기로 실패했을 때 — 재개**:
```bash
# Phase 4 까지는 apply + Pod Running 완료 상태
# 남은 Phase 5~8 만 수동 실행
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-init.sh # Phase 5
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vault-seed-apps.sh # Phase 6
REPO_ROOT="$(pwd)" bash k8s/scripts/tasks/vso-install.sh # Phase 7
kubectl apply -k k8s/overlays/dev/vso/ # Phase 8
```
또는 bootstrap.sh 를 그냥 다시 실행해도 된다 (idempotent).
> **참고 — 다른 Pod 들이 `ContainerCreating` 상태**: `auth-server`, `keycloak`, `identity-postgres`, `docker-registry`, `migration-flyway``ContainerCreating` 에 머무는 건 **VSO 가 만드는 K8s Secret 이 아직 없어서** volume mount 가 대기 중인 것. Vault 초기화 + 앱 secret 주입 (§7) + VSO sync 가 끝나면 차례로 Running 으로 전환된다. 정상 동작.
> **참고 — `test-server-*` 가 `ImagePullBackOff`**: `registry.example.com/test-platform/test-server-*:0.1.0`**예시 이미지** 로, 실제 레지스트리에 존재하지 않는다. 사용자가 실제 이미지를 빌드해서 내부 Registry 에 푸시해야 한다. 무시해도 된다.
### 15.11 `helm upgrade``has no deployed releases` 로 실패
**현상** — bootstrap Phase 7 (VSO Helm) 에서:
```
Error: UPGRADE FAILED: "vault-secrets-operator" has no deployed releases
```
**원인**:
이전 `helm upgrade --install` 시도가 `--atomic` 때문에 rollback 되며 릴리즈가 `failed` 또는 `uninstalled` 상태로 남음. Helm 이 metadata 는 보존하는데 실제 배포물은 없는 상태. 이 상태에선 `upgrade --install`**upgrade 로 분기하려다 "deployed release 없음" 으로 실패**.
**해결**:
> **참고**: `tasks/vso-install.sh` 는 이제 실행 시 릴리즈 상태를 먼저 검사해서 `failed`/`pending*`/`uninstalling`/`uninstalled` 면 자동으로 `helm uninstall` 을 먼저 수행한다. 또한 `--atomic` 플래그를 제거했다 (실패 시 재실행으로 복구가 더 안전).
구버전 스크립트로 이미 이 상태에 빠졌다면 수동 정리:
```bash
helm -n mnt uninstall vault-secrets-operator
# (Error: uninstall: Release not loaded: ... 이 떠도 무시)
bash k8s/scripts/bin/bootstrap.sh dev
# Phase 7 부터 깔끔하게 재개됨
```
+26
View File
@@ -0,0 +1,26 @@
# k8s AGENTS
Role:
- own Kubernetes/K3s source-of-truth manifests and Kustomize composition
- keep base resources, environment overlays, helper scripts, and Vault Secrets Operator assets separate
- make render / validate / diff / apply possible from Git without relying on live cluster state
Scope:
- `base/`: environment-neutral bases
- `overlays/<env>/`: environment-specific composition
- `scripts/`: helper automation
- `vso/`: Vault Secrets Operator assets
Read first:
- `/docs/standards/infra/architecture-environments.md`
- `/docs/standards/infra/kustomize.md`
- `/docs/standards/infra/k3s-specific.md`
- `/docs/standards/infra/operations-runbook-upgrade-rollback.md`
Rules:
- prefer `kubectl kustomize`, `kubectl diff -k`, and `kubectl apply -k`
- do not use server-local manifests as source of truth
- do not place environment differences in `base/`
- do not place base resource definitions directly in overlays unless the resource is environment-only by design
- do not commit production secret values
- keep scripts as helpers; manifests remain declarative source
+58
View File
@@ -0,0 +1,58 @@
# Kubernetes Packaging
This tree is organized for many independently owned workloads.
Entry points:
- `overlays/<env>/` renders the whole environment skeleton.
- `overlays/<env>/<domain>/` renders one namespace/domain boundary.
- `overlays/<env>/managing/vault-secrets-operator/` is applied after the VSO CRDs are installed.
- `scripts/env/` contains environment bootstrap and teardown orchestration.
- `scripts/` contains imperative bootstrap and break-glass operations only.
- `vso/` contains Vault Secrets Operator installation lifecycle files.
Scale rules:
- Do not place hundreds of workloads directly under one package.
- Add workloads under `base/app/units/<unit>/<domain>/<workload-kind>/<workload>/`.
- Add matching environment overrides under `overlays/<env>/app/units/<unit>/<domain>/<workload-kind>/<workload>/` only when that workload has environment-specific differences.
- A workload package owns its Kubernetes object files together, such as `deployment.yaml`, `service.yaml`, `configmap.yaml`, `cronjob.yaml`, `statefulset.yaml`, or `pvc.yaml`.
- Do not create kind-based package roots such as `deployments/`, `services/`, or `configmaps/`.
Example for a 1000-workload organization:
```text
base/app/units/
├── commerce/
│ ├── checkout/
│ │ ├── services/order-api/
│ │ ├── services/payment-api/
│ │ ├── workers/payment-settlement-worker/
│ │ ├── schedulers/cart-expiry-scheduler/
│ │ └── stateful/orders-postgres/
│ └── catalog/
│ ├── services/catalog-api/
│ ├── workers/search-index-worker/
│ └── jobs/catalog-backfill-job/
├── identity/
│ ├── auth/
│ │ ├── services/auth-api/
│ │ ├── services/token-api/
│ │ └── schedulers/token-cleanup-scheduler/
│ └── profile/
│ ├── services/profile-api/
│ └── workers/profile-event-worker/
├── media/
│ ├── playback/
│ │ ├── services/playback-api/
│ │ └── workers/session-event-worker/
│ └── recommendation/
│ ├── services/recommendation-api/
│ ├── workers/model-feature-worker/
│ └── jobs/model-refresh-job/
└── data/
├── ingestion/
│ ├── workers/event-ingest-worker/
│ └── stateful/ingest-kafka/
└── analytics/
├── schedulers/daily-report-scheduler/
└── jobs/monthly-rollup-job/
```
+38
View File
@@ -0,0 +1,38 @@
# k8s/base AGENTS
Role:
- own environment-neutral Kustomize base resources
- define reusable workload, namespace, service, storage, policy, and platform/plugin shapes
- keep environment-specific values out of base
Scope:
- `app/`: application-facing base units
- `managing/`: management and operational base units
- `plugins/`: platform/plugin base resources
Allowed:
- shared labels/selectors
- common workload shape
- common probe/resource shape
- common service/storage/policy shape
- unit composition through nested `kustomization.yaml`
Forbidden:
- environment-specific hostnames
- production-only replicas/resources
- environment-specific secret values
- direct references to a specific cluster context
- overlay-only patches masquerading as base manifests
Read first:
- `/docs/standards/infra/kustomize.md`
- `/docs/standards/infra/architecture-environments.md`
- `/docs/standards/infra/workload-selection.md`
- `/docs/standards/infra/config-and-secrets.md`
- `/docs/standards/infra/security-hardening.md`
Rules:
- base must be reusable by dev, staging, and prod overlays
- base may define default shape, but overlays own environment differences
- unit ownership should be visible in path names
- large fleets should stay navigable by role first, then domain/unit
+13
View File
@@ -0,0 +1,13 @@
# Base Packages
Base packages contain shared Kubernetes definitions.
Boundaries:
- `managing/` owns infrastructure management resources in the `mnt` namespace.
- `app/` owns application namespace resources and large-scale workload package contracts.
- `plugins/` owns plugin namespace resources.
Rules:
- Base packages must not contain environment-specific values.
- A base package can be rendered with `kubectl kustomize` through its own `kustomization.yaml`.
- Parent packages compose child packages; child packages own their internal Kubernetes object files.
+69
View File
@@ -0,0 +1,69 @@
# k8s/base/app AGENTS
Role:
- own application-facing infrastructure base units
- define environment-neutral app workload, service, config reference, secret reference, storage, network policy, probe, and resource shapes
- organize many services by domain/service path directly under `k8s/base/app/<domain>/`
Current structure:
- domains sit directly under `k8s/base/app/` (`identity/`, `storage/`, `test/`)
- inside each domain, separate by workload role (`stateful/`, `stateless/`)
- example: `identity/auth/stateful/identity-postgres/`, `identity/auth/stateless/auth-server/`
Allowed:
- Kustomize base edits for application units
- Deployment / StatefulSet / Job / CronJob base resources
- Service / NetworkPolicy / PDB / HPA base resources
- ConfigMap / Secret reference wiring without secret values
- probe / resource / PVC / StorageClass reference shape
- component-specific manifests aligned with standards
Forbidden:
- environment-specific values that belong in `k8s/overlays/<env>`
- editing packaged K3s component manifests directly
- treating server-local manifests as source of truth
- embedding production secret values in Git
- generating opaque YAML via script as the primary ownership path
- merging unrelated app rollout + migration + cluster upgrade into one hidden change
Read first:
- `/docs/standards/infra/architecture-environments.md`
- `/docs/standards/infra/kustomize.md`
- `/docs/standards/infra/config-and-secrets.md`
- `/docs/standards/infra/workload-selection.md`
- `/docs/standards/infra/storage-pvc.md`
- `/docs/standards/infra/network-ingress-tls.md`
- `/docs/standards/infra/resources-probes-availability.md`
- `/docs/standards/infra/security-hardening.md`
- `/docs/standards/infra/observability-health.md`
- `/docs/standards/infra/db-and-migration.md`
Component-specific routing:
- paths containing `keycloak` -> `/docs/standards/infra/keycloak.md`
- paths containing `vault` -> `/docs/standards/infra/vault.md`
- paths containing `minio` -> `/docs/standards/infra/minio.md`
- paths containing `flyway` or migration jobs -> `/docs/standards/infra/flyway.md`
Examples:
- `/docs/examples/infra/kustomize.md`
- `/docs/examples/infra/config-and-secrets.md`
- `/docs/examples/infra/workload-selection.md`
- `/docs/examples/infra/storage-pvc.md`
- `/docs/examples/infra/network-ingress-tls.md`
- `/docs/examples/infra/resources-probes-availability.md`
- `/docs/examples/infra/security-hardening.md`
- `/docs/examples/infra/observability-health.md`
- `/docs/examples/infra/db-and-migration.md`
- `/docs/examples/infra/keycloak.md`
- `/docs/examples/infra/vault.md`
- `/docs/examples/infra/minio.md`
- `/docs/examples/infra/flyway.md`
Rules:
- base must stay environment-neutral
- app services default to `ClusterIP`
- public exposure must be explicit and added through overlays/ingress policy
- health/metrics/admin endpoints stay non-public by default
- DB migration must stay separate from app startup
- stateful workloads must have explicit storage and restore reasoning
- domain/unit nesting should make ownership clear for large service counts
+23
View File
@@ -0,0 +1,23 @@
# Application Base
This package owns the `app` namespace and the contract for application workloads.
For large scale, workloads are grouped by unit, domain, workload kind, and workload:
```text
base/app/units/<unit>/<domain>/<workload-kind>/<workload>/
```
Examples:
- `base/app/units/commerce/checkout/services/order-api/`
- `base/app/units/commerce/checkout/workers/payment-settlement-worker/`
- `base/app/units/identity/auth/schedulers/token-cleanup-scheduler/`
- `base/app/units/data/storage/stateful/orders-postgres/`
Rules:
- The unit folder is the broad ownership boundary for a business unit, platform area, or organization.
- The domain folder is the bounded context inside a unit.
- The workload-kind folder groups similar operating models inside one domain.
- The workload folder is the smallest independently deployable package.
- Kubernetes object files stay together inside the workload package.
- Shared namespace-level objects go in `shared/`.
@@ -0,0 +1,30 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: identity-postgres-initdb
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
data:
01-create-databases.sh: |
#!/usr/bin/env bash
set -euo pipefail
KEYCLOAK_DB_PASSWORD="$(cat /run/secrets/keycloak-db/password)"
AUTH_SERVER_DB_PASSWORD="$(cat /run/secrets/auth-server-db/SPRING_DATASOURCE_PASSWORD)"
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-'SQL'
CREATE ROLE keycloak LOGIN;
CREATE DATABASE keycloak OWNER keycloak;
CREATE ROLE auth_server LOGIN;
CREATE DATABASE auth_server OWNER auth_server;
SQL
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-c "ALTER ROLE keycloak PASSWORD '$(printf '%s' "$KEYCLOAK_DB_PASSWORD" | sed "s/'/''/g")';"
psql --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
-c "ALTER ROLE auth_server PASSWORD '$(printf '%s' "$AUTH_SERVER_DB_PASSWORD" | sed "s/'/''/g")';"
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- configmap-initdb.yaml
- service-headless.yaml
- service.yaml
- statefulset.yaml
labels:
- pairs:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
name: identity-postgres-headless
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
clusterIP: None
selector:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
ports:
- name: postgres
port: 5432
targetPort: postgres
protocol: TCP
appProtocol: postgresql
@@ -0,0 +1,22 @@
apiVersion: v1
kind: Service
metadata:
name: identity-postgres
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
ports:
- name: postgres
port: 5432
targetPort: postgres
protocol: TCP
appProtocol: postgresql
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: identity-postgres-sa
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
automountServiceAccountToken: false
@@ -0,0 +1,174 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: identity-postgres
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
replicas: 1
serviceName: identity-postgres-headless
podManagementPolicy: OrderedReady
updateStrategy:
type: RollingUpdate
persistentVolumeClaimRetentionPolicy:
whenDeleted: Retain
whenScaled: Retain
selector:
matchLabels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
template:
metadata:
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
serviceAccountName: identity-postgres-sa
automountServiceAccountToken: false
terminationGracePeriodSeconds: 60
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
fsGroup: 999
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: postgres
image: postgres:16.4
imagePullPolicy: IfNotPresent
ports:
- name: postgres
containerPort: 5432
protocol: TCP
env:
- name: POSTGRES_DB
value: postgres
- name: POSTGRES_USER_FILE
value: /run/secrets/superuser/username
- name: POSTGRES_PASSWORD_FILE
value: /run/secrets/superuser/password
- name: PGDATA
value: /var/lib/postgresql/data/pgdata
- name: KEYCLOAK_DB_PASSWORD_FILE
value: /run/secrets/keycloak-db/password
- name: AUTH_SERVER_DB_PASSWORD_FILE
value: /run/secrets/auth-server-db/SPRING_DATASOURCE_PASSWORD
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "1"
memory: 1Gi
startupProbe:
exec:
command:
- sh
- -c
- pg_isready -U "$(cat "$POSTGRES_USER_FILE")"
periodSeconds: 5
failureThreshold: 60
timeoutSeconds: 3
readinessProbe:
exec:
command:
- sh
- -c
- pg_isready -U "$(cat "$POSTGRES_USER_FILE")"
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 3
livenessProbe:
exec:
command:
- sh
- -c
- pg_isready -U "$(cat "$POSTGRES_USER_FILE")"
periodSeconds: 30
failureThreshold: 3
timeoutSeconds: 3
securityContext:
runAsNonRoot: true
runAsUser: 999
runAsGroup: 999
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: data
mountPath: /var/lib/postgresql/data
- name: initdb
mountPath: /docker-entrypoint-initdb.d
readOnly: true
- name: superuser
mountPath: /run/secrets/superuser
readOnly: true
- name: keycloak-db
mountPath: /run/secrets/keycloak-db
readOnly: true
- name: auth-server-db
mountPath: /run/secrets/auth-server-db
readOnly: true
- name: tmp
mountPath: /tmp
- name: run
mountPath: /var/run/postgresql
volumes:
- name: initdb
configMap:
name: identity-postgres-initdb
defaultMode: 0755
- name: superuser
secret:
secretName: identity-postgres-superuser
defaultMode: 0400
- name: keycloak-db
secret:
secretName: keycloak-db
defaultMode: 0400
- name: auth-server-db
secret:
secretName: auth-server-db
defaultMode: 0400
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 128Mi
- name: run
emptyDir:
medium: Memory
sizeLimit: 64Mi
volumeClaimTemplates:
- metadata:
name: data
labels:
app.kubernetes.io/name: identity-postgres
app.kubernetes.io/instance: identity-postgres
app.kubernetes.io/version: "16.4"
app.kubernetes.io/component: database
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
accessModes:
- ReadWriteOnce
storageClassName: local-path
volumeMode: Filesystem
resources:
requests:
storage: 5Gi
@@ -0,0 +1,19 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: auth-server-config
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
data:
SPRING_PROFILES_ACTIVE: dev
SPRING_DATASOURCE_URL: jdbc:postgresql://identity-postgres:5432/auth_server
APP_DATASOURCE_URL: jdbc:postgresql://identity-postgres:5432/auth_server
MANAGEMENT_SERVER_ADDRESS: 0.0.0.0
MANAGEMENT_SERVER_PORT: "8081"
MANAGEMENT_ENDPOINTS_WEB_EXPOSURE_INCLUDE: health,prometheus
SERVER_PORT: "8080"
@@ -0,0 +1,128 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: auth-server
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
replicas: 1
revisionHistoryLimit: 5
progressDeadlineSeconds: 600
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
template:
metadata:
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
serviceAccountName: auth-server-sa
automountServiceAccountToken: false
terminationGracePeriodSeconds: 45
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: auth-server
image: registry.example.com/auth-platform/auth-server:0.1.0
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 8080
protocol: TCP
- name: metrics
containerPort: 8081
protocol: TCP
envFrom:
- configMapRef:
name: auth-server-config
- secretRef:
name: auth-server-db
env:
- name: JAVA_TOOL_OPTIONS
value: "-XX:MaxRAMPercentage=75 -XX:+ExitOnOutOfMemoryError"
- name: SPRING_CONFIG_IMPORT
value: "optional:configtree:/etc/secrets/"
resources:
requests:
cpu: 250m
memory: 512Mi
limits:
memory: 768Mi
startupProbe:
httpGet:
path: /actuator/health/liveness
port: metrics
periodSeconds: 5
failureThreshold: 24
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /actuator/health/readiness
port: metrics
periodSeconds: 5
failureThreshold: 3
timeoutSeconds: 2
livenessProbe:
httpGet:
path: /actuator/health/liveness
port: metrics
periodSeconds: 15
failureThreshold: 3
timeoutSeconds: 3
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: db-creds
mountPath: /etc/secrets
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: db-creds
secret:
secretName: auth-server-db
defaultMode: 0400
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 128Mi
@@ -0,0 +1,17 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- configmap.yaml
- deployment.yaml
- service.yaml
labels:
- pairs:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
@@ -0,0 +1,27 @@
apiVersion: v1
kind: Service
metadata:
name: auth-server
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
ports:
- name: http
port: 80
targetPort: http
protocol: TCP
appProtocol: http
- name: metrics
port: 8081
targetPort: metrics
protocol: TCP
appProtocol: http
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: auth-server-sa
labels:
app.kubernetes.io/name: auth-server
app.kubernetes.io/instance: auth-server
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: api
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
automountServiceAccountToken: false
@@ -0,0 +1,113 @@
apiVersion: k8s.keycloak.org/v2beta1
kind: Keycloak
metadata:
name: keycloak
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/version: "26.6.1"
app.kubernetes.io/component: identity-provider
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: keycloak-operator
spec:
instances: 2
db:
vendor: postgres
host: identity-postgres
port: 5432
database: keycloak
usernameSecret:
name: keycloak-db-operator
key: username
passwordSecret:
name: keycloak-db-operator
key: password
poolInitialSize: 5
poolMinSize: 5
poolMaxSize: 20
bootstrapAdmin:
user:
secret: keycloak-bootstrap-admin-operator # 어떤 시크릿에서 읽을지 지정하는 거
hostname:
strict: true # 명시된 hostname을 기준으로 URL을 만든다.
backchannelDynamic: false # 백URL도 프론트 hostname과 동일하게 고정한다.
http:
httpEnabled: true
httpPort: 8080
serviceHttpPort: 80
serviceName: keycloak
labels:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/component: identity-provider
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: keycloak-operator
proxy:
headers: xforwarded # X-Forwarded-For X-Forwarded-Proto X-Forwarded-Host X-Forwarded-Port 해당 헤더들을 사용하겠다는 의미
ingress:
enabled: false
networkPolicy:
enabled: false
additionalOptions:
- name: cache
value: ispn # 애플리케이션의 캐시 엔진으로 ispn으로 사용
- name: cache-stack
value: jdbc-ping # 클러스터링된 서버끼리 서로를 찾는(Discovery) 방식을 JDBC-PING으로 정한 것
- name: health-enabled
value: "true" # 상태 확인(Health Check) 엔드포인트를 활성화
- name: metrics-enabled
value: "true" # 메트릭(성능 지표) 수집 기능을 켬
- name: log-console-output
value: json # 로그 형식을 JSON 구조로 변경
env:
- name: JAVA_OPTS_APPEND
value: "-XX:MaxRAMPercentage=70 -XX:InitialRAMPercentage=50 -XX:+ExitOnOutOfMemoryError"
resources:
requests:
cpu: 500m
memory: 1Gi
limits:
cpu: "2"
memory: 2Gi
scheduling:
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/instance: keycloak
app.kubernetes.io/managed-by: keycloak-operator
affinity: # 특정 노드에 다른 pod에 관계에 따라 배치 위치를 결정하는 규칙을 정의한다
podAntiAffinity: # 특정 조건을 만족하는 포드와는 같은 장소에 있지 않는다.
preferredDuringSchedulingIgnoredDuringExecution: # 가급적 지켜줘
- weight: 100 # 가중치가 100임 매우 중요하다고 알림
podAffinityTerm: # 어떤 파드를 피해다닐지
topologyKey: kubernetes.io/hostname # 피할 기준은 호스트 네임으로 설정
labelSelector:
matchLabels:
app.kubernetes.io/instance: keycloak # keycloak과 피하겠다
app.kubernetes.io/managed-by: keycloak-operator # keycloak-operator에 의해 관리되는
unsupported:
podTemplate:
spec:
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: keycloak
securityContext:
allowPrivilegeEscalation: false
capabilities:
drop:
- ALL
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
seccompProfile:
type: RuntimeDefault
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- keycloak.yaml
labels:
- pairs:
app.kubernetes.io/name: keycloak
app.kubernetes.io/instance: keycloak
app.kubernetes.io/version: "26.6.1"
app.kubernetes.io/component: identity-provider
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
@@ -0,0 +1,14 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- tenant.yaml
labels:
- pairs:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio
app.kubernetes.io/version: "2025.01.20"
app.kubernetes.io/component: object-storage
app.kubernetes.io/part-of: storage-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
@@ -0,0 +1,92 @@
apiVersion: minio.min.io/v2
kind: Tenant
metadata:
name: minio
labels:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio
app.kubernetes.io/version: "2025.01.20"
app.kubernetes.io/component: object-storage
app.kubernetes.io/part-of: storage-platform
app.kubernetes.io/managed-by: kustomize
spec:
image: minio/minio:RELEASE.2025-01-20T14-49-07Z
imagePullPolicy: IfNotPresent
mountPath: /export
configuration:
name: minio-tenant-env
requestAutoCert: true
certConfig:
commonName: minio
organizationName:
- example.com
dnsNames:
- minio
- minio-hl
pools:
- name: pool-0
servers: 4
volumesPerServer: 1
volumeClaimTemplate:
metadata:
name: data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 20Gi
storageClassName: local-path
resources:
requests:
cpu: 250m
memory: 1Gi
limits:
cpu: "1"
memory: 2Gi
securityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containerSecurityContext:
runAsUser: 1000
runAsGroup: 1000
runAsNonRoot: true
allowPrivilegeEscalation: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
features:
bucketDNS: false
prometheusOperator: false
podManagementPolicy: Parallel
exposeServices:
minio: false
console: false
logging:
anonymous: false
json: true
quiet: false
---
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: minio-pdb
labels:
app.kubernetes.io/name: minio
app.kubernetes.io/instance: minio
app.kubernetes.io/version: "2025.01.20"
app.kubernetes.io/component: object-storage
app.kubernetes.io/part-of: storage-platform
app.kubernetes.io/managed-by: kustomize
spec:
maxUnavailable: 1
unhealthyPodEvictionPolicy: AlwaysAllow
selector:
matchLabels:
v1.min.io/tenant: minio
+46
View File
@@ -0,0 +1,46 @@
# k8s/base/managing AGENTS
Role:
- own management and operational Kubernetes base units
- model bootstrap, migration, backup, restore, maintenance, and admin workloads as declarative Kustomize bases
- keep operational workloads separate from long-running application serving workloads
Allowed:
- Job / CronJob base resources for operational tasks
- maintenance ServiceAccount / RBAC / ConfigMap / Secret reference wiring
- backup / restore / migration helper workload shapes
- admin-only service shapes when explicitly justified
Forbidden:
- long-running product application workloads
- environment-specific values that belong in `k8s/overlays/<env>`
- scripts becoming the primary source of YAML truth
- large heredoc-generated manifests as the default path
- embedding production secret values
- hiding environment differences in shell conditionals instead of overlays
- giant all-in-one jobs that mix unrelated concerns
Read first:
- `/docs/standards/infra/workload-selection.md`
- `/docs/standards/infra/db-and-migration.md`
- `/docs/standards/infra/flyway.md`
- `/docs/standards/infra/backup-restore.md`
- `/docs/standards/infra/operations-runbook-upgrade-rollback.md`
- `/docs/standards/infra/config-and-secrets.md`
- `/docs/standards/infra/security-hardening.md`
- `/docs/standards/infra/kustomize.md`
Examples:
- `/docs/examples/infra/flyway.md`
- `/docs/examples/infra/backup-restore.md`
- `/docs/examples/infra/operations-runbook-upgrade-rollback.md`
- `/docs/examples/infra/db-and-migration.md`
- `/docs/examples/infra/kustomize.md`
- `/docs/examples/infra/scripts.md`
Rules:
- management jobs are explicit operational units, not hidden app startup hooks
- migrations must stay separate from app startup
- backup and restore paths must be documented before risky stateful changes
- destructive operations require explicit opt-in and runbook backing
- operational workloads must still follow security, resource, secret, and namespace standards
+5
View File
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace
@@ -0,0 +1,80 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: migration-flyway-sql
labels:
app.kubernetes.io/name: migration-flyway
app.kubernetes.io/instance: migration-flyway
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
data:
V1__create_users_table.sql: |
create schema if not exists auth;
create or replace function auth.set_updated_at()
returns trigger as $$
begin
new.updated_at := current_timestamp;
return new;
end;
$$ language plpgsql;
create table auth.users (
id uuid not null,
email text not null,
encoded_password text not null,
name text not null,
provider text not null,
created_at timestamp with time zone not null default current_timestamp,
updated_at timestamp with time zone not null default current_timestamp,
constraint pk_users primary key (id),
constraint uq_users__email unique (email),
constraint ck_users__provider check (provider in ('LOCAL', 'GOOGLE', 'GITHUB'))
);
create index ix_users__created_at on auth.users (created_at);
create trigger trg_users__set_updated_at
before update on auth.users
for each row
when (old.* is distinct from new.*)
execute function auth.set_updated_at();
V2__add_oauth_login_columns.sql: |
alter table auth.users alter column encoded_password drop not null;
alter table auth.users add column provider_subject text;
alter table auth.users
add constraint uq_users__provider_provider_subject unique (provider, provider_subject);
V3__add_user_provider_field_constraints.sql: |
alter table auth.users
add constraint ck_users__local_password_required
check (
(provider = 'LOCAL' and encoded_password is not null and provider_subject is null)
or (provider <> 'LOCAL')
);
alter table auth.users
add constraint ck_users__social_subject_required
check (
(provider <> 'LOCAL' and provider_subject is not null and encoded_password is null)
or (provider = 'LOCAL')
);
V4__add_keycloak_provider.sql: |
alter table auth.users drop constraint ck_users__provider;
alter table auth.users
add constraint ck_users__provider check (provider in ('LOCAL', 'KEYCLOAK', 'GOOGLE', 'GITHUB'));
V5__keycloak_only_provider.sql: |
alter table auth.users drop constraint if exists ck_users__local_password_required;
alter table auth.users drop constraint if exists ck_users__social_subject_required;
alter table auth.users drop constraint if exists ck_users__provider;
alter table auth.users drop column if exists encoded_password;
alter table auth.users alter column provider_subject set not null;
alter table auth.users
add constraint ck_users__provider check (provider = 'KEYCLOAK');
+119
View File
@@ -0,0 +1,119 @@
apiVersion: batch/v1
kind: Job
metadata:
name: migration-flyway
labels:
app.kubernetes.io/name: migration-flyway
app.kubernetes.io/instance: migration-flyway
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
parallelism: 1
completions: 1
backoffLimit: 0
activeDeadlineSeconds: 1800
ttlSecondsAfterFinished: 86400
template:
metadata:
labels:
app.kubernetes.io/name: migration-flyway
app.kubernetes.io/instance: migration-flyway
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
spec:
serviceAccountName: migration-flyway-sa
automountServiceAccountToken: false
restartPolicy: Never
securityContext:
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
fsGroup: 1000
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
initContainers:
- name: flyway-info
image: flyway/flyway:10.20.1
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args: ["FLYWAY_USER=\"$(cat /run/secrets/db/SPRING_DATASOURCE_USERNAME)\" FLYWAY_PASSWORD=\"$(cat /run/secrets/db/SPRING_DATASOURCE_PASSWORD)\" exec /flyway/flyway info"]
env: &flywayEnv
- name: FLYWAY_URL
value: jdbc:postgresql://identity-postgres:5432/auth_server
- name: FLYWAY_LOCATIONS
value: filesystem:/flyway/sql
- name: FLYWAY_SCHEMAS
value: auth
- name: FLYWAY_DEFAULT_SCHEMA
value: auth
- name: FLYWAY_TABLE
value: flyway_schema_history
- name: FLYWAY_VALIDATE_ON_MIGRATE
value: "true"
- name: FLYWAY_BASELINE_ON_MIGRATE
value: "false"
- name: FLYWAY_OUT_OF_ORDER
value: "false"
- name: FLYWAY_MIXED
value: "false"
- name: FLYWAY_CLEAN_DISABLED
value: "true"
resources:
requests:
cpu: 50m
memory: 128Mi
limits:
memory: 256Mi
securityContext: &flywaySC
runAsNonRoot: true
runAsUser: 1000
runAsGroup: 1000
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts: &flywayVM
- name: sql
mountPath: /flyway/sql
readOnly: true
- name: db-secret
mountPath: /run/secrets/db
readOnly: true
- name: tmp
mountPath: /tmp
containers:
- name: flyway-migrate
image: flyway/flyway:10.20.1
imagePullPolicy: IfNotPresent
command: ["/bin/sh", "-c"]
args: ["FLYWAY_USER=\"$(cat /run/secrets/db/SPRING_DATASOURCE_USERNAME)\" FLYWAY_PASSWORD=\"$(cat /run/secrets/db/SPRING_DATASOURCE_PASSWORD)\" exec /flyway/flyway migrate"]
env: *flywayEnv
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
securityContext: *flywaySC
volumeMounts: *flywayVM
volumes:
- name: sql
configMap:
name: migration-flyway-sql
- name: db-secret
secret:
secretName: auth-server-db
defaultMode: 0400
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 128Mi
@@ -0,0 +1,16 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- configmap-sql.yaml
- job.yaml
labels:
- pairs:
app.kubernetes.io/name: migration-flyway
app.kubernetes.io/instance: migration-flyway
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
@@ -0,0 +1,12 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: migration-flyway-sa
labels:
app.kubernetes.io/name: migration-flyway
app.kubernetes.io/instance: migration-flyway
app.kubernetes.io/version: "0.1.0"
app.kubernetes.io/component: migration
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
automountServiceAccountToken: false
@@ -0,0 +1,5 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- namespace.yaml
@@ -0,0 +1,16 @@
apiVersion: v1
kind: Namespace
metadata:
name: mnt
labels:
app.kubernetes.io/name: mnt
app.kubernetes.io/instance: mnt
app.kubernetes.io/component: namespace
app.kubernetes.io/part-of: infra-platform
app.kubernetes.io/managed-by: kustomize
pod-security.kubernetes.io/enforce: restricted
pod-security.kubernetes.io/enforce-version: latest
pod-security.kubernetes.io/audit: restricted
pod-security.kubernetes.io/audit-version: latest
pod-security.kubernetes.io/warn: restricted
pod-security.kubernetes.io/warn-version: latest
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: docker-registry-config
data:
REGISTRY_HTTP_ADDR: "0.0.0.0:5000"
REGISTRY_LOG_FORMATTER: "json"
REGISTRY_STORAGE: "s3"
REGISTRY_STORAGE_S3_REGION: "us-east-1"
REGISTRY_STORAGE_S3_REGIONENDPOINT: "http://minio"
REGISTRY_STORAGE_S3_BUCKET: "docker-registry"
REGISTRY_STORAGE_S3_FORCEPATHSTYLE: "true"
REGISTRY_STORAGE_S3_SKIPVERIFY: "true"
REGISTRY_STORAGE_REDIRECT_DISABLE: "true"
REGISTRY_STORAGE_DELETE_ENABLED: "true"
@@ -0,0 +1,94 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: docker-registry
spec:
replicas: 1
revisionHistoryLimit: 5
progressDeadlineSeconds: 600
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: docker-registry
app.kubernetes.io/instance: docker-registry
template:
metadata:
labels:
app.kubernetes.io/name: docker-registry
app.kubernetes.io/instance: docker-registry
spec:
serviceAccountName: docker-registry
automountServiceAccountToken: false
terminationGracePeriodSeconds: 45
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
fsGroup: 10001
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: registry
image: registry:2.8.3
imagePullPolicy: IfNotPresent
ports:
- name: http
containerPort: 5000
protocol: TCP
envFrom:
- configMapRef:
name: docker-registry-config
- secretRef:
name: docker-registry-minio
resources:
requests:
cpu: 100m
memory: 256Mi
limits:
memory: 512Mi
startupProbe:
httpGet:
path: /v2/
port: http
periodSeconds: 5
failureThreshold: 12
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /v2/
port: http
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /v2/
port: http
periodSeconds: 30
failureThreshold: 3
timeoutSeconds: 3
securityContext:
runAsNonRoot: true
runAsUser: 10001
runAsGroup: 10001
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: tmp
mountPath: /tmp
volumes:
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 64Mi
+19
View File
@@ -0,0 +1,19 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- configmap.yaml
- service.yaml
- deployment.yaml
labels:
- pairs:
app.kubernetes.io/name: docker-registry
app.kubernetes.io/instance: docker-registry
app.kubernetes.io/version: "2.8.3"
app.kubernetes.io/component: registry
app.kubernetes.io/part-of: platform-registry
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
+15
View File
@@ -0,0 +1,15 @@
apiVersion: v1
kind: Service
metadata:
name: docker-registry
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: docker-registry
app.kubernetes.io/instance: docker-registry
ports:
- name: http
port: 5000
targetPort: http
protocol: TCP
appProtocol: http
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: docker-registry
automountServiceAccountToken: false
+6
View File
@@ -0,0 +1,6 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- vault
- docker-registry
@@ -0,0 +1,115 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: oauth2-proxy
spec:
replicas: 1
revisionHistoryLimit: 5
progressDeadlineSeconds: 600
strategy:
type: RollingUpdate
rollingUpdate:
maxSurge: 25%
maxUnavailable: 0
selector:
matchLabels:
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: oauth2-proxy
template:
metadata:
labels:
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: oauth2-proxy
spec:
serviceAccountName: oauth2-proxy-sa
automountServiceAccountToken: false
terminationGracePeriodSeconds: 30
topologySpreadConstraints:
- maxSkew: 1
topologyKey: kubernetes.io/hostname
whenUnsatisfiable: ScheduleAnyway
labelSelector:
matchLabels:
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: oauth2-proxy
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
fsGroup: 65532
fsGroupChangePolicy: OnRootMismatch
seccompProfile:
type: RuntimeDefault
containers:
- name: oauth2-proxy
image: quay.io/oauth2-proxy/oauth2-proxy:v7.15.2
imagePullPolicy: IfNotPresent
args:
- --config=/etc/oauth2-proxy/oauth2-proxy.cfg
ports:
- name: http
containerPort: 4180
protocol: TCP
- name: metrics
containerPort: 44180
protocol: TCP
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
memory: 256Mi
startupProbe:
httpGet:
path: /ping
port: http
periodSeconds: 5
failureThreshold: 12
timeoutSeconds: 3
readinessProbe:
httpGet:
path: /ready
port: http
periodSeconds: 10
failureThreshold: 3
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /ping
port: http
periodSeconds: 15
failureThreshold: 3
timeoutSeconds: 3
securityContext:
runAsNonRoot: true
runAsUser: 65532
runAsGroup: 65532
allowPrivilegeEscalation: false
privileged: false
readOnlyRootFilesystem: true
capabilities:
drop:
- ALL
seccompProfile:
type: RuntimeDefault
volumeMounts:
- name: config
mountPath: /etc/oauth2-proxy
readOnly: true
- name: secrets
mountPath: /etc/oauth2-proxy-secrets
readOnly: true
- name: tmp
mountPath: /tmp
volumes:
- name: config
configMap:
name: oauth2-proxy-config
- name: secrets
secret:
secretName: oauth2-proxy-secrets
defaultMode: 0400
- name: tmp
emptyDir:
medium: Memory
sizeLimit: 64Mi
@@ -0,0 +1,18 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- service.yaml
- deployment.yaml
labels:
- pairs:
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: oauth2-proxy
app.kubernetes.io/version: "7.15.2"
app.kubernetes.io/component: auth-proxy
app.kubernetes.io/part-of: auth-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
@@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: oauth2-proxy
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: oauth2-proxy
app.kubernetes.io/instance: oauth2-proxy
ports:
- name: http
port: 4180
targetPort: http
protocol: TCP
appProtocol: http
- name: metrics
port: 44180
targetPort: metrics
protocol: TCP
appProtocol: http
@@ -0,0 +1,5 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: oauth2-proxy-sa
automountServiceAccountToken: false
@@ -0,0 +1,12 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: vault-tokenreview-binding
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: ClusterRole
name: system:auth-delegator
subjects:
- kind: ServiceAccount
name: vault
namespace: mnt
+21
View File
@@ -0,0 +1,21 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: vault-config
data:
vault.hcl: |
ui = true
disable_mlock = true
storage "file" {
path = "/vault/data"
}
listener "tcp" {
address = "0.0.0.0:8200"
cluster_address = "0.0.0.0:8201"
tls_disable = 1
}
api_addr = "http://vault:8200"
cluster_addr = "http://vault:8201"
+20
View File
@@ -0,0 +1,20 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- serviceaccount.yaml
- clusterrolebinding.yaml
- configmap.yaml
- service.yaml
- statefulset.yaml
labels:
- pairs:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
app.kubernetes.io/version: "1.17.2"
app.kubernetes.io/component: secret-management
app.kubernetes.io/part-of: security-platform
app.kubernetes.io/managed-by: kustomize
includeSelectors: false
includeTemplates: true
+20
View File
@@ -0,0 +1,20 @@
apiVersion: v1
kind: Service
metadata:
name: vault
spec:
type: ClusterIP
selector:
app.kubernetes.io/name: vault
app.kubernetes.io/instance: vault
ports:
- name: http
port: 8200
targetPort: http
protocol: TCP
appProtocol: http
- name: cluster
port: 8201
targetPort: cluster
protocol: TCP
appProtocol: http
+4
View File
@@ -0,0 +1,4 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: vault

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