Ctrl K
ComunidadGuías…

scripts/deploy-azure.sh

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env bash
# =============================================================================
# deploy-azure.sh — Full Wachd enterprise deployment to Azure AKS
#
# What this script does (in order):
#   1.  Checks prerequisites (az, kubectl, helm, terraform)
#   2.  Runs terraform apply to provision AKS, PostgreSQL, Redis, Entra, Key Vault
#   3.  Configures kubectl to point at the new AKS cluster
#   4.  Installs cert-manager (for TLS certificate automation)
#   5.  Installs nginx ingress controller (for external traffic routing)
#   6.  Creates Kubernetes namespace and all required secrets
#   7.  Patches values-azure.yaml with real Terraform outputs
#   8.  Runs helm upgrade --install
#   9.  Waits for pods to become ready
#   10. Prints bootstrap admin credentials and test commands
#
# Usage:
#   chmod +x scripts/deploy-azure.sh
#   ./scripts/deploy-azure.sh
#
# Optional environment variables:
#   WACHD_NAMESPACE      K8s namespace (default: wachd)
#   WACHD_RELEASE        Helm release name (default: wachd)
#   SLACK_WEBHOOK_URL    Slack webhook URL (creates K8s secret)
#   GITHUB_TOKEN         GitHub read-only token (creates K8s secret)
#   CLAUDE_API_KEY       Anthropic API key (creates K8s secret)
#   WACHD_LICENSE_KEY    License key for SMB/Enterprise tier
#   SKIP_TERRAFORM       Set to "true" to skip terraform apply (infra already exists)
#   DRY_RUN              Set to "true" to print commands without running them
# =============================================================================

set -euo pipefail

# ─── Configuration ────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)"
INFRA_DIR="${REPO_ROOT}/infra/azure"
HELM_DIR="${REPO_ROOT}/helm/wachd"
NAMESPACE="${WACHD_NAMESPACE:-wachd}"
RELEASE="${WACHD_RELEASE:-wachd}"
SKIP_TERRAFORM="${SKIP_TERRAFORM:-false}"
DRY_RUN="${DRY_RUN:-false}"

# Cert-manager version — check https://cert-manager.io/docs/releases/ for latest
CERT_MANAGER_VERSION="v1.15.3"
# nginx ingress controller version
NGINX_INGRESS_VERSION="4.11.3"

# ─── Colours ──────────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
BLUE='\033[0;34m'; CYAN='\033[0;36m'; BOLD='\033[1m'; RESET='\033[0m'

log()  { echo -e "${BLUE}[deploy]${RESET} $*"; }
ok()   { echo -e "${GREEN}[  ok  ]${RESET} $*"; }
warn() { echo -e "${YELLOW}[ warn ]${RESET} $*"; }
err()  { echo -e "${RED}[ fail ]${RESET} $*"; exit 1; }
step() { echo -e "\n${BOLD}${CYAN}══ Step $* ══${RESET}"; }
run()  {
  if [[ "${DRY_RUN}" == "true" ]]; then
    echo -e "${YELLOW}[dry-run]${RESET} $*"
  else
    eval "$@"
  fi
}

# ─── Step 1: Prerequisites ────────────────────────────────────────────────────
step "1/10 — Checking prerequisites"

check_cmd() {
  if ! command -v "$1" &>/dev/null; then
    err "Required tool '$1' not found. Install it and re-run.\n  $2"
  fi
  ok "$1 found: $(command -v "$1")"
}

check_cmd az        "https://docs.microsoft.com/en-us/cli/azure/install-azure-cli"
check_cmd kubectl   "https://kubernetes.io/docs/tasks/tools/"
check_cmd helm      "https://helm.sh/docs/intro/install/"
check_cmd jq        "https://stedolan.github.io/jq/download/"

if [[ "${SKIP_TERRAFORM}" != "true" ]]; then
  check_cmd terraform "https://developer.hashicorp.com/terraform/downloads"
fi

# Check Azure login
if ! az account show &>/dev/null; then
  err "Not logged in to Azure. Run: az login"
fi

