# 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 -` 모드로 **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 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: # password: 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 : 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//vso/` 60s timeout. 타임아웃 시 `VaultStaticSecret / VaultAuth / VaultConnection` finalizer 강제 해제 | | 3 | VSO Helm uninstall — `helm uninstall --wait 5m` (Operator 제거) | | 4 | 인프라 overlay 삭제 — `kubectl delete -k overlays//` 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 ``` --- ## 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 | 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 부터 깔끔하게 재개됨 ```