#!/usr/bin/env bash # init_cnpg_gke.sh # Provision CloudNativePG on GKE with GCS backup via Workload Identity. # # Usage: # ./etc/init_cnpg_gke.sh [--project PROJECT_ID] [--region REGION] [--cluster CLUSTER_NAME] # # Env vars (override flags): # GCP_PROJECT_ID, GCP_REGION, GKE_CLUSTER, GCS_BACKUP_BUCKET, # CNPG_NAMESPACE, ARTIFACT_REGISTRY, CLOUDSDK_AUTH_ACCESS_TOKEN set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" GKE_MANIFEST_DIR="$REPO_ROOT/deploy/gcp/gke" # ── Defaults ──────────────────────────────────────────────────────────────── GCP_PROJECT_ID="${GCP_PROJECT_ID:-}" GCP_REGION="${GCP_REGION:-us-central1}" GKE_CLUSTER="${GKE_CLUSTER:-knoe-dev-0}" GCS_BACKUP_BUCKET="${GCS_BACKUP_BUCKET:-knoe-0-backups}" GCS_WAL_BUCKET="${GCS_WAL_BUCKET:-knoe-0-wal}" CNPG_NAMESPACE="${CNPG_NAMESPACE:-ecosystem-0}" CNPG_BACKUP_SA="${CNPG_BACKUP_SA:-cnpg-backup}" ARTIFACT_REGISTRY="${ARTIFACT_REGISTRY:-}" # ── Argument parsing ───────────────────────────────────────────────────────── while [[ $# -gt 0 ]]; do case "$1" in --project) GCP_PROJECT_ID="$2"; shift 2 ;; --region) GCP_REGION="$2"; shift 2 ;; --cluster) GKE_CLUSTER="$2"; shift 2 ;; --bucket) GCS_BACKUP_BUCKET="$2"; shift 2 ;; --namespace) CNPG_NAMESPACE="$2"; shift 2 ;; --registry) ARTIFACT_REGISTRY="$2"; shift 2 ;; *) echo "Unknown argument: $1" >&2; exit 1 ;; esac done [[ -z "$GCP_PROJECT_ID" ]] && { # Try to read from gcp.cfg GCP_CFG="$REPO_ROOT/conf/prod/gcp.cfg" if [[ -f "$GCP_CFG" ]]; then GCP_PROJECT_ID=$(grep '^project_id' "$GCP_CFG" | sed 's/.*=\s*"\?\([^"]*\)"\?.*/\1/' | tr -d '[:space:]') fi } [[ -z "$GCP_PROJECT_ID" ]] && { echo "ERROR: GCP_PROJECT_ID not set and conf/prod/gcp.cfg not found." >&2; exit 1; } GCP_SA_EMAIL="${CNPG_BACKUP_SA}@${GCP_PROJECT_ID}.iam.gserviceaccount.com" log() { echo "[init_cnpg_gke] $*"; } # ── Tool checks ────────────────────────────────────────────────────────────── for tool in gcloud kubectl gsutil; do command -v "$tool" >/dev/null || { echo "ERROR: $tool not found in PATH." >&2; exit 1; } done # ── 1. Acquire kubeconfig ──────────────────────────────────────────────────── log "Fetching kubeconfig for $GKE_CLUSTER in $GCP_REGION ..." gcloud container clusters get-credentials "$GKE_CLUSTER" \ --project "$GCP_PROJECT_ID" \ --region "$GCP_REGION" # ── 2. Create GCS buckets ──────────────────────────────────────────────────── for bucket in "$GCS_BACKUP_BUCKET" "$GCS_WAL_BUCKET"; do if gsutil ls -b "gs://$bucket" >/dev/null 2>&1; then log "Bucket gs://$bucket already exists." else log "Creating bucket gs://$bucket ..." gsutil mb -p "$GCP_PROJECT_ID" -l "$GCP_REGION" "gs://$bucket" gsutil versioning set on "gs://$bucket" gsutil lifecycle set /dev/stdin "gs://$bucket" </dev/null 2>&1; then log "Service account $GCP_SA_EMAIL already exists." else log "Creating service account $GCP_SA_EMAIL ..." gcloud iam service-accounts create "$CNPG_BACKUP_SA" \ --display-name="CNPG GCS Backup" \ --project="$GCP_PROJECT_ID" fi # ── 4. Grant storage access ─────────────────────────────────────────────────── log "Granting objectAdmin on backup buckets to $GCP_SA_EMAIL ..." for bucket in "$GCS_BACKUP_BUCKET" "$GCS_WAL_BUCKET"; do gsutil iam ch \ "serviceAccount:${GCP_SA_EMAIL}:objectAdmin" \ "gs://$bucket" done # ── 5. Bind Workload Identity ───────────────────────────────────────────────── log "Binding Workload Identity ..." gcloud iam service-accounts add-iam-policy-binding "$GCP_SA_EMAIL" \ --role=roles/iam.workloadIdentityUser \ --member="serviceAccount:${GCP_PROJECT_ID}.svc.id.goog[${CNPG_NAMESPACE}/cnpg-backup-sa]" \ --project="$GCP_PROJECT_ID" # ── 6. Apply namespace ──────────────────────────────────────────────────────── log "Applying namespace $CNPG_NAMESPACE ..." kubectl apply -f "$GKE_MANIFEST_DIR/namespace.yaml" # ── 7. Install CNPG operator (if not present) ───────────────────────────────── if ! kubectl get crd clusters.postgresql.cnpg.io >/dev/null 2>&1; then log "Installing CloudNativePG operator ..." kubectl apply --server-side -f \ https://raw.githubusercontent.com/cloudnative-pg/cloudnative-pg/main/releases/cnpg-1.24.0.yaml log "Waiting for CNPG operator to be ready ..." kubectl rollout status deployment/cnpg-controller-manager -n cnpg-system --timeout=120s else log "CNPG operator already installed." fi # ── 8. Apply ServiceAccount + annotate with WI ─────────────────────────────── log "Applying CNPG backup ServiceAccount with Workload Identity annotation ..." GCP_PROJECT_ID="$GCP_PROJECT_ID" envsubst < "$GKE_MANIFEST_DIR/knoe-db-backup-gcs.yaml" \ | kubectl apply -f - kubectl annotate serviceaccount cnpg-backup-sa \ -n "$CNPG_NAMESPACE" \ "iam.gke.io/gcp-service-account=${GCP_SA_EMAIL}" \ --overwrite # ── 9. Apply CNPG cluster ──────────────────────────────────────────────────── log "Applying knoe-db CNPG cluster ..." ARTIFACT_REGISTRY="${ARTIFACT_REGISTRY}" envsubst < "$GKE_MANIFEST_DIR/knoe-db.yaml" \ | kubectl apply -f - log "" log "Done. Monitor with:" log " kubectl -n $CNPG_NAMESPACE get cluster knoe-db" log " kubectl -n $CNPG_NAMESPACE get pods -l cnpg.io/cluster=knoe-db" log " kubectl -n $CNPG_NAMESPACE get scheduledbackup"