SUBSCRIPTION_NAME=$(az account show --query name -o tsv)
SUBSCRIPTION_ID=$(az account show --query id -o tsv)
ok "Azure subscription: ${SUBSCRIPTION_NAME} (${SUBSCRIPTION_ID})"

# Check terraform.tfvars exists
if [[ "${SKIP_TERRAFORM}" != "true" ]]; then
  if [[ ! -f "${INFRA_DIR}/terraform.tfvars" ]]; then
    warn "terraform.tfvars not found."
    log "Copy the example and fill in your values:"
    log "  cp ${INFRA_DIR}/terraform.tfvars.example ${INFRA_DIR}/terraform.tfvars"
    log "  vi ${INFRA_DIR}/terraform.tfvars"
    err "Create terraform.tfvars first."
  fi
  ok "terraform.tfvars found"
fi

# ─── Step 2: Terraform ────────────────────────────────────────────────────────
step "2/10 — Provisioning Azure infrastructure with Terraform"

if [[ "${SKIP_TERRAFORM}" == "true" ]]; then
  warn "SKIP_TERRAFORM=true — skipping terraform apply, reading existing state"
else
  cd "${INFRA_DIR}"
  run terraform init -upgrade
  run terraform validate

  log "Planning..."
  run terraform plan -out=tfplan

  echo ""
  echo -e "${BOLD}Review the plan above. Continue with apply? [y/N]${RESET} \c"
  read -r CONFIRM
  [[ "${CONFIRM}" =~ ^[Yy]$ ]] || err "Aborted."

  run terraform apply tfplan
  ok "Terraform apply complete"
fi

# ─── Step 3: Read Terraform outputs ──────────────────────────────────────────
step "3/10 — Reading Terraform outputs"

cd "${INFRA_DIR}"

TF_OUTPUT=$(terraform output -json)

get_output() {
  echo "${TF_OUTPUT}" | jq -r ".$1.value"
}

AKS_CLUSTER_NAME=$(get_output aks_cluster_name)
AKS_RESOURCE_GROUP=$(get_output aks_resource_group)
POSTGRES_HOST=$(get_output postgres_host)
POSTGRES_USER=$(get_output postgres_username)
POSTGRES_PASS=$(get_output postgres_password)
REDIS_HOST=$(get_output redis_host)
REDIS_PORT=$(get_output redis_port_tls)
REDIS_PASS=$(get_output redis_primary_key)
ENTRA_TENANT_ID=$(get_output entra_tenant_id)
ENTRA_CLIENT_ID=$(get_output entra_client_id)
ENTRA_CLIENT_SECRET=$(get_output entra_client_secret)
ENCRYPTION_KEY=$(get_output wachd_encryption_key)
KEY_VAULT_NAME=$(get_output key_vault_name)

log "AKS cluster:   ${AKS_CLUSTER_NAME}"
log "PostgreSQL:    ${POSTGRES_HOST}"
log "Redis:         ${REDIS_HOST}:${REDIS_PORT} (TLS)"
log "Entra tenant:  ${ENTRA_TENANT_ID}"
log "Entra app:     ${ENTRA_CLIENT_ID}"
log "Key Vault:     ${KEY_VAULT_NAME}"

# ─── Step 4: Configure kubectl ────────────────────────────────────────────────
step "4/10 — Configuring kubectl for AKS"

run "az aks get-credentials \
  --resource-group '${AKS_RESOURCE_GROUP}' \
  --name '${AKS_CLUSTER_NAME}' \
  --overwrite-existing"

run "kubectl cluster-info"
ok "kubectl configured for ${AKS_CLUSTER_NAME}"

# ─── Step 5: Install cert-manager ────────────────────────────────────────────
step "5/10 — Installing cert-manager ${CERT_MANAGER_VERSION}"

if kubectl get namespace cert-manager &>/dev/null; then
  ok "cert-manager namespace already exists — skipping install"
