Files
project-infra/guide.md
T

902 lines
36 KiB
Markdown
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 부터 깔끔하게 재개됨
```