#!/usr/bin/env bash # Deploy one environment through explicit readiness boundaries. # # Kustomize stages are the deployment contract: # namespaces -> 00-platform -> 10-vault -> 20-secrets # -> 30-data -> MinIO credential provisioning -> 35-registry # -> 40-operations -> 50-apps # # `gitops/clusters//main/all` is deliberately not applied. It exists only for local # render, schema, and policy validation because Kustomize cannot express CRD # establishment, controller readiness, Vault initialization, or one-shot Job # completion. 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)" GITOPS_ROOT="$REPO_ROOT/gitops" TASKS_DIR="$SCRIPT_DIR/../tasks" FIELD_MANAGER="project-infra-bootstrap" NAMESPACE="mnt" export REPO_ROOT 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 oauth2-proxy-secrets minio-tenant-env docker-registry-minio docker-registry-basic-auth docker-registry-pull-credentials ) PRE_DATA_SECRETS=( identity-postgres-superuser keycloak-db keycloak-db-operator auth-server-db keycloak-bootstrap-admin keycloak-bootstrap-admin-operator keycloak-client-auth-server-ingress oauth2-proxy-secrets minio-tenant-env docker-registry-basic-auth docker-registry-pull-credentials ) usage() { cat >&2 <<'EOF' Usage: scripts/bin/bootstrap.sh The selected overlay must exist. The repository currently ships `lab`. Required: KUBE_CONTEXT_ or KUBE_CONTEXT Expected kubectl context Options: VAULT_KEYS_FILE= Vault init result. prod requires a path outside the repo RESET_STALE_SECRETS=yes Delete VSO-managed Kubernetes Secrets before resync ALLOW_PROD_SECRET_RESET=yes Required with RESET_STALE_SECRETS=yes in prod SKIP_DIFF=yes Skip kubectl diff (not recommended) CONFIRM=yes Approve each stage in non-interactive automation EOF exit 1 } render_entrypoint() { local entrypoint="$1" local output="$2" kubectl kustomize "$entrypoint" >"$output" } apply_entrypoint() { local label="$1" local entrypoint="$2" [[ -f "$entrypoint/kustomization.yaml" ]] \ || die "Kustomize entrypoint 없음: $entrypoint" local tmpdir rendered rc=0 tmpdir="$(mktemp -d -t project-infra-bootstrap.XXXXXX)" trap_cleanup_path "$tmpdir" rendered="$tmpdir/rendered.yaml" render_entrypoint "$entrypoint" "$rendered" log "[$label] render 완료: $(grep -c '^kind:' "$rendered") resources" if ! kubectl apply -f "$rendered" \ --dry-run=server \ --server-side \ --field-manager="$FIELD_MANAGER" \ >"$tmpdir/dry-run.out" 2>&1; then err "[$label] server-side dry-run 실패" sed 's/^/ /' "$tmpdir/dry-run.out" >&2 die "$label 검증 실패" fi if [[ "${SKIP_DIFF:-}" == "yes" ]]; then warn "[$label] SKIP_DIFF=yes: diff 생략" else kubectl diff -f "$rendered" \ --server-side \ --field-manager="$FIELD_MANAGER" \ >"$tmpdir/diff.out" 2>&1 || rc=$? case "$rc" in 0) log "[$label] 변경 없음" ;; 1) log "[$label] diff" sed 's/^/ /' "$tmpdir/diff.out" >&2 ;; *) err "[$label] kubectl diff 실패 (exit=$rc)" sed 's/^/ /' "$tmpdir/diff.out" >&2 die "$label diff 실패" ;; esac fi confirm "[$label] env=$ENV_NAME context='$KUBE_CONTEXT_LOCKED' 에 적용합니까?" \ || die "사용자 취소" kubectl apply -f "$rendered" \ --server-side \ --field-manager="$FIELD_MANAGER" } precheck_namespaces() { local phase ns_exists "$NAMESPACE" || return 0 phase="$(ns_phase "$NAMESPACE")" [[ "$phase" != "Terminating" ]] \ || die "namespace '$NAMESPACE' 가 Terminating 상태입니다. teardown 복구를 먼저 완료하세요." } wait_platform_ready() { log "cert-manager CRD / controller 준비 대기" retry 30 2 kubectl wait --for=condition=Established --timeout=10s \ crd/certificates.cert-manager.io \ crd/clusterissuers.cert-manager.io kubectl -n cert-manager rollout status deployment/cert-manager --timeout=300s kubectl -n cert-manager rollout status deployment/cert-manager-webhook --timeout=300s kubectl -n cert-manager rollout status deployment/cert-manager-cainjector --timeout=300s log "Keycloak Operator 준비 대기" retry 30 2 kubectl wait --for=condition=Established --timeout=10s \ crd/keycloaks.k8s.keycloak.org \ crd/keycloakrealmimports.k8s.keycloak.org kubectl -n "$NAMESPACE" rollout status deployment/keycloak-operator --timeout=300s } install_helm_operators() { log "MinIO Operator 설치" ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/minio-operator-install.sh" log "Vault Secrets Operator 설치" ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vso-install.sh" } wait_vault_running() { log "Vault 초기화 전 pod/vault-0 Running 대기" retry 3 10 kubectl -n "$NAMESPACE" wait \ --for=jsonpath='{.status.phase}'=Running \ pod/vault-0 \ --timeout=120s } initialize_and_seed_vault() { log "Vault 초기화 / unseal / Kubernetes auth 구성" ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vault-init.sh" log "애플리케이션 Secret seed" ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/vault-seed-apps.sh" } reset_stale_secrets_if_requested() { [[ "${RESET_STALE_SECRETS:-}" == "yes" ]] || return 0 if [[ "$ENV_NAME" == "prod" && "${ALLOW_PROD_SECRET_RESET:-}" != "yes" ]]; then die "prod Secret 재생성은 ALLOW_PROD_SECRET_RESET=yes 가 추가로 필요합니다." fi warn "RESET_STALE_SECRETS=yes: VSO destination Secret 재생성" local secret_name for secret_name in "${VSO_MANAGED_SECRETS[@]}"; do kubectl -n "$NAMESPACE" delete secret "$secret_name" --ignore-not-found done } pre_data_secrets_ready() { local secret_name for secret_name in "${PRE_DATA_SECRETS[@]}"; do kubectl -n "$NAMESPACE" get secret "$secret_name" >/dev/null 2>&1 || return 1 done } wait_secrets_ready() { log "30-data 선행 VSO destination Secret 생성 대기" retry 90 2 pre_data_secrets_ready } wait_data_ready() { log "PostgreSQL 준비 대기" kubectl -n "$NAMESPACE" rollout status statefulset/identity-postgres --timeout=600s log "MinIO Tenant 준비 대기" retry 24 5 kubectl -n "$NAMESPACE" wait --for=condition=Ready \ pod -l v1.min.io/tenant=minio --timeout=20s log "Keycloak 준비 대기" retry 24 5 kubectl -n "$NAMESPACE" wait --for=condition=Ready \ pod -l app.kubernetes.io/instance=keycloak --timeout=20s } provision_registry() { log "docker-registry용 MinIO bucket / access key 구성" ENV_NAME="$ENV_NAME" bash "$TASKS_DIR/minio-provision-registry.sh" apply_entrypoint "35-registry" "$CLUSTER_ROOT/stages/35-registry" log "docker-registry MinIO credential Secret 생성 대기" retry 90 2 kubectl -n "$NAMESPACE" get secret docker-registry-minio log "docker-registry 준비 대기" kubectl -n "$NAMESPACE" rollout status deployment/docker-registry --timeout=300s } wait_operations_complete() { log "Flyway migration 완료 대기" kubectl -n "$NAMESPACE" wait --for=condition=Complete \ job/auth-server-migrate-0-1-0 --timeout=1800s log "Keycloak realm import 완료 대기" kubectl -n "$NAMESPACE" wait --for=condition=Done \ keycloakrealmimport/platform-realm-v1 --timeout=600s } wait_apps_ready() { kubectl -n "$NAMESPACE" rollout status deployment/auth-server --timeout=600s kubectl -n "$NAMESPACE" rollout status deployment/oauth2-proxy --timeout=300s } summary() { log "============================================" log " $ENV_NAME staged bootstrap 완료" log "============================================" log "Vault init material: ${VAULT_KEYS_FILE:-$REPO_ROOT/vault-init-keys.json}" log "이 파일은 Git에서 제외되지만 평문입니다. 오프라인 보관소로 이동하세요." log "검증: kubectl -n $NAMESPACE get pods,jobs,vaultstaticsecrets" } main() { ENV_NAME="${1:-}" case "$ENV_NAME" in lab | staging | prod) ;; *) usage ;; esac CLUSTER_ROOT="$GITOPS_ROOT/clusters/$ENV_NAME/main" NAMESPACE_ENTRYPOINT="$CLUSTER_ROOT/namespaces" [[ -f "$CLUSTER_ROOT/all/kustomization.yaml" ]] \ || die "클러스터 entrypoint가 없습니다: $CLUSTER_ROOT" require_cmd kubectl helm jq openssl mc curl require_kube_context "$ENV_NAME" precheck_namespaces log "Project-Infra staged bootstrap: env=$ENV_NAME context=$KUBE_CONTEXT_LOCKED" # Namespace first: a server-side dry-run cannot create a Namespace and # validate namespaced resources against it in the same request. apply_entrypoint "namespaces" "$NAMESPACE_ENTRYPOINT" apply_entrypoint "00-platform" "$CLUSTER_ROOT/stages/00-platform" wait_platform_ready install_helm_operators apply_entrypoint "10-vault" "$CLUSTER_ROOT/stages/10-vault" wait_vault_running initialize_and_seed_vault reset_stale_secrets_if_requested apply_entrypoint "20-secrets" "$CLUSTER_ROOT/stages/20-secrets" wait_secrets_ready apply_entrypoint "30-data" "$CLUSTER_ROOT/stages/30-data" wait_data_ready provision_registry apply_entrypoint "40-operations" "$CLUSTER_ROOT/stages/40-operations" wait_operations_complete apply_entrypoint "50-apps" "$CLUSTER_ROOT/stages/50-apps" wait_apps_ready summary } if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then main "$@" fi