refactor: 폴더 구조 변경
This commit is contained in:
Executable
+305
@@ -0,0 +1,305 @@
|
||||
#!/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/<env>/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 <lab|staging|prod>
|
||||
|
||||
The selected overlay must exist. The repository currently ships `lab`.
|
||||
|
||||
Required:
|
||||
KUBE_CONTEXT_<ENV> or KUBE_CONTEXT Expected kubectl context
|
||||
|
||||
Options:
|
||||
VAULT_KEYS_FILE=<path> 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
|
||||
Executable
+157
@@ -0,0 +1,157 @@
|
||||
#!/usr/bin/env bash
|
||||
# Remove an environment in reverse dependency order.
|
||||
#
|
||||
# Default scope removes only application and one-shot operation resources.
|
||||
# Stateful data, Vault, namespaces, CRDs, and shared operators require explicit
|
||||
# opt-in flags.
|
||||
|
||||
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"
|
||||
NAMESPACE="mnt"
|
||||
VSO_NAMESPACE="${VSO_NAMESPACE:-vault-secrets-operator-system}"
|
||||
VSO_RELEASE="${VSO_RELEASE:-vault-secrets-operator}"
|
||||
MINIO_OPERATOR_NAMESPACE="${MINIO_OPERATOR_NAMESPACE:-minio-operator}"
|
||||
MINIO_OPERATOR_RELEASE="${MINIO_OPERATOR_RELEASE:-minio-operator}"
|
||||
NAMESPACE_DELETE_INCOMPLETE=0
|
||||
|
||||
usage() {
|
||||
cat >&2 <<'EOF'
|
||||
Usage: scripts/bin/teardown.sh <lab|staging|prod>
|
||||
|
||||
Default:
|
||||
Remove stages 50-apps and 40-operations only.
|
||||
|
||||
Options:
|
||||
DELETE_DATA=yes Also remove data, VSO CRs, Vault, and app namespace
|
||||
TEARDOWN_PLATFORM=yes Also remove shared Helm operators and stage 00
|
||||
(requires DELETE_DATA=yes)
|
||||
FORCE_FINALIZERS=yes Last-resort namespace finalizer removal
|
||||
(requires DELETE_DATA=yes)
|
||||
ALLOW_PROD_DESTRUCTIVE=yes Required for prod
|
||||
CONFIRM=yes Non-interactive confirmation
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
delete_entrypoint() {
|
||||
local label="$1"
|
||||
local entrypoint="$2"
|
||||
[[ -f "$entrypoint/kustomization.yaml" ]] || {
|
||||
log "[$label] entrypoint 없음: skip"
|
||||
return 0
|
||||
}
|
||||
|
||||
log "[$label] delete"
|
||||
kubectl delete -k "$entrypoint" \
|
||||
--ignore-not-found \
|
||||
--wait=true \
|
||||
--timeout=300s
|
||||
}
|
||||
|
||||
delete_application_scope() {
|
||||
delete_entrypoint "50-apps" "$CLUSTER_ROOT/stages/50-apps"
|
||||
delete_entrypoint "40-operations" "$CLUSTER_ROOT/stages/40-operations"
|
||||
}
|
||||
|
||||
delete_data_scope() {
|
||||
[[ "${DELETE_DATA:-}" == "yes" ]] || {
|
||||
log "DELETE_DATA 미지정: data / secrets / Vault / namespace 보존"
|
||||
return 0
|
||||
}
|
||||
|
||||
delete_entrypoint "35-registry" "$CLUSTER_ROOT/stages/35-registry"
|
||||
delete_entrypoint "30-data" "$CLUSTER_ROOT/stages/30-data"
|
||||
delete_entrypoint "20-secrets" "$CLUSTER_ROOT/stages/20-secrets"
|
||||
delete_entrypoint "10-vault" "$CLUSTER_ROOT/stages/10-vault"
|
||||
|
||||
log "namespace/$NAMESPACE 삭제"
|
||||
if ! kubectl delete namespace "$NAMESPACE" \
|
||||
--ignore-not-found \
|
||||
--wait=true \
|
||||
--timeout=300s; then
|
||||
warn "namespace 삭제가 제한 시간 안에 완료되지 않았습니다."
|
||||
NAMESPACE_DELETE_INCOMPLETE=1
|
||||
fi
|
||||
}
|
||||
|
||||
force_finalize_if_requested() {
|
||||
[[ "${FORCE_FINALIZERS:-}" == "yes" ]] || return 0
|
||||
[[ "${DELETE_DATA:-}" == "yes" ]] \
|
||||
|| die "FORCE_FINALIZERS=yes 는 DELETE_DATA=yes 와 함께만 사용할 수 있습니다."
|
||||
ns_exists "$NAMESPACE" || return 0
|
||||
|
||||
warn "FORCE_FINALIZERS=yes: orphaned volume/controller state가 생길 수 있습니다."
|
||||
confirm "namespace/$NAMESPACE finalizer를 강제로 제거합니까?" || die "사용자 취소"
|
||||
strip_finalizers_all_ns_resources "$NAMESPACE"
|
||||
force_finalize_namespace "$NAMESPACE"
|
||||
wait_namespace_gone "$NAMESPACE" 120 \
|
||||
|| die "namespace/$NAMESPACE 강제 finalize 후에도 남아 있습니다."
|
||||
NAMESPACE_DELETE_INCOMPLETE=0
|
||||
}
|
||||
|
||||
delete_platform_scope() {
|
||||
[[ "${TEARDOWN_PLATFORM:-}" == "yes" ]] || {
|
||||
log "TEARDOWN_PLATFORM 미지정: 공유 operator / CRD / stage 00 보존"
|
||||
return 0
|
||||
}
|
||||
[[ "${DELETE_DATA:-}" == "yes" ]] \
|
||||
|| die "TEARDOWN_PLATFORM=yes 는 DELETE_DATA=yes 와 함께만 사용할 수 있습니다."
|
||||
|
||||
warn "공유 controller와 cluster-scoped 리소스를 제거합니다."
|
||||
confirm "이 클러스터가 다른 환경과 공유되지 않음을 확인했습니까?" || die "사용자 취소"
|
||||
|
||||
helm -n "$VSO_NAMESPACE" uninstall "$VSO_RELEASE" --wait --timeout=5m \
|
||||
2>/dev/null || warn "VSO Helm release가 없거나 uninstall에 실패했습니다."
|
||||
helm -n "$MINIO_OPERATOR_NAMESPACE" uninstall "$MINIO_OPERATOR_RELEASE" --wait --timeout=5m \
|
||||
2>/dev/null || warn "MinIO Operator Helm release가 없거나 uninstall에 실패했습니다."
|
||||
|
||||
delete_entrypoint "00-platform" "$CLUSTER_ROOT/stages/00-platform"
|
||||
kubectl delete namespace "$VSO_NAMESPACE" "$MINIO_OPERATOR_NAMESPACE" \
|
||||
--ignore-not-found \
|
||||
--wait=true \
|
||||
--timeout=300s \
|
||||
|| warn "operator namespace 삭제가 완료되지 않았습니다."
|
||||
}
|
||||
|
||||
main() {
|
||||
ENV_NAME="${1:-}"
|
||||
case "$ENV_NAME" in
|
||||
lab | staging | prod) ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
CLUSTER_ROOT="$GITOPS_ROOT/clusters/$ENV_NAME/main"
|
||||
[[ -f "$CLUSTER_ROOT/all/kustomization.yaml" ]] \
|
||||
|| die "클러스터 entrypoint가 없습니다: $CLUSTER_ROOT"
|
||||
|
||||
require_cmd kubectl helm jq
|
||||
require_kube_context "$ENV_NAME"
|
||||
require_production_gate "$ENV_NAME" "$NAMESPACE"
|
||||
|
||||
local scope="apps + one-shot operations"
|
||||
[[ "${DELETE_DATA:-}" == "yes" ]] && scope="$scope + data + Vault + namespace"
|
||||
[[ "${TEARDOWN_PLATFORM:-}" == "yes" ]] && scope="$scope + shared platform"
|
||||
|
||||
log "Project-Infra teardown: env=$ENV_NAME context=$KUBE_CONTEXT_LOCKED"
|
||||
confirm "삭제 범위: $scope. 계속합니까?" || die "사용자 취소"
|
||||
|
||||
delete_application_scope
|
||||
delete_data_scope
|
||||
force_finalize_if_requested
|
||||
delete_platform_scope
|
||||
|
||||
if ((NAMESPACE_DELETE_INCOMPLETE == 1)) && ns_exists "$NAMESPACE"; then
|
||||
die "$ENV_NAME teardown 미완료: namespace/$NAMESPACE 가 남아 있습니다."
|
||||
fi
|
||||
|
||||
log "$ENV_NAME teardown 완료: $scope"
|
||||
}
|
||||
|
||||
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
|
||||
main "$@"
|
||||
fi
|
||||
Reference in New Issue
Block a user