else
  run "kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/${CERT_MANAGER_VERSION}/cert-manager.crds.yaml"

  run "helm repo add jetstack https://charts.jetstack.io --force-update"
  run "helm upgrade --install cert-manager jetstack/cert-manager \
    --namespace cert-manager \
    --create-namespace \
    --version ${CERT_MANAGER_VERSION} \
    --set installCRDs=false \
    --wait"

  ok "cert-manager ${CERT_MANAGER_VERSION} installed"
fi

# Create Let's Encrypt ClusterIssuer (prod)
# Replace EMAIL with your email for Let's Encrypt expiry notifications
LETSENCRYPT_EMAIL="${LETSENCRYPT_EMAIL:-admin@mycompany.com}"

# IMPORTANT: Use ingressClassName (not the deprecated class field).
# The deprecated class field causes cert-manager to create solver ingresses
# that the nginx controller ignores — HTTP-01 challenges will always fail.
#
# IMPORTANT: Add ssl-redirect: "false" to the solver ingress template.
# Without this, nginx redirects the ACME challenge HTTP request to HTTPS
# before Let's Encrypt can read the token, causing the challenge to fail.
cat <<EOF | run "kubectl apply -f -"
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: letsencrypt-prod
spec:
  acme:
    server: https://acme-v02.api.letsencrypt.org/directory
    email: ${LETSENCRYPT_EMAIL}
    privateKeySecretRef:
      name: letsencrypt-prod-account-key
    solvers:
      - http01:
          ingress:
            ingressClassName: nginx
            ingressTemplate:
              metadata:
                annotations:
                  nginx.ingress.kubernetes.io/ssl-redirect: "false"
EOF
ok "Let's Encrypt ClusterIssuer created"

# ─── Step 6: Install nginx ingress controller ─────────────────────────────────
step "6/10 — Installing nginx ingress controller ${NGINX_INGRESS_VERSION}"

if kubectl get namespace ingress-nginx &>/dev/null; then
  ok "ingress-nginx namespace already exists — skipping install"
else
  run "helm repo add ingress-nginx https://kubernetes.github.io/ingress-nginx --force-update"
  run "helm upgrade --install ingress-nginx ingress-nginx/ingress-nginx \
    --namespace ingress-nginx \
    --create-namespace \
    --version ${NGINX_INGRESS_VERSION} \
    --set controller.service.annotations.'service\.beta\.kubernetes\.io/azure-load-balancer-health-probe-protocol'=tcp \
    --set controller.service.annotations.'service\.beta\.kubernetes\.io/port_80_health-probe_protocol'=Tcp \
    --set controller.service.annotations.'service\.beta\.kubernetes\.io/port_443_health-probe_protocol'=Tcp \
    --set controller.replicaCount=2 \
    --set controller.nodeSelector.'kubernetes\.io/os'=linux \
    --set defaultBackend.nodeSelector.'kubernetes\.io/os'=linux \
    --wait"

  ok "nginx ingress controller installed"
fi

# Get the public IP assigned to the ingress controller
log "Waiting for ingress LoadBalancer IP..."
for i in $(seq 1 30); do
  INGRESS_IP=$(kubectl get svc ingress-nginx-controller \
    -n ingress-nginx \
    -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true)
  if [[ -n "${INGRESS_IP}" ]]; then
    break
  fi
  sleep 5
done

if [[ -n "${INGRESS_IP}" ]]; then
  ok "Ingress public IP: ${INGRESS_IP}"
  echo ""
  echo -e "${BOLD}${YELLOW}ACTION REQUIRED:${RESET}"
  echo -e "  Create a DNS A record:  wachd.mycompany.com  →  ${INGRESS_IP}"
  echo -e "  Or for quick testing:  wachd.${INGRESS_IP}.nip.io"
  echo ""
else
  warn "Could not determine ingress IP yet — check manually with:"
  warn "  kubectl get svc ingress-nginx-controller -n ingress-nginx"
fi

# ─── Step 7: Create K8s namespace and secrets ─────────────────────────────────
step "7/10 — Creating K8s namespace and secrets in '${NAMESPACE}'"

run "kubectl create namespace '${NAMESPACE}' --dry-run=client -o yaml | kubectl apply -f -"

# Encryption key (required — AES-256 for secrets at rest)
run "kubectl create secret generic wachd-encryption-key \
  --namespace '${NAMESPACE}' \
  --from-literal=encryption-key='${ENCRYPTION_KEY}' \
  --dry-run=client -o yaml | kubectl apply -f -"
ok "Secret: wachd-encryption-key"

# PostgreSQL password
run "kubectl create secret generic wachd-db-secret \
  --namespace '${NAMESPACE}' \
  --from-literal=password='${POSTGRES_PASS}' \
  --dry-run=client -o yaml | kubectl apply -f -"
ok "Secret: wachd-db-secret"

# Redis password
run "kubectl create secret generic wachd-redis-secret \
  --namespace '${NAMESPACE}' \
  --from-literal=password='${REDIS_PASS}' \
  --dry-run=client -o yaml | kubectl apply -f -"
ok "Secret: wachd-redis-secret"

# Entra client secret
run "kubectl create secret generic wachd-entra-secret \
  --namespace '${NAMESPACE}' \
  --from-literal=client-secret='${ENTRA_CLIENT_SECRET}' \
  --dry-run=client -o yaml | kubectl apply -f -"
ok "Secret: wachd-entra-secret"

# Optional: Slack webhook URL
if [[ -n "${SLACK_WEBHOOK_URL:-}" ]]; then
  run "kubectl create secret generic wachd-slack-webhook \
    --namespace '${NAMESPACE}' \
    --from-literal=url='${SLACK_WEBHOOK_URL}' \
    --dry-run=client -o yaml | kubectl apply -f -"
  ok "Secret: wachd-slack-webhook"
else
  warn "SLACK_WEBHOOK_URL not set — Slack notifications disabled. Set and re-run to enable."
fi

# Optional: GitHub token
if [[ -n "${GITHUB_TOKEN:-}" ]]; then
  run "kubectl create secret generic wachd-github-token \
    --namespace '${NAMESPACE}' \
    --from-literal=token='${GITHUB_TOKEN}' \
    --dry-run=client -o yaml | kubectl apply -f -"
  ok "Secret: wachd-github-token"
else
  warn "GITHUB_TOKEN not set — GitHub context collection disabled."
fi

# Optional: Claude API key
if [[ -n "${CLAUDE_API_KEY:-}" ]]; then
  run "kubectl create secret generic wachd-claude-key \
    --namespace '${NAMESPACE}' \
    --from-literal=api-key='${CLAUDE_API_KEY}' \
    --dry-run=client -o yaml | kubectl apply -f -"
  ok "Secret: wachd-claude-key"
else
  warn "CLAUDE_API_KEY not set — AI analysis will use Ollama (local). Set CLAUDE_API_KEY and update analysis.backend=claude in values-azure.yaml."
fi

# Optional: License key
if [[ -n "${WACHD_LICENSE_KEY:-}" ]]; then
  run "kubectl create secret generic wachd-license \
    --namespace '${NAMESPACE}' \
    --from-literal=license-key='${WACHD_LICENSE_KEY}' \
    --dry-run=client -o yaml | kubectl apply -f -"
  ok "Secret: wachd-license"
fi

# ─── Step 8: Patch values-azure.yaml with real values ────────────────────────
step "8/10 — Patching values-azure.yaml with Terraform outputs"

VALUES_AZURE="${HELM_DIR}/values-azure.yaml"
VALUES_PATCHED="${HELM_DIR}/values-azure-patched.yaml"
cp "${VALUES_AZURE}" "${VALUES_PATCHED}"

# Read wachd_hostname from tfvars
WACHD_HOSTNAME=$(grep '^wachd_hostname' "${INFRA_DIR}/terraform.tfvars" | sed 's/.*=\s*"\(.*\)"/\1/')
if [[ -z "${WACHD_HOSTNAME}" ]]; then
  warn "Could not read wachd_hostname from terraform.tfvars — using nip.io with ingress IP"
  WACHD_HOSTNAME="${INGRESS_IP:-UNKNOWN}.nip.io"
fi

sed -i.bak \
  -e "s|<psql-wachd-prod.postgres.database.azure.com>|${POSTGRES_HOST}|g" \
  -e "s|<redis-wachd-prod.redis.cache.windows.net>|${REDIS_HOST}|g" \
  -e "s|6380|${REDIS_PORT}|g" \
  -e "s|<your-tenant-id>|${ENTRA_TENANT_ID}|g" \
  -e "s|<your-client-id>|${ENTRA_CLIENT_ID}|g" \
  -e "s|<wachd.mycompany.com>|${WACHD_HOSTNAME}|g" \
  "${VALUES_PATCHED}"

ok "values-azure-patched.yaml written with real Azure endpoints"

# ─── Step 9: Helm install/upgrade ────────────────────────────────────────────
step "9/10 — Deploying Wachd via Helm"

run "helm upgrade --install '${RELEASE}' '${HELM_DIR}' \
  --namespace '${NAMESPACE}' \
  --create-namespace \
  -f '${HELM_DIR}/values.yaml' \
  -f '${VALUES_PATCHED}'"

ok "Helm release '${RELEASE}' deployed to namespace '${NAMESPACE}'"

# Wait for pods — do NOT use --wait/--atomic; they time out if old pods are in
# CrashLoopBackOff during a rollout. Check pod status manually instead.
log "Waiting for pods to become ready (up to 5 min)..."
run "kubectl rollout status deployment/wachd-server -n '${NAMESPACE}' --timeout=5m || true"
run "kubectl rollout status deployment/wachd-worker -n '${NAMESPACE}' --timeout=5m || true"

# ─── Step 10: Wait for pods + print bootstrap info ───────────────────────────
step "10/10 — Verifying deployment"

log "Pods in namespace ${NAMESPACE}:"
run "kubectl get pods -n '${NAMESPACE}'"

log "Services:"
run "kubectl get svc -n '${NAMESPACE}'"

log "Ingress:"
run "kubectl get ingress -n '${NAMESPACE}'"

# Retrieve bootstrap admin credentials from server logs
log "Retrieving bootstrap admin credentials..."
sleep 5  # Give server time to log credentials

BOOTSTRAP_LOG=$(kubectl logs -n "${NAMESPACE}" -l app.kubernetes.io/component=server \
  --tail=100 --timestamps 2>/dev/null | grep -i "superadmin\|bootstrap\|initial password" || true)

echo ""
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════════${RESET}"
echo -e "${BOLD}${GREEN}  Wachd deployed successfully!${RESET}"
echo -e "${BOLD}${GREEN}══════════════════════════════════════════════════════${RESET}"
echo ""
echo -e "  URL:     ${BOLD}https://${WACHD_HOSTNAME}${RESET}"
echo ""
if [[ -n "${BOOTSTRAP_LOG}" ]]; then
  echo -e "  ${BOLD}Bootstrap credentials from server logs:${RESET}"
  echo "${BOOTSTRAP_LOG}"
else
  echo -e "  ${YELLOW}Get bootstrap credentials from server logs:${RESET}"
  echo -e "    kubectl logs -n ${NAMESPACE} -l app.kubernetes.io/component=server --tail=200 | grep -i superadmin"
fi
echo ""
echo -e "  ${BOLD}Next steps:${RESET}"
echo -e "  1. ${YELLOW}Grant admin consent for GroupMember.Read.All:${RESET}"
echo -e "     $(get_output entra_admin_consent_url)"
echo ""
echo -e "  2. ${YELLOW}Run E2E tests:${RESET}"
echo -e "     WACHD_URL=https://${WACHD_HOSTNAME} ./scripts/test-e2e-azure.sh"
echo ""
echo -e "  3. ${YELLOW}Access admin panel:${RESET}"
echo -e "     https://${WACHD_HOSTNAME}/admin"
echo ""
echo -e "  ${BOLD}Useful commands:${RESET}"
echo -e "    kubectl logs -n ${NAMESPACE} -l app.kubernetes.io/component=server -f"
echo -e "    kubectl logs -n ${NAMESPACE} -l app.kubernetes.io/component=worker -f"
echo -e "    kubectl get pods -n ${NAMESPACE} -w"
echo ""