Files
project-infra/k8s/scripts/bin/bootstrap.sh
T

347 lines
13 KiB
Bash
Executable File
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/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