#!/usr/bin/env bash # 10-cost-guard.sh — Deploy Cloud Function som lytter på billing Pub/Sub # og utfører auto-teardown + sender webhook-alert til chat-app # ved 100% av budsjett. # # Forutsetter: # - Pub/Sub topic 'billing-alert-auto-teardown' finnes (01-setupenv.sh) # - Secret Manager secret 'webhook-url' inneholder chat-webhook URL # gcloud secrets create webhook-url --data-file=<(echo -n "https://DIN-WEBHOOK-URL") # Source .env før kjøring: source .env set -euo pipefail : "${PROJECT_ID:?Set PROJECT_ID}" : "${REGION:?Set REGION}" : "${CLOUD_RUN_SERVICE:?Set CLOUD_RUN_SERVICE}" FUNCTION_NAME="billing-auto-teardown" PUBSUB_TOPIC="billing-alert-auto-teardown" FUNC_DIR="/tmp/cost-guard-fn" echo "=== 10: Cost Guard — Cloud Function deployment ===" bash "$(dirname "$0")/00-authcheck.sh" gcloud services enable cloudfunctions.googleapis.com pubsub.googleapis.com --project="${PROJECT_ID}" --quiet # --- Skriv Cloud Function kildekode --- mkdir -p "${FUNC_DIR}" cat > "${FUNC_DIR}/main.py" << 'PYEOF' import base64 import json import os import subprocess import urllib.request import functions_framework from google.cloud import secretmanager def get_webhook_url(project_id: str) -> str | None: try: client = secretmanager.SecretManagerServiceClient() name = f"projects/{project_id}/secrets/webhook-url/versions/latest" response = client.access_secret_version(request={"name": name}) return response.payload.data.decode("UTF-8") except Exception: return None def send_webhook(url: str, message: str) -> None: payload = json.dumps({"text": message}).encode() req = urllib.request.Request( url, data=payload, headers={"Content-Type": "application/json"}, method="POST", ) with urllib.request.urlopen(req, timeout=10): pass @functions_framework.cloud_event def billing_alert(cloud_event): project_id = os.environ.get("PROJECT_ID", "") service = os.environ.get("CLOUD_RUN_SERVICE", "") region = os.environ.get("REGION", "") # Dekod Pub/Sub-melding data = base64.b64decode(cloud_event.data["message"]["data"]).decode() alert = json.loads(data) budget_amount = alert.get("budgetAmount", "?") cost_amount = alert.get("costAmount", "?") alert_threshold = alert.get("alertThresholdExceeded", 0) print(f"Billing alert: {cost_amount} / {budget_amount} ({alert_threshold*100:.0f}%)") webhook_url = get_webhook_url(project_id) base_msg = ( f"🚨 *OSVauco Cost Alert*\n" f"Prosjekt: `{project_id}`\n" f"Forbruk: `{cost_amount}` av budsjett `{budget_amount}`\n" f"Terskel: `{alert_threshold*100:.0f}%`" ) # Kjør auto-teardown kun ved 100% terskel if alert_threshold >= 1.0: print("100% terskel nådd — starter auto-teardown") try: subprocess.run( [ "gcloud", "run", "services", "delete", service, f"--region={region}", f"--project={project_id}", "--quiet", ], check=True, capture_output=True, ) teardown_msg = base_msg + "\n\nℹ️ Auto-teardown utført: Cloud Run stoppet." except subprocess.CalledProcessError as e: teardown_msg = base_msg + f"\n\n⚠️ Auto-teardown FEILET: {e.stderr.decode()}" msg = teardown_msg else: msg = base_msg + "\n\n⚠️ Ingen tiltak nå — men pass på forbruket." if webhook_url: try: send_webhook(webhook_url, msg) print("Webhook-alert sendt") except Exception as ex: print(f"Webhook feilet: {ex}") else: print("Ingen webhook-url i Secret Manager — hopper over varsling") PYEOF cat > "${FUNC_DIR}/requirements.txt" << 'EOF' functions-framework==3.* google-cloud-secret-manager>=2.0.0 EOF echo "✓ Cloud Function kildekode skrevet til ${FUNC_DIR}" # --- Deploy Cloud Function --- gcloud functions deploy "${FUNCTION_NAME}" \ --gen2 \ --runtime=python312 \ --region="${REGION}" \ --source="${FUNC_DIR}" \ --entry-point=billing_alert \ --trigger-topic="${PUBSUB_TOPIC}" \ --project="${PROJECT_ID}" \ --set-env-vars="PROJECT_ID=${PROJECT_ID},CLOUD_RUN_SERVICE=${CLOUD_RUN_SERVICE},REGION=${REGION}" \ --service-account="${AGENT_SA:-$(gcloud config get-value account)}" \ --quiet echo "✓ Cloud Function deployed: ${FUNCTION_NAME}" # --- IAM: Cloud Function SA trenger run.admin --- PROJECT_NUMBER=$(gcloud projects describe "${PROJECT_ID}" --format="value(projectNumber)") gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \ --role="roles/run.admin" --quiet gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${PROJECT_NUMBER}-compute@developer.gserviceaccount.com" \ --role="roles/secretmanager.secretAccessor" --quiet echo "✓ IAM: run.admin + secretmanager.secretAccessor gitt til Cloud Function SA" echo "" echo "=== 10: Cost Guard COMPLETE ===" echo " Legg til webhook URL i Secret Manager:" echo " gcloud secrets create webhook-url --data-file=<(echo -n \"https://DIN-WEBHOOK-URL\")" echo " Kompatibel med: Google Chat, Slack, Discord, Teams" echo " Tester: publiser en test-melding på Pub/Sub:" echo " gcloud pubsub topics publish ${PUBSUB_TOPIC} --message='{\"budgetAmount\":75,\"costAmount\":75,\"alertThresholdExceeded\":1.0}'"