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
+43
View File
@@ -0,0 +1,43 @@
# k8s/scripts AGENTS
Role:
- own helper automation for render, diff, apply, validate, backup, restore, and CI checks
- support Kustomize and operations workflows without replacing declarative ownership
Allowed:
- bash entrypoints
- shell libraries
- CI validation scripts
- backup / restore helper scripts
- wrapper commands around `kubectl kustomize`, `kubectl diff -k`, `kubectl apply -k`
Forbidden:
- 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 deploy scripts that mix unrelated concerns
Read first:
- `/docs/standards/infra/scripts.md`
- `/docs/standards/infra/kustomize.md`
- `/docs/standards/infra/operations-runbook-upgrade-rollback.md`
- `/docs/standards/infra/backup-restore.md`
- `/docs/standards/infra/k3s-specific.md`
Examples:
- `/docs/examples/infra/scripts.md`
- `/docs/examples/infra/kustomize.md`
- `/docs/examples/infra/backup-restore.md`
- `/docs/examples/infra/operations-runbook-upgrade-rollback.md`
- `/docs/examples/infra/k3s-specific.md`
Rules:
- scripts are helpers, not source of truth
- prefer bash
- non-trivial scripts use functions + `main "$@"`
- use `local` for function-local variables
- ShellCheck must pass
- prefer render -> diff -> apply flow
- destructive actions require explicit opt-in
- environment/context/namespace must be explicit, not implicit
+80
View File
@@ -0,0 +1,80 @@
# Kubernetes Scripts
Imperative bootstrap, teardown, and break-glass operations for the `mnt` infra
stack. 선언적 관리(Service/Job/DB rollout 등)는 Kustomize 패키지의 몫이고,
여기 스크립트는 **phase orchestration****one-shot imperative setup**
(Vault init / unseal, Helm install) 만 담당한다.
## Layout
```
scripts/
├── bin/ # 사용자가 직접 실행하는 엔트리 포인트 (main "$@")
│ ├── bootstrap.sh # 전체 스택 부트스트랩
│ └── teardown.sh # 전체 스택 제거
├── tasks/ # bin/ 이 호출하는 단일 책임 태스크
│ ├── minio-operator-install.sh
│ ├── vault-init.sh # Vault init + unseal + k8s auth + policy/role
│ ├── vault-seed-apps.sh # 앱 시크릿 5 개 Vault KV 에 seed
│ ├── vault-setup-admin.sh
│ └── vso-install.sh # VSO Helm upgrade --install
├── lib/ # 공유 쉘 라이브러리
│ ├── common.sh # log/die, retry, confirm, kube-context 가드, finalizer 헬퍼
│ └── vault.sh # vault_exec 래퍼, 로그인/unseal 유틸
└── ci/
└── validate.sh # kustomize build + kubeconform + kube-linter + shellcheck + shfmt
```
## Entry points
```bash
# 부트스트랩 (대화형, bash history 안전)
bash k8s/scripts/bin/bootstrap.sh <dev|staging|prod>
# 제거 (정상 경로)
bash k8s/scripts/bin/teardown.sh <dev|staging|prod>
CONFIRM=yes bash k8s/scripts/bin/teardown.sh <dev|staging|prod>
# 로컬/CI 품질 게이트
bash k8s/scripts/ci/validate.sh
```
## 필수 환경 변수 (안전 가드)
쉘 rc 에 한 번 선언:
```bash
export KUBE_CONTEXT_DEV="homelab"
export KUBE_CONTEXT_STAGING="staging-cluster"
export KUBE_CONTEXT_PROD="prod-cluster"
```
- `bin/bootstrap.sh``bin/teardown.sh``ENV_NAME` 에 대응하는
`KUBE_CONTEXT_<ENV>``kubectl config current-context` 가 일치하는지
**실행 전 검증**한다. 매핑이 없으면 대화형으로 현재 context 이름 재입력을
요구한다.
- `env=prod` destructive 작업은 `ALLOW_PROD_DESTRUCTIVE=yes` 와 namespace
이름 재입력 TTY 확인이 추가로 필요하다.
## 파괴적 플래그
| 플래그 | 범위 | 효과 |
| --- | --- | --- |
| `CONFIRM=yes` | 두 엔트리 | 대화형 확인 프롬프트 자동 승인 (CI) |
| `SKIP_DIFF=yes` | bootstrap | Phase 3 의 `kubectl diff` preview 생략 (비권장) |
| `RESET_STALE_SECRETS=yes` | bootstrap | VSO-managed K8s Secret 을 선제 삭제 (Vault 값 재주입 유도) |
| `VAULT_KEYS_FILE=<path>` | bootstrap | Vault init key 저장 위치. **env=prod 는 repo 바깥 경로 필수** |
| `FORCE_FINALIZERS=yes` | teardown | Phase 6 활성 — finalizer 강제 제거 + `/finalize` API. Terminating 복구 외 금지 |
| `ALLOW_PROD_DESTRUCTIVE=yes` | teardown | env=prod teardown 허용 (namespace 재입력 추가 확인 필요) |
| `TEARDOWN_VSO_OPERATOR=yes` | teardown | **클러스터 공용** VSO Helm 릴리즈 + 전용 namespace + `vault-tokenreview-binding` 제거. 다른 env 의 Vault/VSO 가 함께 멈춤 |
| `TEARDOWN_VSO_CRDS=yes` | teardown | **클러스터 공용** VSO CRD + ClusterRole/CRB/Webhook 제거. 모든 env 의 VaultStaticSecret 인스턴스가 삭제됨 |
| `TEARDOWN_MINIO_OPERATOR=yes` | teardown | MinIO Operator Helm 릴리즈까지 제거 |
## Rules
- 서비스 단위 스크립트를 새로 만들지 않는다.
- Service / Job / Scheduler / DB rollout 은 Kustomize 패키지에서 선언.
- 스크립트는 phase orchestration 이지 Kubernetes 리소스 스펙의 출처가 아니다.
- 비트리비얼 스크립트는 `functions + main "$@"` 구조를 따른다.
- 모든 `.sh``ci/validate.sh``shellcheck -S style` + `shfmt -i 2 -bn -ci`
를 통과해야 한다.
+346
View File
@@ -0,0 +1,346 @@
#!/usr/bin/env bash
# Entry point: bootstrap the full infra stack for a given environment.
#
# Phases:
# 0. MinIO Operator Helm install (tasks/minio-operator-install.sh)
# 1. Apply namespace + PSS labels (kubectl apply -k base/managing/namespace)
# 2. (optional) Reset stale K8s Secrets (RESET_STALE_SECRETS=yes 일 때만)
# 2.5 VSO CRD 선행 설치 (VaultStaticSecret CR 가 overlay 에 포함돼 있어)
# 2.6 cert-manager 선행 설치 (ClusterIssuer / Certificate CR 가 overlay 에 포함돼 있어)
# 2.7 Keycloak Operator 선행 설치 (Keycloak / KeycloakRealmImport CR 가 overlay 에 포함돼 있어)
# 2.8 Traefik HelmChartConfig + Middleware/TLSOption (kube-system 에 배치 — root overlay 의 `namespace: mnt` 와 충돌하므로 별도 apply)
# 3. Render + diff + confirm + apply overlay (--server-side --field-manager=project-infra-bootstrap)
# 4. Wait for vault-0 Running
# 5. Vault init + unseal + KV + k8s auth + policy / role (tasks/vault-init.sh)
# 6. Seed application secrets (tasks/vault-seed-apps.sh)
# 7. Helm install VSO (tasks/vso-install.sh)
# 8. Apply VSO CRs (kubectl apply -k overlays/<env>/vso/)
# 9. MinIO docker-registry bucket/user/policy 프로비저닝 (tasks/minio-provision-registry.sh)
#
# 대화형 실행 (bash history 에 비밀번호 남지 않음):
# bash k8s/scripts/bin/bootstrap.sh dev
#
# Required context (안전 가드):
# KUBE_CONTEXT_DEV / KUBE_CONTEXT_STAGING / KUBE_CONTEXT_PROD
# 또는 KUBE_CONTEXT= 로 명시. 쉘 rc 에 선언해 두면 현재 kubectl context
# 와 일치 여부를 자동 검증한다. 매핑이 없으면 대화형으로 context 이름
# 재입력을 요구.
#
# Optional env:
# RESET_STALE_SECRETS=yes 이미 있는 VSO-managed K8s Secret 을 삭제 후 재생성
# VAULT_KEYS_FILE=<path> env=prod 필수 — repo 바깥 경로
# SKIP_DIFF=yes Phase 3 의 kubectl diff preview 생략
# CONFIRM=yes 대화형 프롬프트 자동 승인 (CI)
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
K8S_ROOT="$REPO_ROOT/k8s"
TASKS_DIR="$SCRIPT_DIR/../tasks"
export REPO_ROOT
NAMESPACE="mnt"
FIELD_MANAGER="project-infra-bootstrap"
# VSO 가 관리하는 K8s Secret 이름 목록 (기존 찌꺼기 감지용)
VSO_MANAGED_SECRETS=(
identity-postgres-superuser
keycloak-db
keycloak-db-operator
auth-server-db
keycloak-bootstrap-admin
keycloak-bootstrap-admin-operator
keycloak-client-auth-server-ingress
minio-tenant-env
docker-registry-minio
)
usage() {
cat >&2 <<EOF
Usage: bin/bootstrap.sh <dev|staging|prod>
Required env (권장 — 쉘 rc 에 선언):
KUBE_CONTEXT_DEV / KUBE_CONTEXT_STAGING / KUBE_CONTEXT_PROD env → kubectl context 매핑
(또는 KUBE_CONTEXT=<name> 로 한 번만 overrides)
Options:
RESET_STALE_SECRETS=yes 이미 존재하는 VSO-managed Secret 을 삭제 후 재생성
VAULT_KEYS_FILE=<path> env=prod 필수 — repo 바깥 경로
SKIP_DIFF=yes Phase 3 diff preview 생략 (비권장)
CONFIRM=yes 대화형 확인 자동 승인 (CI)
EOF
exit 1
}
precheck_namespace() {
if ! ns_exists "$NAMESPACE"; then
log "Precheck: namespace '$NAMESPACE' 없음 — 신규 생성 흐름"
return 0
fi
local phase
phase="$(ns_phase "$NAMESPACE")"
if [[ "$phase" == "Terminating" ]]; then
err "namespace '$NAMESPACE' 가 Terminating 상태입니다."
err "이전 teardown 이 완료되지 않아 새 리소스 생성이 불가능합니다."
err ""
err "복구 절차:"
err " 1) 이전 teardown 을 마저 끝내기:"
err " CONFIRM=yes bash k8s/scripts/bin/teardown.sh $ENV_NAME"
err " 2) namespace 가 완전히 사라진 걸 확인한 뒤 bootstrap 재실행:"
err " kubectl get namespace $NAMESPACE"
err " bash k8s/scripts/bin/bootstrap.sh $ENV_NAME"
die "bootstrap 중단"
fi
log "Precheck: namespace '$NAMESPACE' phase=$phase — 계속 진행"
}
phase0_minio_operator() {
log "[0/9] MinIO Operator 설치 (Tenant CRD 선행)"
bash "$TASKS_DIR/minio-operator-install.sh"
}
phase1_namespace() {
log "[1/9] Namespace + PSS 라벨 선행 apply"
kubectl apply -k "$K8S_ROOT/base/managing/namespace" \
--server-side --field-manager="$FIELD_MANAGER"
retry 5 1 kubectl get namespace "$NAMESPACE" >/dev/null
}
phase2_reset_stale_secrets() {
log "[2/9] 기존 VSO-managed K8s Secret 점검"
local stale_found=0 s
for s in "${VSO_MANAGED_SECRETS[@]}"; do
if kubectl -n "$NAMESPACE" get secret "$s" >/dev/null 2>&1; then
stale_found=$((stale_found + 1))
if [[ "${RESET_STALE_SECRETS:-}" == "yes" ]]; then
log " 삭제: $s (RESET_STALE_SECRETS=yes)"
kubectl -n "$NAMESPACE" delete secret "$s" --ignore-not-found
else
warn " $s 이미 존재 (VSO 가 덮어쓰지 않음). Vault 값 반영이 필요하면 RESET_STALE_SECRETS=yes 로 재실행하거나 수동 삭제하세요."
fi
fi
done
(( stale_found == 0 )) && log " 기존 Secret 없음"
}
phase2_5_vso_crds() {
# overlays/<env>/{database,keycloak,storage}/vault-secrets.yaml 에 VaultStaticSecret
# CR 들이 포함돼 있어, Phase 3 overlay apply 시점에 CRD 가 없으면 "resource mapping
# not found" 로 실패. Helm 차트 install 은 Phase 7 이므로 CRD 만 선행 적용.
log "[2.5/9] VSO CRD 선행 설치"
local version="${VSO_VERSION:-0.9.0}"
helm repo add hashicorp https://helm.releases.hashicorp.com >/dev/null 2>&1 || true
helm repo update hashicorp >/dev/null
helm show crds hashicorp/vault-secrets-operator --version "$version" \
| kubectl apply -f - --server-side --field-manager="$FIELD_MANAGER"
}
# cert-manager 전체 (CRD + namespace + controller + webhook) 을 overlay 밖에서
# 선행 설치. 이후 phase 3 에서 overlay 가 참조하는 ClusterIssuer / Certificate CR
# 이 등록될 수 있다. CRD established 대기를 반드시 건다 — deploy/webhook 가
# Ready 되기 전에 Certificate CR apply 시 admission webhook 이 거부함.
phase2_6_cert_manager() {
log "[2.6/9] cert-manager 선행 설치"
kubectl apply -k "$K8S_ROOT/overlays/$ENV_NAME/platform/cert-manager" \
--server-side --field-manager="$FIELD_MANAGER"
log " cert-manager CRD established 대기"
retry 30 2 kubectl wait --for=condition=Established --timeout=10s \
crd/clusterissuers.cert-manager.io \
crd/certificates.cert-manager.io \
crd/certificaterequests.cert-manager.io \
crd/orders.acme.cert-manager.io \
crd/challenges.acme.cert-manager.io
log " cert-manager Deployment Available 대기"
retry 30 5 kubectl -n cert-manager wait --for=condition=Available --timeout=10s \
deploy/cert-manager \
deploy/cert-manager-webhook \
deploy/cert-manager-cainjector
}
# Keycloak Operator (CRDs + Operator Deployment) 선행 설치. Keycloak /
# KeycloakRealmImport CR 이 phase 3 에 포함되므로 CRD 등록이 먼저 되어야 한다.
phase2_7_keycloak_operator() {
log "[2.7/9] Keycloak Operator 선행 설치"
kubectl apply -k "$K8S_ROOT/overlays/$ENV_NAME/platform/keycloak-operator" \
--server-side --field-manager="$FIELD_MANAGER"
log " Keycloak Operator CRD established 대기"
retry 30 2 kubectl wait --for=condition=Established --timeout=10s \
crd/keycloaks.k8s.keycloak.org \
crd/keycloakrealmimports.k8s.keycloak.org
log " Keycloak Operator Deployment Available 대기"
retry 60 5 kubectl -n "$NAMESPACE" wait --for=condition=Available --timeout=10s \
deploy/keycloak-operator
}
# kube-system Traefik 커스터마이징 — HelmChartConfig (K3s Helm-controller 가
# 재수렴), Middleware (https-redirect / security-headers), TLSOption (modern-tls).
# root overlay 가 `namespace: mnt` 로 전역 주입하므로 kube-system 타깃 리소스는
# 이 overlay 빌드에 포함시키지 않고 별도 apply 한다.
phase2_8_traefik() {
log "[2.8/9] Traefik HelmChartConfig + Middleware 적용 (kube-system)"
kubectl apply -k "$K8S_ROOT/overlays/$ENV_NAME/platform/traefik" \
--server-side --field-manager="$FIELD_MANAGER"
}
# render → server-side dry-run → diff → confirm → apply
#
# kubectl diff exit codes (GNU man page):
# 0 — 변경 없음
# 1 — 변경 있음 (정상)
# >1 — 실행 오류 (RBAC / API 연결 / invalid manifest 등)
# 이전 구현은 `|| true` 로 모든 비-0 을 흡수해서 에러가 apply 까지 흘러갔다.
phase3_overlay_apply() {
log "[3/9] 인프라 overlay 배포 ($OVERLAY_DIR)"
local tmpdir
tmpdir="$(mktemp -d -t project-infra-bootstrap.XXXXXX)"
trap_cleanup_path "$tmpdir"
local rendered="$tmpdir/rendered.yaml"
kustomize build "$OVERLAY_DIR" > "$rendered"
log " render 완료: $(grep -c '^kind:' "$rendered") resources"
# --- server-side dry-run: admission / RBAC / schema 를 API server 로 검증 ---
log " server-side dry-run 검증"
if ! kubectl apply -f "$rendered" --dry-run=server \
--server-side --field-manager="$FIELD_MANAGER" \
>"$tmpdir/dryrun.out" 2>&1; then
err " server-side dry-run 실패 — apply 중단"
sed 's/^/ /' "$tmpdir/dryrun.out" >&2
die "dry-run 검증 실패"
fi
# --- diff preview — 변경 있음(rc=1)은 정상, 실행 오류(rc>=2)는 die ---
if [[ "${SKIP_DIFF:-}" == "yes" ]]; then
warn " SKIP_DIFF=yes → diff preview 생략"
else
log " server-side diff preview (대용량이면 스크롤)"
local rc=0
kubectl diff -f "$rendered" --server-side --field-manager="$FIELD_MANAGER" \
>"$tmpdir/diff.out" 2>&1 || rc=$?
case "$rc" in
0) log " (변경 없음)" ;;
1)
if [[ -s "$tmpdir/diff.out" ]]; then
sed 's/^/ /' "$tmpdir/diff.out" >&2
else
log " (diff 출력 없음)"
fi
;;
*)
err " kubectl diff 실행 실패 (exit=$rc) — apply 중단"
sed 's/^/ /' "$tmpdir/diff.out" >&2
die "diff 실패"
;;
esac
fi
confirm "위 변경사항을 env=$ENV_NAME context='$(kubectl config current-context)' 에 apply 하시겠습니까?" \
|| die "사용자 취소"
kubectl apply -f "$rendered" --server-side --field-manager="$FIELD_MANAGER"
log " vault/registry/앱 워크로드 apply 완료"
}
phase4_wait_vault_running() {
# Vault readiness probe 는 initialized+unsealed 일 때만 통과하므로 초기화 *전*
# 에는 Ready 가 될 수 없음 → Running 단계까지만 기다림.
log "[4/9] vault-0 Running 대기 (120s × 재시도 3회)"
retry 3 10 kubectl -n "$NAMESPACE" wait \
--for=jsonpath='{.status.phase}'=Running pod/vault-0 --timeout=120s
}
phase5_vault_init() {
log "[5/9] Vault 초기화 / unseal / auth / policy / role"
ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vault-init.sh"
}
phase6_seed_apps() {
log "[6/9] 앱 시크릿 5 개 Vault KV 에 seed"
ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vault-seed-apps.sh"
}
phase7_vso_install() {
log "[7/9] VSO Helm upgrade --install"
ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vso-install.sh"
}
phase8_vso_crs() {
log "[8/9] VSO CR 적용 ($OVERLAY_DIR/vso/)"
kubectl apply -k "$OVERLAY_DIR/vso/" \
--server-side --field-manager="$FIELD_MANAGER"
}
# docker-registry 가 MinIO 를 S3 backend 로 쓰도록 bucket + 서비스 user +
# bucket-scoped policy 를 설정. MinIO Tenant 는 phase 8 에서 minio-tenant-env
# Secret 이 생긴 뒤 기동되므로 이 phase 는 반드시 phase 8 이후에 실행.
phase9_minio_provision_registry() {
log "[9/9] MinIO docker-registry bucket/user/policy 프로비저닝"
ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/minio-provision-registry.sh"
}
summary() {
log "============================================"
log " $ENV_NAME 부트스트랩 완료"
log "============================================"
log "unseal keys + root token: ${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json} (0600)"
log " → 오프라인 / 외부 KMS 로 즉시 이동하세요 (Git 반입 금지)"
log ""
log "앱 시크릿 5 개는 Phase 6 에서 입력/seed 됨."
log "조회: vault kv get -field=password secret/<path>"
log ""
log "다음 확인:"
log " kubectl -n $NAMESPACE get pods"
log " kubectl -n $NAMESPACE get secrets"
log " kubectl -n $NAMESPACE get vaultstaticsecret"
}
main() {
ENV_NAME="${1:-}"
case "$ENV_NAME" in
dev|staging|prod) ;;
*) usage ;;
esac
OVERLAY_DIR="$K8S_ROOT/overlays/$ENV_NAME"
[[ -d "$OVERLAY_DIR" ]] || die "overlay 디렉토리 없음: $OVERLAY_DIR"
require_cmd kubectl helm jq kustomize
# 안전 가드 — 실수 클러스터 apply 방지
require_kube_context "$ENV_NAME"
log "============================================"
log " Project-Infra 부트스트랩 ($ENV_NAME)"
log " context : $(kubectl config current-context)"
log "============================================"
precheck_namespace
phase0_minio_operator
phase1_namespace
phase2_reset_stale_secrets
phase2_5_vso_crds
phase2_6_cert_manager
phase2_7_keycloak_operator
phase2_8_traefik
phase3_overlay_apply
phase4_wait_vault_running
phase5_vault_init
phase6_seed_apps
phase7_vso_install
phase8_vso_crs
phase9_minio_provision_registry
summary
}
# Google shell style — 직접 실행일 때만 main 호출 (source 된 경우 함수만 노출).
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+309
View File
@@ -0,0 +1,309 @@
#!/usr/bin/env bash
# Entry point: tear down the full infra stack for a given environment.
#
# 체계적 정리 — 기본 경로는 "정상 API 삭제" 만 사용한다. finalizer 강제 제거와
# /finalize API 호출 같은 파괴적 복구 경로는 별도 플래그 아래로 격리했다.
#
# Phases:
# 1. Precheck (namespace 존재 여부)
# 2. VSO CR 삭제 (controller 살아있을 때 정상 경로만)
# 3. VSO Helm uninstall
# 4. 인프라 overlay 삭제 (Vault / Registry / 앱)
# 5. namespace 삭제
# 6. (opt) FORCE_FINALIZERS=yes 일 때만 — finalizer 강제 제거 + /finalize
# 7. Cluster-scoped 리소스 정리 (ClusterRoleBinding, VSO ClusterRole/Webhook/CRD)
#
# 대화형:
# bash k8s/scripts/bin/teardown.sh dev
#
# CI (비대화):
# CONFIRM=yes bash k8s/scripts/bin/teardown.sh dev
#
# Required context (bootstrap 과 동일한 안전 가드):
# KUBE_CONTEXT_<ENV_UPPER> 또는 KUBE_CONTEXT
#
# Destructive 플래그:
# FORCE_FINALIZERS=yes
# Phase 6 실행 — PVC / CRD / 모든 namespaced 리소스 finalizer 강제 제거 및
# namespace /finalize 호출. PV 가 orphaned 되고 컨트롤러 정리 누락이 발생할
# 수 있으므로 Terminating 복구 외 목적으로는 쓰지 말 것.
#
# ALLOW_PROD_DESTRUCTIVE=yes
# env=prod teardown 을 허용. 추가로 namespace 이름 재입력 TTY 확인이 필요.
#
# TEARDOWN_VSO_OPERATOR=yes
# Phase 3 에서 VSO Helm 릴리즈와 vault-secrets-operator-system namespace 까지
# 제거. VSO operator 는 환경별이 아니라 **클러스터 공용** 이다. 다른 env 가
# 같은 클러스터에 있으면 그쪽 VSO reconciliation 이 같이 멈춘다.
# Phase 7 의 vault-tokenreview-binding (cluster-scoped 이름 고정) 도 이
# 플래그로 함께 삭제한다. VSO 를 완전히 내릴 때만 켜라. 기본은 skip.
#
# TEARDOWN_VSO_CRDS=yes
# Phase 7 에서 cluster-scoped VSO 리소스 (ClusterRole/CRB/Webhook/CRD) 를
# 제거. CRD 는 클러스터 전체 공유라 같은 클러스터에 다른 env 가 있으면
# 그쪽 인스턴스까지 날아간다. 단일-env-per-cluster 환경 또는 VSO 를 완전히
# 버리려는 의도일 때만 켜라. 기본은 skip.
#
# TEARDOWN_MINIO_OPERATOR=yes
# MinIO Operator Helm 릴리즈도 제거 (기본 유지).
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
K8S_ROOT="$REPO_ROOT/k8s"
NAMESPACE="mnt"
VSO_NAMESPACE="${VSO_NAMESPACE:-vault-secrets-operator-system}"
VSO_RELEASE="${VSO_RELEASE:-vault-secrets-operator}"
VSO_CRD_KINDS=(vaultstaticsecret vaultauth vaultconnection)
usage() {
cat >&2 <<EOF
Usage: bin/teardown.sh <dev|staging|prod>
Required env:
KUBE_CONTEXT_DEV / KUBE_CONTEXT_STAGING / KUBE_CONTEXT_PROD
(또는 KUBE_CONTEXT=<name>)
Options:
CONFIRM=yes 대화형 확인 자동 승인
FORCE_FINALIZERS=yes Phase 6 (finalizer 강제 + /finalize) 활성화
ALLOW_PROD_DESTRUCTIVE=yes env=prod teardown 허용 + namespace 재입력 필요
TEARDOWN_VSO_OPERATOR=yes VSO Helm 릴리즈 + 전용 namespace + vault-tokenreview-binding
모두 제거. VSO 는 클러스터 공용 플랫폼 리소스이므로
다른 env 의 reconciliation 이 함께 멈춤.
TEARDOWN_VSO_CRDS=yes VSO cluster-scoped 리소스 (ClusterRole/CRB/Webhook/CRD)
제거. 클러스터 전체에 영향.
TEARDOWN_MINIO_OPERATOR=yes MinIO Operator 도 함께 제거
EOF
exit 1
}
phase1_precheck() {
log "[1/7] Precheck"
if ns_exists "$NAMESPACE"; then
log " namespace $NAMESPACE phase=$(ns_phase "$NAMESPACE")"
else
log " namespace $NAMESPACE 없음 → 일부 phase 는 skip"
fi
}
phase2_vso_cr_delete() {
log "[2/7] VSO CR 삭제"
if kubectl get -k "$OVERLAY_DIR/vso/" >/dev/null 2>&1; then
# 정상 API 삭제만 시도. 타임아웃 나도 Phase 6 (FORCE_FINALIZERS) 에 위임.
kubectl delete -k "$OVERLAY_DIR/vso/" --ignore-not-found \
--wait=true --timeout=60s 2>&1 \
|| warn " VSO CR 삭제 타임아웃 — FORCE_FINALIZERS=yes 로 재실행하면 강제 해제"
else
log " VSO overlay 리소스 없음 → skip"
fi
}
uninstall_vso_in_ns() {
local ns="$1"
kubectl get namespace "$ns" >/dev/null 2>&1 || return 0
# pre-delete hook Job 이 stuck 이면 uninstall 이 hang → 먼저 정리
kubectl -n "$ns" get job -l app.kubernetes.io/instance="$VSO_RELEASE" -o name 2>/dev/null \
| xargs -r kubectl -n "$ns" delete --force --grace-period=0 --ignore-not-found 2>/dev/null || true
kubectl -n "$ns" delete job pdcc-vault-secrets-operator \
--force --grace-period=0 --ignore-not-found 2>/dev/null || true
if helm -n "$ns" status "$VSO_RELEASE" >/dev/null 2>&1; then
log " [$ns] helm uninstall $VSO_RELEASE"
helm -n "$ns" uninstall "$VSO_RELEASE" --wait --timeout 5m 2>&1 \
|| warn " [$ns] helm uninstall 실패 — 릴리즈 Secret 잔존물 직접 제거"
else
log " [$ns] VSO 릴리즈 없음"
fi
# uninstall 실패 / rollback 잔존 상태에서 남는 sh.helm.release.v1.* Secret 정리
kubectl -n "$ns" get secret -o name 2>/dev/null \
| grep "sh.helm.release.*${VSO_RELEASE}" \
| xargs -r kubectl -n "$ns" delete --ignore-not-found 2>/dev/null || true
}
# $NAMESPACE (mnt) 내 잔존 uninstall 은 **env-specific 레거시 cleanup** 이라
# 항상 실행한다 — 과거 VSO 가 mnt 에 설치됐던 적이 있으면 릴리즈 메타데이터가
# 남아 다음 install 을 막으므로. $VSO_NAMESPACE (공용 operator 공간) 에 대한
# uninstall 은 클러스터 공유이므로 명시적 opt-in 요구.
phase3_vso_helm_uninstall() {
log "[3/7] VSO Helm uninstall"
uninstall_vso_in_ns "$NAMESPACE"
if [[ "${TEARDOWN_VSO_OPERATOR:-}" == "yes" ]]; then
warn " TEARDOWN_VSO_OPERATOR=yes — 공용 VSO operator ($VSO_NAMESPACE) 제거"
warn " 경고: 같은 클러스터에 다른 env 의 VaultStaticSecret 이 있으면 reconciliation 이 멈춥니다."
uninstall_vso_in_ns "$VSO_NAMESPACE"
else
log " VSO operator ($VSO_NAMESPACE) 보존 (TEARDOWN_VSO_OPERATOR=yes 로 제거 가능)"
fi
}
phase4_overlay_delete() {
log "[4/7] 인프라 리소스 삭제 (kustomize overlay)"
if ns_exists "$NAMESPACE"; then
kubectl delete -k "$OVERLAY_DIR" --ignore-not-found \
--wait=true --timeout=120s 2>&1 \
|| warn " overlay 삭제 타임아웃 — 필요 시 FORCE_FINALIZERS=yes 로 Phase 6 사용"
else
log " namespace 없음 → overlay 삭제 skip"
fi
}
phase5_namespace_delete() {
log "[5/7] namespace 정상 삭제"
if ns_exists "$NAMESPACE"; then
kubectl delete namespace "$NAMESPACE" --ignore-not-found --wait=false 2>/dev/null || true
log " namespace 제거 대기 (최대 60s)"
if wait_namespace_gone "$NAMESPACE" 60; then
log " namespace $NAMESPACE 제거 완료"
else
warn " namespace $NAMESPACE 가 Terminating 60s 초과."
warn " 정상 삭제로 끝나지 않은 경우:"
warn " 1) 'kubectl get all -n $NAMESPACE' 로 남은 리소스 원인 확인"
warn " 2) controller/operator 재기동 으로 finalizer 처리 시도"
warn " 3) 그래도 막히면 FORCE_FINALIZERS=yes 로 재실행 — 단 파괴적"
fi
else
log " namespace 이미 없음"
fi
}
# FORCE_FINALIZERS=yes 일 때만 — PV orphaned / 데이터 정합성 리스크 있음.
phase6_force_finalizers() {
if [[ "${FORCE_FINALIZERS:-}" != "yes" ]]; then
log "[6/7] FORCE_FINALIZERS!=yes → skip (파괴적 경로 비활성)"
return 0
fi
log "[6/7] FORCE_FINALIZERS=yes — finalizer 강제 제거 + /finalize"
warn " 경고: PV orphan / controller 정리 누락이 발생할 수 있습니다."
warn " Terminating stuck 복구 외 목적으로 사용하지 마세요."
if ! ns_exists "$NAMESPACE"; then
log " namespace 이미 없음 → skip"
return 0
fi
strip_finalizers_in_ns "$NAMESPACE" persistentvolumeclaim
strip_finalizers_in_ns "$NAMESPACE" "${VSO_CRD_KINDS[@]}" 2>/dev/null || true
log " namespace $NAMESPACE 전체 리소스 finalizer 일괄 제거 (최후 수단)"
strip_finalizers_all_ns_resources "$NAMESPACE"
kubectl delete namespace "$NAMESPACE" --ignore-not-found --wait=false 2>/dev/null || true
if wait_namespace_gone "$NAMESPACE" 30; then
log " namespace $NAMESPACE 제거 완료 (FORCE_FINALIZERS)"
return 0
fi
warn " namespace 여전히 Terminating → /finalize API 호출"
force_finalize_namespace "$NAMESPACE" \
|| warn " /finalize 호출 실패 — 수동 확인 필요"
if wait_namespace_gone "$NAMESPACE" 30; then
log " namespace $NAMESPACE 제거 완료 (/finalize)"
else
err " namespace $NAMESPACE 여전히 존재. 'kubectl get namespace $NAMESPACE -o yaml' 로 확인 필요."
fi
}
phase7_cluster_scoped() {
log "[7/7] Cluster-scoped 리소스 정리"
# vault-tokenreview-binding 은 cluster-scoped 이름 고정이라 env 별 분리가
# 불가능하다 (kustomize base 를 env-scoped 이름으로 재설계하기 전까지).
# 기본 경로에서 삭제하면 다른 env 의 Vault k8s auth TokenReview 가 부서지므로
# TEARDOWN_VSO_OPERATOR=yes 와 함께 게이트 (플랫폼 auth 평면 전체 정리).
if [[ "${TEARDOWN_VSO_OPERATOR:-}" == "yes" ]]; then
log " vault-tokenreview-binding 제거 (TEARDOWN_VSO_OPERATOR=yes)"
kubectl delete clusterrolebinding vault-tokenreview-binding --ignore-not-found 2>&1 \
| sed 's/^/ /' || true
if ns_exists "$VSO_NAMESPACE"; then
log " VSO namespace $VSO_NAMESPACE 삭제"
kubectl delete namespace "$VSO_NAMESPACE" --ignore-not-found --wait=true --timeout=60s 2>/dev/null \
|| warn " $VSO_NAMESPACE 삭제 타임아웃 — 수동 확인 필요"
fi
else
log " vault-tokenreview-binding 보존 (TEARDOWN_VSO_OPERATOR=yes 로 제거 가능)"
fi
# VSO 의 ClusterRole / CRB / Webhook / CRD 는 클러스터 전체 공유다.
# 운영자가 의도적으로 VSO 전체를 버릴 때만 TEARDOWN_VSO_CRDS=yes 로 활성화.
if [[ "${TEARDOWN_VSO_CRDS:-}" == "yes" ]]; then
warn " TEARDOWN_VSO_CRDS=yes — cluster-scoped VSO 리소스 제거 (ClusterRole/CRB/Webhook/CRD)"
warn " 경고: 같은 클러스터의 다른 env 에서 VSO 를 쓰고 있으면 모두 영향받습니다."
local kind
for kind in clusterrole clusterrolebinding validatingwebhookconfiguration mutatingwebhookconfiguration; do
kubectl get "$kind" -o name 2>/dev/null \
| grep -E 'vault-secrets-operator' \
| xargs -r kubectl delete --ignore-not-found 2>/dev/null || true
done
kubectl get crd -o name 2>/dev/null \
| grep 'secrets.hashicorp.com' \
| xargs -r kubectl delete --ignore-not-found 2>/dev/null || true
else
log " cluster-scoped VSO 리소스 보존 (TEARDOWN_VSO_CRDS=yes 로 제거 가능)"
fi
if [[ "${TEARDOWN_MINIO_OPERATOR:-}" == "yes" ]]; then
log " TEARDOWN_MINIO_OPERATOR=yes → MinIO Operator 제거"
helm -n minio-operator uninstall minio-operator --wait --timeout 5m 2>/dev/null \
|| warn " MinIO Operator Helm uninstall 실패"
kubectl delete namespace minio-operator --ignore-not-found --wait=true --timeout=60s \
|| warn " minio-operator namespace 삭제 실패"
else
log " MinIO Operator 유지 (TEARDOWN_MINIO_OPERATOR=yes 로 제거 가능)"
fi
}
summary() {
log "============================================"
log " $ENV_NAME 삭제 완료"
log "============================================"
log "Vault unseal key 파일은 그대로 남아있습니다 (${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json})."
log "완전 초기화하려면 수동 삭제하세요."
}
main() {
ENV_NAME="${1:-}"
case "$ENV_NAME" in
dev|staging|prod) ;;
*) usage ;;
esac
OVERLAY_DIR="$K8S_ROOT/overlays/$ENV_NAME"
[[ -d "$OVERLAY_DIR" ]] || die "overlay 디렉토리 없음: $OVERLAY_DIR"
require_cmd kubectl helm jq
# 안전 가드 1 — kubectl context 가 env 와 맞는지
require_kube_context "$ENV_NAME"
# 안전 가드 2 — prod 는 추가 게이트 (namespace 재입력 포함)
require_production_gate "$ENV_NAME" "$NAMESPACE"
log "============================================"
log " Project-Infra 삭제 ($ENV_NAME)"
log " context : $(kubectl config current-context)"
log "============================================"
confirm "namespace='$NAMESPACE' 의 모든 Vault / Registry / VSO / 앱 리소스를 삭제합니다. 계속?" \
|| die "사용자 취소"
phase1_precheck
phase2_vso_cr_delete
phase3_vso_helm_uninstall
phase4_overlay_delete
phase5_namespace_delete
phase6_force_finalizers
phase7_cluster_scoped
summary
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+167
View File
@@ -0,0 +1,167 @@
#!/usr/bin/env bash
# Validate all kustomize overlays against schema + lint rules,
# 그리고 scripts/ 전체에 대해 shell 품질 게이트 (shellcheck + shfmt) 를 실행한다.
#
# Runs, for each environment (dev, staging, prod):
# 1. `kustomize build overlays/<env>` — structural validity
# 2. `kustomize build overlays/<env>/vso` — VSO CRD overlay (built separately
# because it requires the VSO Helm
# release to be installed first)
# 3. `kubeconform -strict -ignore-missing-schemas` — OpenAPI schema validation
# with CRD schemas fetched from the Datree catalog.
# 4. `kube-linter lint` — anti-pattern lint against
# root .kube-linter.yaml configuration.
#
# Plus (not per-overlay):
# 5. `shellcheck -S style` over k8s/scripts/**/*.sh — shell correctness + style
# 6. `shfmt -i 2 -bn -ci -d` over k8s/scripts/ — formatting diff (fail on drift)
#
# Exit codes:
# 0 — everything passed
# 1 — at least one step failed (details printed)
#
# Tools expected on $PATH:
# kustomize kubeconform kube-linter shellcheck shfmt
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
REPO_ROOT="$(cd "$SCRIPT_DIR/../../.." && pwd)"
K8S_ROOT="$REPO_ROOT/k8s"
KUBE_LINTER_CFG="$REPO_ROOT/.kube-linter.yaml"
# Local-user tooling fallback: when tools aren't installed system-wide,
# allow users to drop them under ~/bin (CI typically installs to a PATH dir).
if [[ -d "$HOME/bin" ]]; then
export PATH="$HOME/bin:$PATH"
fi
require_cmd kustomize kubeconform kube-linter shellcheck shfmt
ENVS=(dev staging prod)
OVERLAYS_TO_BUILD=()
for env in "${ENVS[@]}"; do
root_overlay="$K8S_ROOT/overlays/$env"
vso_overlay="$K8S_ROOT/overlays/$env/vso"
if [[ -f "$root_overlay/kustomization.yaml" ]]; then
OVERLAYS_TO_BUILD+=("$root_overlay")
else
log "skip (kustomization.yaml 없음): overlays/$env"
fi
if [[ -f "$vso_overlay/kustomization.yaml" ]]; then
OVERLAYS_TO_BUILD+=("$vso_overlay")
fi
done
# Datree CRD catalog — kubeconform fetches per-CRD JSON schema on demand.
CRD_CATALOG='https://raw.githubusercontent.com/datreeio/CRDs-catalog/main/{{.Group}}/{{.ResourceKind}}_{{.ResourceAPIVersion}}.json'
WORK_DIR="$(mktemp -d -t project-infra-validate.XXXXXX)"
trap_cleanup_path "$WORK_DIR"
declare -A BUILD_STATUS SCHEMA_STATUS LINT_STATUS
for overlay in "${OVERLAYS_TO_BUILD[@]}"; do
rel="${overlay#"$REPO_ROOT/"}"
rendered="$WORK_DIR/$(echo "$rel" | tr '/' '_').yaml"
# -- 1. kustomize build ----------------------------------------------------
if kustomize build "$overlay" > "$rendered" 2>"$rendered.err"; then
BUILD_STATUS[$rel]="ok"
log "kustomize build $rel — OK ($(grep -c '^kind:' "$rendered") resources)"
else
BUILD_STATUS[$rel]="fail"
err "kustomize build $rel — FAIL"
cat "$rendered.err" >&2
continue
fi
# -- 2. kubeconform --------------------------------------------------------
if kubeconform -strict -ignore-missing-schemas \
-schema-location default \
-schema-location "$CRD_CATALOG" \
-summary \
"$rendered" >"$rendered.kconf" 2>&1; then
SCHEMA_STATUS[$rel]="ok"
log " kubeconform — OK ($(tail -1 "$rendered.kconf"))"
else
SCHEMA_STATUS[$rel]="fail"
err " kubeconform — FAIL"
cat "$rendered.kconf" >&2
fi
# -- 3. kube-linter --------------------------------------------------------
if kube-linter lint --config "$KUBE_LINTER_CFG" "$rendered" \
>"$rendered.klint" 2>&1; then
LINT_STATUS[$rel]="ok"
log " kube-linter — OK"
else
LINT_STATUS[$rel]="fail"
err " kube-linter — findings:"
sed 's/^/ /' "$rendered.klint" >&2
fi
done
# -----------------------------------------------------------------------------
# 5. shellcheck — scripts/**/*.sh 전체
# -----------------------------------------------------------------------------
SHELL_STATUS="skip"
SCRIPTS_ROOT="$K8S_ROOT/scripts"
mapfile -t SHELL_FILES < <(find "$SCRIPTS_ROOT" -type f -name '*.sh' -print | sort)
if (( ${#SHELL_FILES[@]} > 0 )); then
log "shellcheck — ${#SHELL_FILES[@]} files"
if shellcheck -S style -x "${SHELL_FILES[@]}" >"$WORK_DIR/shellcheck.out" 2>&1; then
SHELL_STATUS="ok"
log " shellcheck — OK"
else
SHELL_STATUS="fail"
err " shellcheck — findings:"
sed 's/^/ /' "$WORK_DIR/shellcheck.out" >&2
fi
fi
# -----------------------------------------------------------------------------
# 6. shfmt — 포매팅 drift 검증 (수정 없이 diff 만 출력)
# -----------------------------------------------------------------------------
FMT_STATUS="skip"
if (( ${#SHELL_FILES[@]} > 0 )); then
log "shfmt -i 2 -bn -ci -d"
if shfmt -i 2 -bn -ci -d "${SHELL_FILES[@]}" >"$WORK_DIR/shfmt.out" 2>&1; then
FMT_STATUS="ok"
log " shfmt — OK"
else
FMT_STATUS="fail"
err " shfmt — drift 감지 (로컬에서 'shfmt -i 2 -bn -ci -w k8s/scripts' 로 정렬하세요):"
sed 's/^/ /' "$WORK_DIR/shfmt.out" >&2
fi
fi
# -----------------------------------------------------------------------------
# Summary
# -----------------------------------------------------------------------------
echo >&2
log "============================================"
log " Validation summary"
log "============================================"
failed=0
for overlay in "${OVERLAYS_TO_BUILD[@]}"; do
rel="${overlay#"$REPO_ROOT/"}"
b="${BUILD_STATUS[$rel]:-skip}"
s="${SCHEMA_STATUS[$rel]:-skip}"
l="${LINT_STATUS[$rel]:-skip}"
printf ' %-40s build=%-4s schema=%-4s lint=%s\n' "$rel" "$b" "$s" "$l" >&2
[[ "$b" == "fail" || "$s" == "fail" || "$l" == "fail" ]] && failed=$((failed + 1))
done
printf ' %-40s shellcheck=%-4s shfmt=%s\n' "scripts/" "$SHELL_STATUS" "$FMT_STATUS" >&2
[[ "$SHELL_STATUS" == "fail" || "$FMT_STATUS" == "fail" ]] && failed=$((failed + 1))
if (( failed > 0 )); then
err "$failed check(s) 실패"
exit 1
fi
log "모든 check 통과"
+302
View File
@@ -0,0 +1,302 @@
#!/usr/bin/env bash
# Common shell library for Project-Infra scripts.
#
# Source this with: . "$(dirname "$0")/../lib/common.sh"
# or from tasks/: . "$SCRIPT_DIR/../lib/common.sh"
#
# Provides:
# - strict mode + safe IFS
# - log()/warn()/err()/die() to stderr with ISO 8601 + level prefix
# - trap_cleanup_path() / trap_cleanup_fn() — EXIT 시 경로 rm -rf 또는 함수 호출
# - require_cmd() / require_env() — preconditions
# - confirm() — interactive + CONFIRM=yes env-var gate
# - mask_secret() — masks sensitive values in logs
# - retry() — retry a command with linear backoff
# - require_kube_context() / require_production_gate() — env 타깃 검증 가드
# - ns_exists() / ns_phase() / strip_finalizers_in_ns() /
# strip_finalizers_all_ns_resources() / force_finalize_namespace() /
# wait_namespace_gone() — namespace teardown 복구 헬퍼
#
# All functions emit diagnostic output to stderr; stdout stays clean
# so callers can pipe subcommand output normally.
# shellcheck shell=bash
# -----------------------------------------------------------------------------
# strict mode
# -----------------------------------------------------------------------------
set -Eeuo pipefail
IFS=$'\n\t'
# -----------------------------------------------------------------------------
# logging
# -----------------------------------------------------------------------------
_ts() { date -u +"%Y-%m-%dT%H:%M:%SZ"; }
log() { printf '%s [INFO] %s\n' "$(_ts)" "$*" >&2; }
warn() { printf '%s [WARN] %s\n' "$(_ts)" "$*" >&2; }
err() { printf '%s [ERROR] %s\n' "$(_ts)" "$*" >&2; }
die() { err "$*"; exit 1; }
# -----------------------------------------------------------------------------
# cleanup registration
#
# eval 기반 문자열 cleanup 은 공통 라이브러리에 두기에 부적절하다 (셸 인젝션
# 경로가 열리기 쉽고, Google shell 가이드 기준 code smell). 대신 두 가지 구체
# 타입만 제공한다:
# - trap_cleanup_path <path> : EXIT 시 rm -rf 로 삭제할 경로
# - trap_cleanup_fn <fname> : EXIT 시 인자 없이 호출할 함수 이름
# 두 종류 다 LIFO 로 실행되고, 실패해도 전체 종료 코드는 보존된다.
# -----------------------------------------------------------------------------
_CLEANUP_PATHS=()
_CLEANUP_FNS=()
trap_cleanup_path() {
local p="$1"
[[ -n "$p" ]] || return 0
_CLEANUP_PATHS+=("$p")
}
trap_cleanup_fn() {
local fn="$1"
declare -F "$fn" >/dev/null 2>&1 \
|| { err "trap_cleanup_fn: 함수를 찾을 수 없음: $fn"; return 1; }
_CLEANUP_FNS+=("$fn")
}
_run_cleanups() {
local rc=$?
local i
for ((i = ${#_CLEANUP_FNS[@]} - 1; i >= 0; i--)); do
"${_CLEANUP_FNS[$i]}" || true
done
for ((i = ${#_CLEANUP_PATHS[@]} - 1; i >= 0; i--)); do
rm -rf -- "${_CLEANUP_PATHS[$i]}" || true
done
exit "$rc"
}
trap _run_cleanups EXIT INT TERM
# -----------------------------------------------------------------------------
# preconditions
# -----------------------------------------------------------------------------
require_cmd() {
local cmd
for cmd in "$@"; do
command -v "$cmd" >/dev/null 2>&1 || die "필수 명령어가 PATH 에 없습니다: $cmd"
done
}
require_env() {
local var
for var in "$@"; do
if [[ -z "${!var:-}" ]]; then
die "필수 환경 변수가 비어있습니다: $var"
fi
done
}
# -----------------------------------------------------------------------------
# destructive gate
# -----------------------------------------------------------------------------
# Usage: confirm "namespace 'mnt' 의 모든 리소스를 삭제합니다. 계속?"
# Returns 0 if the user said yes (interactively or via CONFIRM=yes env).
confirm() {
local prompt="$1"
if [[ "${CONFIRM:-}" == "yes" ]]; then
log "CONFIRM=yes → 자동 진행: $prompt"
return 0
fi
if [[ ! -t 0 ]]; then
die "비대화 환경에서는 CONFIRM=yes 환경 변수를 지정하세요: $prompt"
fi
local answer
read -r -p "$prompt [y/N]: " answer
[[ "$answer" == "y" || "$answer" == "Y" ]]
}
# -----------------------------------------------------------------------------
# secret masking (for logs)
# -----------------------------------------------------------------------------
# Usage: log "root token = $(mask_secret "$ROOT_TOKEN")"
mask_secret() {
local s="$1"
local n=${#s}
if (( n <= 8 )); then
printf '***'
else
printf '%s***%s' "${s:0:4}" "${s: -4}"
fi
}
# -----------------------------------------------------------------------------
# retry helper
# -----------------------------------------------------------------------------
# Usage: retry 5 2 kubectl wait --for=condition=Ready pod/vault-0 -n mnt --timeout=10s
# - $1: max attempts
# - $2: sleep seconds between attempts
# - $3..: command and arguments
retry() {
local attempts="$1"; shift
local delay="$1"; shift
local i=0
until "$@"; do
i=$((i + 1))
if (( i >= attempts )); then
err "최대 시도 횟수 ${attempts} 회 초과: $*"
return 1
fi
warn "실패 ($i/$attempts), ${delay}s 후 재시도: $*"
sleep "$delay"
done
}
# -----------------------------------------------------------------------------
# kube-context / env 타깃 검증
# -----------------------------------------------------------------------------
# require_kube_context <env_name>
#
# env → 기대 context 매핑을 다음 우선순위로 해결한다:
# 1) $KUBE_CONTEXT (명시적으로 주입된 값 — CI 에서 사용)
# 2) $KUBE_CONTEXT_<ENV_UPPER> (env 별 매핑 — 쉘 rc 에 선언하면 편함)
#
# 매칭 실패 시 현재 context 를 출력하고 사용자에게 context 이름을 직접
# 재입력받아 확인한다. 비대화 환경은 die.
#
# 이 함수를 통과하면 다음이 보장된다:
# - kubectl 이 가리키는 cluster 가 env 의 의도된 cluster
# - 사용자/CI 가 그 사실을 명시적으로 인지함 (실수 클러스터 apply 방지)
require_kube_context() {
local env_name="$1"
require_cmd kubectl
local current
current="$(kubectl config current-context 2>/dev/null || true)"
[[ -n "$current" ]] || die "kubectl current-context 가 비어있습니다. kubeconfig 를 먼저 설정하세요."
local env_upper
env_upper="$(printf '%s' "$env_name" | tr '[:lower:]' '[:upper:]')"
local mapped_var="KUBE_CONTEXT_${env_upper}"
local expected="${KUBE_CONTEXT:-${!mapped_var:-}}"
if [[ -n "$expected" ]]; then
if [[ "$current" != "$expected" ]]; then
die "kube-context 불일치: env=$env_name 기대='$expected' 현재='$current' (KUBE_CONTEXT 또는 ${mapped_var} 와 kubectl 현재 context 가 다름)"
fi
log "kube-context OK: env=$env_name context='$current'"
return 0
fi
# 매핑이 없을 때:
# - 비대화(CI) → 무조건 die. CONFIRM=yes 로도 우회 불가 (context 는
# destructive 작업의 타깃이라 명시성이 절대 원칙).
# - 대화형 TTY → 현재 context 이름 재입력으로 확인.
if [[ ! -t 0 ]]; then
die "비대화 환경에서는 KUBE_CONTEXT 또는 ${mapped_var} 가 필수입니다 (CONFIRM=yes 로 우회 불가)."
fi
warn "env=$env_name 의 기대 context 가 지정되지 않았습니다."
warn " (권장) export KUBE_CONTEXT_${env_upper}='<context-name>' 를 쉘 rc 에 선언"
warn "현재 context: $current"
local typed
read -r -p "확인을 위해 현재 context 이름을 그대로 입력하세요 ('$current'): " typed
[[ "$typed" == "$current" ]] || die "context 이름 불일치 — 중단"
}
# require_production_gate <env_name>
#
# env=prod 에서 파괴적 작업을 실행하려면 ALLOW_PROD_DESTRUCTIVE=yes 를 요구.
# 추가로 namespace 이름 재입력을 강제해서 오타 한 번으로 prod 가 날아가는 것을 막는다.
# dev/staging 은 통과.
require_production_gate() {
local env_name="$1"
local ns="$2"
[[ "$env_name" == "prod" ]] || return 0
if [[ "${ALLOW_PROD_DESTRUCTIVE:-}" != "yes" ]]; then
die "env=prod 파괴적 작업은 ALLOW_PROD_DESTRUCTIVE=yes 환경 변수가 필요합니다."
fi
if [[ ! -t 0 ]]; then
die "env=prod 는 대화형 TTY 에서만 실행 가능합니다 (namespace 재입력 확인 필요)."
fi
local typed
warn "env=prod 파괴적 작업 — namespace '$ns' 를 그대로 재입력하세요."
read -r -p "namespace: " typed
[[ "$typed" == "$ns" ]] || die "namespace 재입력 불일치 — 중단"
}
# -----------------------------------------------------------------------------
# namespace / finalizer 정리 헬퍼
# -----------------------------------------------------------------------------
# ns_phase <namespace> — namespace 의 .status.phase 를 출력. 없으면 빈 문자열.
ns_phase() {
local ns="$1"
kubectl get namespace "$ns" -o jsonpath='{.status.phase}' 2>/dev/null || true
}
# ns_exists <namespace> — 존재하면 0, 없으면 1
ns_exists() {
kubectl get namespace "$1" >/dev/null 2>&1
}
# strip_finalizers_in_ns <namespace> <kind...>
# 지정한 kind 들의 모든 인스턴스에서 metadata.finalizers 를 제거한다.
# kind 가 CRD 여도 동작 (kubectl 이 해당 API 서버에 등록되어 있기만 하면).
strip_finalizers_in_ns() {
local ns="$1"; shift
local kind obj
for kind in "$@"; do
while IFS= read -r obj; do
[[ -z "$obj" ]] && continue
kubectl -n "$ns" patch "$obj" --type=merge \
-p '{"metadata":{"finalizers":null}}' >/dev/null 2>&1 || true
log " finalizer 제거: -n $ns $obj"
done < <(kubectl -n "$ns" get "$kind" -o name 2>/dev/null || true)
done
}
# strip_finalizers_all_ns_resources <namespace>
# namespace 에 남아있는 모든 namespaced 리소스의 finalizer 를 일괄 제거.
# 최후 수단 — Terminating 에 걸린 리소스들을 떼어낼 때만 사용.
strip_finalizers_all_ns_resources() {
local ns="$1"
local kinds
# namespaced=true 리소스 종류만
kinds=$(kubectl api-resources --namespaced=true --verbs=delete -o name 2>/dev/null)
local kind
for kind in $kinds; do
while IFS= read -r obj; do
[[ -z "$obj" ]] && continue
kubectl -n "$ns" patch "$obj" --type=merge \
-p '{"metadata":{"finalizers":null}}' >/dev/null 2>&1 || true
done < <(kubectl -n "$ns" get "$kind" -o name 2>/dev/null || true)
done
}
# force_finalize_namespace <namespace>
# namespace 자체의 spec.finalizers 를 비워서 API 서버가 강제 삭제하도록 한다.
# kubectl replace --raw 로 /finalize 엔드포인트 호출.
# 전제: kubectl + jq 존재. 주의 — orphaned PV 등이 남을 수 있음.
force_finalize_namespace() {
local ns="$1"
require_cmd jq
log " namespace $ns 강제 finalize (API /finalize)"
kubectl get namespace "$ns" -o json \
| jq '.spec.finalizers = [] | .metadata.finalizers = []' \
| kubectl replace --raw "/api/v1/namespaces/${ns}/finalize" -f - >/dev/null
}
# wait_namespace_gone <namespace> <timeout_seconds>
# namespace 가 완전히 사라질 때까지 대기. timeout 초과 시 1 반환.
wait_namespace_gone() {
local ns="$1" timeout="${2:-60}" i=0
while ns_exists "$ns"; do
i=$((i + 1))
if (( i >= timeout )); then
return 1
fi
sleep 1
done
return 0
}
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env bash
# Vault helper functions. Depends on lib/common.sh being sourced first.
#
# Environment contract:
# VAULT_NAMESPACE — k8s namespace where vault-0 runs (default: mnt)
# VAULT_POD — Vault pod name (default: vault-0)
# VAULT_KEYS_FILE — path to JSON file produced by vault operator init
# (default: $REPO_ROOT/vault-init-keys.json)
# shellcheck shell=bash
# shellcheck source=./common.sh
# (common.sh must be sourced by the caller.)
: "${VAULT_NAMESPACE:=mnt}"
: "${VAULT_POD:=vault-0}"
# vault_exec <args...> — exec vault CLI inside the Vault pod.
# -i 를 붙여서 호출자의 stdin 을 컨테이너로 파이프 (vault login -no-print - 등).
vault_exec() {
kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- vault "$@"
}
# vault_exec_sh <script> — exec /bin/sh -c "$script" inside the Vault pod.
# Use when the vault CLI call needs shell features (heredocs, redirects, stdin).
vault_exec_sh() {
kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- sh -c "$1"
}
# vault_status_json — echoes the JSON output of `vault status`, or empty on error.
vault_status_json() {
kubectl exec -n "$VAULT_NAMESPACE" "$VAULT_POD" -- \
vault status -format=json 2>/dev/null || true
}
vault_is_initialized() {
local json
json="$(vault_status_json)"
[[ -n "$json" ]] && echo "$json" | jq -e '.initialized == true' >/dev/null
}
vault_is_sealed() {
local json
json="$(vault_status_json)"
[[ -n "$json" ]] && echo "$json" | jq -e '.sealed == true' >/dev/null
}
# vault_unseal_from_keyfile <keys_file>
#
# 주의: `vault operator unseal <KEY>` 형태로 argv 에 키를 넘기면 `ps` 로
# 노출된다 (container 내부 프로세스라도 보안 경계는 유지). vault CLI 는
# argv 가 없으면 stdin 에서 읽으므로 stdin 으로만 전달한다.
vault_unseal_from_keyfile() {
local keys_file="$1"
[[ -f "$keys_file" ]] || die "unseal 키 파일이 없습니다: $keys_file"
local threshold
threshold="$(jq -r '.unseal_threshold' "$keys_file")"
local i key
for ((i = 0; i < threshold; i++)); do
key="$(jq -r ".unseal_keys_b64[$i]" "$keys_file")"
vault_exec operator unseal "$key" >/dev/null
done
unset key
log "Vault unseal 완료 (threshold=${threshold})"
}
# vault_login_root_from_keyfile <keys_file>
# Logs the vault CLI inside the pod using the root token. stdout is suppressed
# so the token never reaches terminals or logs.
vault_login_root_from_keyfile() {
local keys_file="$1"
local token
token="$(jq -r '.root_token' "$keys_file")"
printf '%s' "$token" | vault_exec_sh 'vault login -no-print -' >/dev/null
}
# vault_kv_exists <path> — returns 0 if the KV v2 secret exists.
vault_kv_exists() {
local path="$1"
[[ "$path" == secret/* ]] || path="secret/${path}"
vault_exec kv get -format=json "$path" >/dev/null 2>&1
}
# vault_auth_method_enabled <mount> — returns 0 if the auth method is enabled.
vault_auth_method_enabled() {
local mount="$1"
vault_exec auth list -format=json 2>/dev/null \
| jq -e --arg m "${mount}/" '.[$m] // empty' >/dev/null
}
# vault_secrets_engine_enabled <mount> — returns 0 if the secrets engine is enabled.
vault_secrets_engine_enabled() {
local mount="$1"
vault_exec secrets list -format=json 2>/dev/null \
| jq -e --arg m "${mount}/" '.[$m] // empty' >/dev/null
}
+59
View File
@@ -0,0 +1,59 @@
#!/usr/bin/env bash
# Install (or upgrade) the MinIO Operator Helm release.
#
# MinIO Tenant CR (minio.min.io/v2) 는 이 Operator 가 CRD 를 먼저 등록해야
# apply 가능하다. bootstrap.sh 의 Phase 1 이전에 실행된다.
#
# Operator 는 자체 namespace (기본 minio-operator) 에 배포된다. 애플리케이션
# namespace (mnt) 와 분리되므로 Tenant CR 만 mnt 에 있어도 동작한다.
#
# Optional env:
# MINIO_OPERATOR_NAMESPACE — 기본 minio-operator
# MINIO_OPERATOR_RELEASE — 기본 minio-operator
# MINIO_OPERATOR_VERSION — 기본 7.0.0
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
: "${MINIO_OPERATOR_NAMESPACE:=minio-operator}"
: "${MINIO_OPERATOR_RELEASE:=minio-operator}"
: "${MINIO_OPERATOR_VERSION:=7.0.0}"
helm_install() {
log "helm repo: minio-operator 등록 (idempotent)"
helm repo add minio-operator https://operator.min.io >/dev/null 2>&1 || true
helm repo update minio-operator >/dev/null
log "helm upgrade --install ${MINIO_OPERATOR_RELEASE} (v=${MINIO_OPERATOR_VERSION} ns=${MINIO_OPERATOR_NAMESPACE})"
helm upgrade --install "$MINIO_OPERATOR_RELEASE" minio-operator/operator \
--namespace "$MINIO_OPERATOR_NAMESPACE" \
--create-namespace \
--version "$MINIO_OPERATOR_VERSION" \
--wait \
--atomic \
--timeout 5m
}
wait_ready() {
log "MinIO Operator Pod Ready 확인"
kubectl -n "$MINIO_OPERATOR_NAMESPACE" wait --for=condition=Ready \
pod -l "app.kubernetes.io/name=operator" \
--timeout=120s
log "Tenant CRD 등록 확인"
kubectl get crd tenants.minio.min.io >/dev/null \
|| die "Tenant CRD 가 여전히 없음. Operator 설치 로그 확인."
}
main() {
require_cmd helm kubectl
helm_install
wait_ready
log "minio-operator-install 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+189
View File
@@ -0,0 +1,189 @@
#!/usr/bin/env bash
# MinIO Tenant 기동 후 docker-registry 용 bucket / 서비스 user / policy 를
# 프로비저닝.
#
# 전제:
# - MinIO Tenant Pod 가 Ready (VSO 가 minio-tenant-env Secret 을 이미 동기화)
# - Vault 가 init + unseal 완료
# - vault-seed 가 다음 경로를 seed 완료:
# secret/minio/tenant-env (root 계정, config.env 포맷)
# secret/docker-registry/minio (access_key / secret_key)
#
# 수행 순서:
# 1. Vault 에서 MinIO root 자격증명 + registry 전용 AK/SK 읽기
# 2. kubectl port-forward 로 127.0.0.1 → MinIO Service 터널 생성
# 3. mc alias 등록 → bucket 생성 → policy 작성 → user 생성 → policy 부착
#
# idempotent: 이미 있는 bucket/user/policy 는 건너뜀 (회전은 별도 절차).
#
# Required env:
# REPO_ROOT
#
# Optional env:
# DOCKER_REGISTRY_BUCKET 기본 docker-registry
# DOCKER_REGISTRY_POLICY 기본 docker-registry-rw
# PF_LOCAL_PORT 기본 9900 (local port-forward)
# MINIO_SERVICE_PORT 기본 443 (Service 쪽 포트 — autoCert HTTPS)
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
: "${DOCKER_REGISTRY_BUCKET:=docker-registry}"
: "${DOCKER_REGISTRY_POLICY:=docker-registry-rw}"
: "${PF_LOCAL_PORT:=9900}"
: "${MINIO_SERVICE_PORT:=443}"
: "${MINIO_NAMESPACE:=mnt}"
: "${MINIO_SERVICE:=minio}"
MC_ALIAS="minio-prov"
PF_PID=""
cleanup_port_forward() {
if [[ -n "${PF_PID:-}" ]] && kill -0 "$PF_PID" 2>/dev/null; then
kill "$PF_PID" 2>/dev/null || true
wait "$PF_PID" 2>/dev/null || true
fi
}
wait_minio_ready() {
log "MinIO Tenant Pod Ready 대기"
retry 30 5 kubectl -n "$MINIO_NAMESPACE" wait --for=condition=Ready --timeout=10s \
pod -l "v1.min.io/tenant=$MINIO_SERVICE"
}
start_port_forward() {
log "port-forward 터널 생성 (127.0.0.1:${PF_LOCAL_PORT} → svc/${MINIO_SERVICE}:${MINIO_SERVICE_PORT})"
kubectl -n "$MINIO_NAMESPACE" port-forward \
"svc/${MINIO_SERVICE}" "${PF_LOCAL_PORT}:${MINIO_SERVICE_PORT}" \
>/dev/null 2>&1 &
PF_PID=$!
trap_cleanup_fn cleanup_port_forward
retry 15 1 curl -ks --max-time 2 \
"https://127.0.0.1:${PF_LOCAL_PORT}/minio/health/live" -o /dev/null
log " 터널 응답 OK"
}
read_root_credentials() {
# secret/minio/tenant-env 는 "config.env" 필드 하나에 쉘 export 문 2 줄이
# 들어 있다. 파싱해서 ROOT_USER / ROOT_PASSWORD 전역에 적재.
local env_blob
env_blob="$(vault_exec kv get -format=json secret/minio/tenant-env \
| jq -r '.data.data["config.env"]')"
ROOT_USER="$(printf '%s\n' "$env_blob" \
| sed -n 's/^export MINIO_ROOT_USER="\(.*\)"$/\1/p')"
ROOT_PASSWORD="$(printf '%s\n' "$env_blob" \
| sed -n 's/^export MINIO_ROOT_PASSWORD="\(.*\)"$/\1/p')"
[[ -n "$ROOT_USER" && -n "$ROOT_PASSWORD" ]] \
|| die "MinIO root 자격증명 파싱 실패 — secret/minio/tenant-env 확인"
}
read_registry_credentials() {
local json
json="$(vault_exec kv get -format=json secret/docker-registry/minio)"
REG_AK="$(printf '%s' "$json" | jq -r '.data.data.access_key')"
REG_SK="$(printf '%s' "$json" | jq -r '.data.data.secret_key')"
[[ -n "$REG_AK" && -n "$REG_SK" && "$REG_AK" != "null" && "$REG_SK" != "null" ]] \
|| die "docker-registry 자격증명 읽기 실패 — secret/docker-registry/minio 확인"
}
configure_mc_alias() {
log "mc alias 등록 (alias=${MC_ALIAS})"
mc --insecure alias set "$MC_ALIAS" \
"https://127.0.0.1:${PF_LOCAL_PORT}" \
"$ROOT_USER" "$ROOT_PASSWORD" >/dev/null
}
ensure_bucket() {
log "bucket 보장 (${DOCKER_REGISTRY_BUCKET})"
mc --insecure mb --ignore-existing "${MC_ALIAS}/${DOCKER_REGISTRY_BUCKET}" >/dev/null
}
ensure_policy() {
log "policy 작성 (${DOCKER_REGISTRY_POLICY}) — bucket-scoped read/write"
local policy_file policy_json
policy_file="$(mktemp)"
trap_cleanup_path "$policy_file"
policy_json="$(jq -n --arg b "$DOCKER_REGISTRY_BUCKET" '{
Version: "2012-10-17",
Statement: [
{
Effect: "Allow",
Action: [
"s3:ListBucket",
"s3:ListBucketMultipartUploads",
"s3:GetBucketLocation"
],
Resource: ["arn:aws:s3:::\($b)"]
},
{
Effect: "Allow",
Action: [
"s3:PutObject",
"s3:GetObject",
"s3:DeleteObject",
"s3:ListMultipartUploadParts",
"s3:AbortMultipartUpload"
],
Resource: ["arn:aws:s3:::\($b)/*"]
}
]
}')"
printf '%s' "$policy_json" > "$policy_file"
chmod 600 "$policy_file"
# `policy create` 는 idempotent: 이미 있으면 덮어쓴다.
mc --insecure admin policy create "$MC_ALIAS" \
"$DOCKER_REGISTRY_POLICY" "$policy_file" >/dev/null
}
ensure_user() {
if mc --insecure admin user info "$MC_ALIAS" "$REG_AK" >/dev/null 2>&1; then
log "user '${REG_AK}' 이미 존재 — 재생성 skip (secret key 회전은 별도 절차)"
return 0
fi
log "user '${REG_AK}' 생성"
mc --insecure admin user add "$MC_ALIAS" "$REG_AK" "$REG_SK" >/dev/null
}
attach_policy() {
log "policy '${DOCKER_REGISTRY_POLICY}' → user '${REG_AK}' 부착"
# 이미 부착돼 있으면 mc 가 exit 1 을 반환. 우리는 idempotent 를 의도하므로
# stderr 를 소비해서 noise 만 판별한 뒤 무시한다.
local out rc=0
out="$(mc --insecure admin policy attach "$MC_ALIAS" \
"$DOCKER_REGISTRY_POLICY" --user "$REG_AK" 2>&1)" || rc=$?
if (( rc != 0 )) && ! grep -qi "already" <<<"$out"; then
err " policy attach 실패: $out"
return "$rc"
fi
}
main() {
require_cmd kubectl jq mc curl
require_env REPO_ROOT
local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}"
vault_login_root_from_keyfile "$keys_file"
wait_minio_ready
start_port_forward
read_root_credentials
read_registry_credentials
configure_mc_alias
ensure_bucket
ensure_policy
ensure_user
attach_policy
log "minio-provision-registry 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+166
View File
@@ -0,0 +1,166 @@
#!/usr/bin/env bash
# Initialize Vault (Shamir 5-of-3) and bootstrap the Kubernetes auth method,
# KV v2 secrets engine, and the vault-secrets-operator policy/role.
#
# Idempotent: 이미 초기화된 Vault 에서는 init 을 skip 하지만 unseal 과
# post-init 구성은 매번 재적용해서 bootstrap.sh 에서 여러 번 호출되어도 안전.
#
# Required env:
# REPO_ROOT — repository root
#
# Optional env:
# VAULT_NAMESPACE / VAULT_POD / VAULT_KEYS_FILE — see lib/vault.sh
# ENV_NAME — dev|staging|prod (bootstrap.sh 가 주입). prod 일 때 기본 경로
# (repo working tree) 사용을 차단한다 — VAULT_KEYS_FILE 를 명시
# 해서 repo 바깥의 안전한 경로로 써야 한다.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
KEY_SHARES=5
KEY_THRESHOLD=3
resolve_keys_file() {
DEFAULT_KEYS_FILE="$REPO_ROOT/vault-init-keys.json"
KEYS_FILE="${VAULT_KEYS_FILE:-$DEFAULT_KEYS_FILE}"
# prod 에서 repo 내부 평문 저장 차단 — root token / unseal key 가 repo
# working tree 에 남으면 accidental commit / backup / IDE 인덱싱 경로로 유출.
if [[ "${ENV_NAME:-}" == "prod" && "$KEYS_FILE" == "$DEFAULT_KEYS_FILE" ]]; then
die "env=prod 에서는 VAULT_KEYS_FILE 를 repo 바깥 경로로 반드시 지정해야 합니다. (예: VAULT_KEYS_FILE=/run/secrets/vault-keys.json 또는 sops age 로 암호화)"
fi
}
initialize_if_needed() {
if vault_is_initialized; then
log "Vault 는 이미 초기화되어 있습니다. 초기화 단계를 건너뜁니다."
return 0
fi
log "Vault 초기화 중 (shares=${KEY_SHARES}, threshold=${KEY_THRESHOLD})..."
local tmp_out
tmp_out="$(mktemp)"
trap_cleanup_path "$tmp_out"
vault_exec operator init \
-key-shares="$KEY_SHARES" \
-key-threshold="$KEY_THRESHOLD" \
-format=json > "$tmp_out"
chmod 600 "$tmp_out"
mv "$tmp_out" "$KEYS_FILE"
chmod 600 "$KEYS_FILE"
log "unseal keys + root token 저장: $KEYS_FILE (권한 0600)"
}
unseal_if_needed() {
if vault_is_sealed; then
log "Vault sealed 상태, unseal 진행"
vault_unseal_from_keyfile "$KEYS_FILE"
else
log "Vault unsealed 상태"
fi
}
enable_kv_v2() {
if vault_secrets_engine_enabled "secret"; then
log "secrets engine 'secret/' 이미 활성화됨"
return 0
fi
log "KV v2 secrets engine 을 secret/ 에 활성화"
vault_exec secrets enable -path=secret kv-v2
}
enable_k8s_auth() {
if vault_auth_method_enabled "kubernetes"; then
log "auth method 'kubernetes/' 이미 활성화됨"
else
log "Kubernetes auth method 활성화"
vault_exec auth enable kubernetes
fi
log "Kubernetes auth method 설정 (kubernetes_host + ca + token_reviewer_jwt)"
vault_exec_sh '
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
' >/dev/null
}
write_policies_and_roles() {
log "policy 'vso-auth-platform' 작성 (identity-postgres/*, auth-server/*, keycloak/*, oauth2-proxy/*)"
vault_exec_sh 'cat <<"EOF" | vault policy write vso-auth-platform -
path "secret/data/identity-postgres/*" {
capabilities = ["read"]
}
path "secret/data/auth-server/*" {
capabilities = ["read"]
}
path "secret/data/keycloak/*" {
capabilities = ["read"]
}
path "secret/data/oauth2-proxy/*" {
capabilities = ["read"]
}
EOF
' >/dev/null
log "policy 'vso-storage' 작성 (minio/*, docker-registry/*)"
vault_exec_sh 'cat <<"EOF" | vault policy write vso-storage -
path "secret/data/minio/*" {
capabilities = ["read"]
}
path "secret/data/docker-registry/*" {
capabilities = ["read"]
}
EOF
' >/dev/null
log "k8s auth role 'vso-auth-platform' 작성"
vault_exec write auth/kubernetes/role/vso-auth-platform \
bound_service_account_names=vault-secrets-operator \
bound_service_account_namespaces="$VAULT_NAMESPACE" \
audience=vault \
policies=vso-auth-platform \
ttl=1h >/dev/null
log "k8s auth role 'vso-storage' 작성"
vault_exec write auth/kubernetes/role/vso-storage \
bound_service_account_names=vault-secrets-operator \
bound_service_account_namespaces="$VAULT_NAMESPACE" \
audience=vault \
policies=vso-storage \
ttl=1h >/dev/null
}
cleanup_legacy() {
log "구 policy/role 정리 (존재하면 삭제)"
# 이전 단일 policy (세분화 전) 잔재
vault_exec policy delete vault-secrets-operator >/dev/null 2>&1 || true
vault_exec delete auth/kubernetes/role/vault-secrets-operator >/dev/null 2>&1 || true
# Registry auth 제거로 더 이상 사용 안 하는 policy/role
vault_exec policy delete vso-registry >/dev/null 2>&1 || true
vault_exec delete auth/kubernetes/role/vso-registry >/dev/null 2>&1 || true
}
main() {
require_cmd kubectl jq
require_env REPO_ROOT
resolve_keys_file
initialize_if_needed
unseal_if_needed
vault_login_root_from_keyfile "$KEYS_FILE"
enable_kv_v2
enable_k8s_auth
write_policies_and_roles
cleanup_legacy
log "vault-init 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+286
View File
@@ -0,0 +1,286 @@
#!/usr/bin/env bash
# Seed application secrets into Vault KV v2.
#
# 이미 Vault KV 에 있는 경로는 skip — 운영자가 회전한 값을 절대 덮어쓰지 않는다.
# 아직 없는 경로에 대해 다음 순으로 값을 결정:
# 1) 환경 변수에 값이 있으면 사용
# 2) TTY 면 read -r -s 로 프롬프트 (확인 재입력 + echo 없음, bash history 안전)
# 3) 비대화 + env 비어있고 AUTO_GENERATE=yes 면 openssl rand 로 자동 생성
# → 생성된 값은 stdout 에 절대 출력하지 않음. 필요 시 나중에:
# vault kv get -field=password secret/<path>
#
# Vault CLI 의 `vault kv put <path> -` 모드로 **JSON payload 를 stdin** 으로 전달해
# 비밀번호가 argv / 프로세스 테이블 / 쉘 히스토리 어디에도 노출되지 않는다.
#
# Required env:
# REPO_ROOT
#
# Optional env (비면 대화형 입력 or AUTO_GENERATE):
# POSTGRES_SUPERUSER_USERNAME 기본 postgres
# POSTGRES_SUPERUSER_PASSWORD
#
# KEYCLOAK_DB_PASSWORD
#
# AUTH_SERVER_DB_USERNAME 기본 auth_server
# AUTH_SERVER_DB_PASSWORD
#
# KEYCLOAK_ADMIN_USERNAME 기본 admin
# KEYCLOAK_ADMIN_PASSWORD
#
# MINIO_ROOT_USER 기본 minioadmin
# MINIO_ROOT_PASSWORD
#
# AUTH_SERVER_INGRESS_CLIENT_SECRET Keycloak realm client 'auth-server-ingress' 의 secret.
# KeycloakRealmImport 가 ${AUTH_SERVER_INGRESS_CLIENT_SECRET}
# env 치환으로 가져간다. oauth2-proxy 도 동일 값을 받음.
#
# OAUTH2_PROXY_COOKIE_SECRET oauth2-proxy session cookie signing/encryption key.
#
# DOCKER_REGISTRY_MINIO_ACCESSKEY docker-registry 가 MinIO 에 접근할 때 쓸 AK.
# 기본값 'docker-registry'. MinIO user 이름이 됨.
# DOCKER_REGISTRY_MINIO_SECRETKEY docker-registry MinIO SK (대소문자+숫자 8자 이상).
#
# DOCKER_REGISTRY_PUSH_USERNAME 외부 Ingress push 용 BasicAuth username.
# 기본값 'registry-push'.
# DOCKER_REGISTRY_PUSH_PASSWORD 외부 Ingress push 용 BasicAuth password.
#
# Options:
# AUTO_GENERATE=yes 대화형 TTY 가 아니고 env 도 비었을 때 랜덤 값 생성
#
# NOTE: 비밀번호에 큰따옴표 " 는 사용 금지 (MinIO env-file 포맷이 깨짐).
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
: "${POSTGRES_SUPERUSER_USERNAME:=postgres}"
: "${AUTH_SERVER_DB_USERNAME:=auth_server}"
: "${KEYCLOAK_ADMIN_USERNAME:=admin}"
: "${MINIO_ROOT_USER:=minioadmin}"
: "${DOCKER_REGISTRY_MINIO_ACCESSKEY:=docker-registry}"
: "${DOCKER_REGISTRY_PUSH_USERNAME:=registry-push}"
generate_random_password() {
openssl rand -base64 48 | tr -d '/+=' | head -c 24
}
resolve_password() {
local var_name="$1"
local prompt_label="$2"
local value="${!var_name:-}"
if [[ -n "$value" ]]; then
printf '%s' "$value"
return 0
fi
if [[ -t 0 ]]; then
local input confirm
while true; do
read -r -s -p "${prompt_label}: " input
echo >&2
if [[ -z "$input" ]]; then
err " 비밀번호가 비어있습니다. 다시 입력하세요."
continue
fi
if [[ "$input" == *'"'* ]]; then
err " 큰따옴표(\") 는 사용할 수 없습니다 (MinIO env 포맷)."
continue
fi
read -r -s -p "${prompt_label} 한 번 더: " confirm
echo >&2
if [[ "$input" == "$confirm" ]]; then
printf '%s' "$input"
return 0
fi
err " 일치하지 않습니다. 다시."
done
fi
if [[ "${AUTO_GENERATE:-}" == "yes" ]]; then
local generated
generated="$(generate_random_password)"
warn " ${var_name}: 자동 생성 (vault kv get 으로 조회 가능)"
printf '%s' "$generated"
return 0
fi
die "${var_name} 가 비어있고 비대화 환경이며 AUTO_GENERATE=yes 도 아닙니다."
}
seed_if_missing() {
local path="$1"
if vault_kv_exists "$path"; then
log " secret/${path}: 이미 존재 → skip (회전된 값 보호)"
return 1
fi
return 0
}
kv_put_json() {
local path="$1"; shift
local jq_expr="$1"; shift
jq -n "$@" "$jq_expr" \
| kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- \
vault kv put "secret/${path}" - >/dev/null
}
vault_kv_get_field() {
local path="$1"
local field="$2"
vault_exec kv get -format=json "secret/${path}" \
| jq -r --arg field "$field" '.data.data[$field] // empty'
}
seed_identity_postgres_superuser() {
seed_if_missing "identity-postgres/superuser" || return 0
local pw
pw="$(resolve_password POSTGRES_SUPERUSER_PASSWORD "Postgres superuser 비밀번호")"
kv_put_json "identity-postgres/superuser" \
'{username: $u, password: $p}' \
--arg u "$POSTGRES_SUPERUSER_USERNAME" \
--arg p "$pw"
log " secret/identity-postgres/superuser 작성"
}
seed_keycloak_db() {
seed_if_missing "keycloak/db" || return 0
local pw
pw="$(resolve_password KEYCLOAK_DB_PASSWORD "Keycloak DB 비밀번호")"
kv_put_json "keycloak/db" \
'{password: $p}' \
--arg p "$pw"
log " secret/keycloak/db 작성"
}
seed_auth_server_db() {
seed_if_missing "auth-server/db" || return 0
local pw
pw="$(resolve_password AUTH_SERVER_DB_PASSWORD "auth-server DB 비밀번호")"
kv_put_json "auth-server/db" \
'{SPRING_DATASOURCE_USERNAME: $u, SPRING_DATASOURCE_PASSWORD: $p}' \
--arg u "$AUTH_SERVER_DB_USERNAME" \
--arg p "$pw"
log " secret/auth-server/db 작성"
}
seed_keycloak_bootstrap_admin() {
seed_if_missing "keycloak/bootstrap-admin" || return 0
local pw
pw="$(resolve_password KEYCLOAK_ADMIN_PASSWORD "Keycloak 관리자 비밀번호")"
kv_put_json "keycloak/bootstrap-admin" \
'{KEYCLOAK_ADMIN: $u, KEYCLOAK_ADMIN_PASSWORD: $p}' \
--arg u "$KEYCLOAK_ADMIN_USERNAME" \
--arg p "$pw"
log " secret/keycloak/bootstrap-admin 작성"
}
seed_minio_tenant_env() {
seed_if_missing "minio/tenant-env" || return 0
local pw env_content
pw="$(resolve_password MINIO_ROOT_PASSWORD "MinIO 루트 비밀번호")"
env_content=$(
printf 'export MINIO_ROOT_USER="%s"\nexport MINIO_ROOT_PASSWORD="%s"\n' \
"$MINIO_ROOT_USER" "$pw"
)
kv_put_json "minio/tenant-env" \
'{"config.env": $cfg}' \
--arg cfg "$env_content"
log " secret/minio/tenant-env 작성"
}
# docker-registry 가 MinIO 에 접근할 때 쓸 AK/SK. 이 값으로 bootstrap 후속
# phase 에서 MinIO admin user 를 만들고 bucket-scoped policy 를 부착한다.
# registry Deployment 는 VSO 가 동기화한 Secret `docker-registry-minio` 에서
# 동일 값을 REGISTRY_STORAGE_S3_ACCESSKEY / SECRETKEY 로 읽는다.
seed_docker_registry_minio() {
seed_if_missing "docker-registry/minio" || return 0
local sk
sk="$(resolve_password DOCKER_REGISTRY_MINIO_SECRETKEY "docker-registry MinIO secret key")"
kv_put_json "docker-registry/minio" \
'{access_key: $ak, secret_key: $sk}' \
--arg ak "$DOCKER_REGISTRY_MINIO_ACCESSKEY" \
--arg sk "$sk"
log " secret/docker-registry/minio 작성 (access_key=${DOCKER_REGISTRY_MINIO_ACCESSKEY})"
}
# 외부에서 registry.project.com 으로 push 할 때 Traefik BasicAuth 가 검증할
# htpasswd 라인을 저장한다. Registry 자체 auth 는 켜지지 않고, 외부 Ingress
# 경계에서만 인증한다.
seed_docker_registry_basic_auth() {
seed_if_missing "docker-registry/basic-auth" || return 0
local pw hash users
pw="$(resolve_password DOCKER_REGISTRY_PUSH_PASSWORD "docker-registry 외부 push 비밀번호")"
hash="$(openssl passwd -apr1 "$pw")"
users="${DOCKER_REGISTRY_PUSH_USERNAME}:${hash}"
kv_put_json "docker-registry/basic-auth" \
'{username: $username, password: $password, users: $users}' \
--arg username "$DOCKER_REGISTRY_PUSH_USERNAME" \
--arg password "$pw" \
--arg users "$users"
log " secret/docker-registry/basic-auth 작성 (username=${DOCKER_REGISTRY_PUSH_USERNAME})"
}
# Keycloak realm import 가 client secret 을 ${AUTH_SERVER_INGRESS_CLIENT_SECRET}
# env 치환으로 요구한다. oauth2-proxy 는 동일 값을 /etc/oauth2-proxy-secrets/
# client-secret 파일로 읽는다. 두 쪽이 같은 값을 써야 OIDC client 인증이 맞는다.
seed_keycloak_client_auth_server_ingress() {
seed_if_missing "keycloak/clients/auth-server-ingress" || return 0
local pw
pw="$(resolve_password AUTH_SERVER_INGRESS_CLIENT_SECRET \
"Keycloak client 'auth-server-ingress' secret")"
AUTH_SERVER_INGRESS_CLIENT_SECRET="$pw"
kv_put_json "keycloak/clients/auth-server-ingress" \
'{client_secret: $p}' \
--arg p "$pw"
log " secret/keycloak/clients/auth-server-ingress 작성"
}
seed_oauth2_proxy_forward_auth() {
seed_if_missing "oauth2-proxy/forward-auth" || return 0
local client_secret cookie_secret
if [[ -n "${AUTH_SERVER_INGRESS_CLIENT_SECRET:-}" ]]; then
client_secret="$AUTH_SERVER_INGRESS_CLIENT_SECRET"
elif vault_kv_exists "keycloak/clients/auth-server-ingress"; then
client_secret="$(vault_kv_get_field "keycloak/clients/auth-server-ingress" "client_secret")"
else
client_secret="$(resolve_password AUTH_SERVER_INGRESS_CLIENT_SECRET \
"Keycloak client 'auth-server-ingress' secret")"
fi
[[ -n "$client_secret" ]] || die "auth-server-ingress client secret 을 확인할 수 없습니다."
cookie_secret="$(resolve_password OAUTH2_PROXY_COOKIE_SECRET \
"oauth2-proxy cookie secret")"
kv_put_json "oauth2-proxy/forward-auth" \
'{client_secret: $client_secret, cookie_secret: $cookie_secret}' \
--arg client_secret "$client_secret" \
--arg cookie_secret "$cookie_secret"
log " secret/oauth2-proxy/forward-auth 작성"
}
main() {
require_cmd kubectl jq openssl
require_env REPO_ROOT
local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}"
vault_login_root_from_keyfile "$keys_file"
log "앱 시크릿 5 개 seed 시작 (이미 존재하는 경로는 skip)"
seed_identity_postgres_superuser
seed_keycloak_db
seed_auth_server_db
seed_keycloak_bootstrap_admin
seed_minio_tenant_env
seed_keycloak_client_auth_server_ingress
seed_oauth2_proxy_forward_auth
seed_docker_registry_minio
seed_docker_registry_basic_auth
log "vault-seed-apps 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+180
View File
@@ -0,0 +1,180 @@
#!/usr/bin/env bash
# Configure operator authentication for Vault.
#
# Adds a `userpass` auth method + `vault-admin` policy + one or more admin users.
# Operators use these credentials instead of the root token for day-to-day work.
# The root token stays offline and is only used for break-glass / rekey.
#
# Required env:
# REPO_ROOT — repo root (for vault-init-keys.json path)
#
# Interactive inputs (대화형 TTY 에서 자동 프롬프트):
# VAULT_ADMIN_USERNAME — login name (비면 프롬프트)
# VAULT_ADMIN_PASSWORD — 초기 비밀번호 (비면 무음 입력 프롬프트)
#
# Non-interactive 시 (CI 등): 위 두 개를 env var 로 전달해야 함.
#
# Optional env:
# VAULT_ADMIN_TTL — token TTL per login (default: 8h)
# VAULT_ADMIN_MAX_TTL — maximum TTL (default: 24h)
#
# 입력 검증:
# - username: 영문/숫자/._- 만 허용, 1..64 자
# - TTL/MAX_TTL: 숫자+단위(s|m|h|d), 1..16 자
# 이 값들은 Vault API 경로/파라미터에 쓰이는데, 과거 구현은 `sh -c` 문자열에
# 직접 보간했다. allowlist 를 통과시키면 쉘 메타문자 주입 경로가 닫히고,
# 아래 `vault write` 호출은 `sh -c` 없이 kubectl exec 로 직접 실행한다.
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
# shellcheck source=../lib/vault.sh
. "$SCRIPT_DIR/../lib/vault.sh"
USERNAME_RE='^[A-Za-z0-9._-]{1,64}$'
TTL_RE='^[0-9]+[smhd]?$'
validate_inputs() {
[[ "$VAULT_ADMIN_USERNAME" =~ $USERNAME_RE ]] \
|| die "VAULT_ADMIN_USERNAME 형식 불일치 (영문/숫자/._- 만 허용, 1..64자): '$VAULT_ADMIN_USERNAME'"
[[ "$ADMIN_TTL" =~ $TTL_RE ]] \
|| die "VAULT_ADMIN_TTL 형식 불일치 (예: 8h, 3600, 30m): '$ADMIN_TTL'"
[[ "$ADMIN_MAX_TTL" =~ $TTL_RE ]] \
|| die "VAULT_ADMIN_MAX_TTL 형식 불일치 (예: 24h, 86400): '$ADMIN_MAX_TTL'"
(( ${#ADMIN_TTL} <= 16 && ${#ADMIN_MAX_TTL} <= 16 )) \
|| die "TTL 값이 너무 깁니다."
}
prompt_username_if_missing() {
[[ -n "${VAULT_ADMIN_USERNAME:-}" ]] && return 0
if [[ ! -t 0 ]]; then
die "VAULT_ADMIN_USERNAME 환경 변수가 비어있습니다 (비대화 환경)."
fi
read -r -p "운영자 username (e.g. alice): " VAULT_ADMIN_USERNAME
[[ -n "$VAULT_ADMIN_USERNAME" ]] || die "username 이 비어있습니다."
}
prompt_password_if_missing() {
[[ -n "${VAULT_ADMIN_PASSWORD:-}" ]] && return 0
if [[ ! -t 0 ]]; then
die "VAULT_ADMIN_PASSWORD 환경 변수가 비어있습니다 (비대화 환경)."
fi
read -r -s -p "${VAULT_ADMIN_USERNAME} 초기 비밀번호: " VAULT_ADMIN_PASSWORD
echo >&2
[[ -n "$VAULT_ADMIN_PASSWORD" ]] || die "비밀번호가 비어있습니다."
local confirm
read -r -s -p "비밀번호 한 번 더 입력: " confirm
echo >&2
[[ "$VAULT_ADMIN_PASSWORD" == "$confirm" ]] \
|| die "비밀번호가 일치하지 않습니다."
unset confirm
}
enable_userpass() {
if vault_auth_method_enabled "userpass"; then
log "auth method 'userpass/' 이미 활성화됨"
else
log "userpass auth method 활성화"
vault_exec auth enable userpass
fi
}
write_admin_policy() {
# sudo capability 는 sys/* 일부 엔드포인트 (seal/audit/generate-root) 때문에 필요.
# 권한이 강하므로 인프라 담당자에게만 부여.
log "policy 'vault-admin' 작성"
vault_exec_sh 'cat <<"EOF" | vault policy write vault-admin -
# KV v2 — 전 경로 관리
path "secret/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/data/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "secret/metadata/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# Auth method / identity 관리
path "auth/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "identity/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# policy 관리 (자기 자신 포함)
path "sys/policies/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
path "sys/policy/*" {
capabilities = ["create", "read", "update", "delete", "list"]
}
# 상태 / 감사 장치 / mount 관리
path "sys/health" { capabilities = ["read"] }
path "sys/seal-status" { capabilities = ["read"] }
path "sys/mounts" { capabilities = ["read", "list"] }
path "sys/mounts/*" {
capabilities = ["create", "read", "update", "delete", "list", "sudo"]
}
path "sys/audit" { capabilities = ["read", "list"] }
path "sys/audit/*" {
capabilities = ["create", "read", "update", "delete", "sudo"]
}
EOF
' >/dev/null
}
# 사용자 생성/갱신 — `sh -c` 를 쓰지 않는다.
#
# 비밀번호는 `vault write -` 의 JSON stdin 으로 전달해 argv / 쉘 히스토리 노출이
# 전혀 없다. 나머지 파라미터 (username / TTL) 는 allowlist 를 통과한 값만
# kubectl exec argv 로 직접 전달된다 (쉘 해석 없음).
create_or_update_user() {
log "userpass user '${VAULT_ADMIN_USERNAME}' 생성/갱신 (ttl=${ADMIN_TTL} max_ttl=${ADMIN_MAX_TTL})"
jq -n --arg p "$VAULT_ADMIN_PASSWORD" \
--arg ttl "$ADMIN_TTL" \
--arg max "$ADMIN_MAX_TTL" \
'{password:$p, token_policies:"vault-admin", token_ttl:$ttl, token_max_ttl:$max}' \
| kubectl exec -i -n "$VAULT_NAMESPACE" "$VAULT_POD" -- \
vault write "auth/userpass/users/${VAULT_ADMIN_USERNAME}" - >/dev/null
}
print_next_steps() {
log "vault-setup-admin 태스크 완료"
log ""
log "다음 단계:"
log " 1) 운영자는 다음 명령으로 로그인 (root token 사용 중단)"
log " vault login -method=userpass username=${VAULT_ADMIN_USERNAME}"
log " 2) 각 운영자는 첫 로그인 후 비밀번호를 변경"
log " vault write auth/userpass/users/${VAULT_ADMIN_USERNAME}/password password='<new>'"
log " 3) root token 은 오프라인 금고로 이동 후 vault-init-keys.json 에서 삭제 검토"
log " (재발급은 \"vault operator generate-root -init\" 으로 가능)"
}
main() {
require_cmd kubectl jq
require_env REPO_ROOT
prompt_username_if_missing
prompt_password_if_missing
ADMIN_TTL="${VAULT_ADMIN_TTL:-8h}"
ADMIN_MAX_TTL="${VAULT_ADMIN_MAX_TTL:-24h}"
validate_inputs
local keys_file="${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}"
vault_login_root_from_keyfile "$keys_file"
enable_userpass
write_admin_policy
create_or_update_user
print_next_steps
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi
+102
View File
@@ -0,0 +1,102 @@
#!/usr/bin/env bash
# Install (or upgrade) the vault-secrets-operator Helm release.
#
# Idempotent. 이전 실행에서 failed / pending / uninstalling 상태로 남은 릴리즈
# 메타데이터가 있으면 먼저 uninstall 한 뒤 깨끗하게 재설치한다 (Helm 의
# "no deployed releases" 문제 회피).
#
# `--atomic` 은 설치 실패 시 자동 rollback 을 수행하는데, rollback 결과
# "no deployed releases" 상태가 돼서 다음 helm upgrade --install 이 실패하는
# 연쇄 문제를 낳는다. 이 스크립트는 대신 실패 상태를 명시적으로 감지해
# uninstall 로 cleanup 하므로 --atomic 없이 실행한다.
#
# VSO 는 전용 namespace 에 설치된다 (mnt 에 직접 두면 PSS Restricted 라벨과
# 차트 Pod spec 의 불일치로 Pod 생성이 막힘). Controller 는 cluster-scoped RBAC
# 를 갖고 있어 mnt 의 VaultAuth/VaultStaticSecret 도 정상 watch 한다.
#
# Optional env:
# VSO_NAMESPACE — target namespace (default: vault-secrets-operator-system)
# VSO_RELEASE — helm release name (default: vault-secrets-operator)
# VSO_VERSION — chart version pin (default: 0.9.0)
# VSO_VALUES_FILE — values file path (default: $REPO_ROOT/k8s/base/plugins/vso/helm/values.yaml)
set -Eeuo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# shellcheck source=../lib/common.sh
. "$SCRIPT_DIR/../lib/common.sh"
: "${VSO_NAMESPACE:=vault-secrets-operator-system}"
: "${VSO_RELEASE:=vault-secrets-operator}"
: "${VSO_VERSION:=0.9.0}"
resolve_values_file() {
: "${VSO_VALUES_FILE:=$REPO_ROOT/k8s/base/plugins/vso/helm/values.yaml}"
[[ -f "$VSO_VALUES_FILE" ]] || die "values.yaml 없음: $VSO_VALUES_FILE"
}
register_repo() {
log "helm repo: hashicorp 등록 (idempotent)"
helm repo add hashicorp https://helm.releases.hashicorp.com >/dev/null 2>&1 || true
helm repo update hashicorp >/dev/null
}
# 기존 릴리즈가 dirty 상태 (failed/pending-*/uninstalling/uninstalled) 면 선제
# uninstall 해야 다음 upgrade --install 이 성공한다.
cleanup_dirty_release() {
local status=""
if helm -n "$VSO_NAMESPACE" status "$VSO_RELEASE" -o json >/dev/null 2>&1; then
status="$(helm -n "$VSO_NAMESPACE" status "$VSO_RELEASE" -o json \
| jq -r '.info.status // "unknown"')"
fi
case "$status" in
"")
log "기존 Helm 릴리즈 없음 → 신규 install"
;;
"deployed")
log "기존 릴리즈 상태=deployed → upgrade 진행"
;;
"failed"|"pending-install"|"pending-upgrade"|"pending-rollback"|"uninstalling"|"uninstalled")
warn "기존 릴리즈 상태=$status (dirty) → helm uninstall 먼저"
helm -n "$VSO_NAMESPACE" uninstall "$VSO_RELEASE" --wait --timeout 5m 2>/dev/null \
|| warn " uninstall 중 오류 무시 (이미 부분 정리됐을 수 있음)"
;;
*)
warn "기존 릴리즈 상태=$status (예상 외) — 일단 upgrade 시도"
;;
esac
}
install_or_upgrade() {
log "helm upgrade --install $VSO_RELEASE (v=$VSO_VERSION ns=$VSO_NAMESPACE)"
helm upgrade --install "$VSO_RELEASE" hashicorp/vault-secrets-operator \
--namespace "$VSO_NAMESPACE" \
--create-namespace \
--version "$VSO_VERSION" \
--values "$VSO_VALUES_FILE" \
--wait \
--timeout 5m
}
wait_ready() {
log "VSO Pod Ready 확인"
kubectl -n "$VSO_NAMESPACE" wait --for=condition=Ready \
pod -l "app.kubernetes.io/name=vault-secrets-operator" \
--timeout=120s
}
main() {
require_cmd helm kubectl jq
require_env REPO_ROOT
resolve_values_file
register_repo
cleanup_dirty_release
install_or_upgrade
wait_ready
log "vso-install 태스크 완료"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi