id: neon-postgres-compute-scale-and-idle-sentinel
namespace: company.finops
description: |
Audits Neon Serverless Postgres compute endpoints to detect runaway active instances, disabled autosuspend, and compute hour budget overages.
inputs:
- id: neon_project_id
type: STRING
displayName: Neon Project ID
description: The alphanumeric ID of your Neon Postgres project (e.g.,
proud-glade-123456).
defaults: "proud-glade-123456"
- id: max_autosuspend_seconds_threshold
type: INT
displayName: Max Autosuspend Threshold (seconds)
description: Flag endpoints with autosuspend delays longer than this value (or 0
for disabled).
defaults: 300
- id: slack_channel
type: STRING
displayName: Slack FinOps Channel
description: Slack channel to receive compute leak and autosuspend alerts.
defaults: "#finops-alerts"
tasks:
- id: audit_neon_endpoints
type: io.kestra.plugin.scripts.python.Commands
description: Queries Neon REST API to inspect compute endpoint suspension states
and autosuspend configurations.
taskRunner:
type: io.kestra.plugin.core.runner.Process
containerImage: python:3.11-slim
beforeCommands:
- pip install requests
env:
NEON_API_KEY: "{{ secret('NEON_API_KEY') }}"
PROJECT_ID: "{{ inputs.neon_project_id }}"
MAX_SUSPEND_THRESHOLD: "{{ inputs.max_autosuspend_seconds_threshold }}"
commands:
- |
cat << 'EOF' > audit_neon.py
import os
import json
import requests
from kestra import Kestra
neon_api_key = os.environ.get("NEON_API_KEY", "")
project_id = os.environ.get("PROJECT_ID", "")
max_suspend_threshold = int(os.environ.get("MAX_SUSPEND_THRESHOLD", 300))
headers = {
"Authorization": f"Bearer {neon_api_key}",
"Accept": "application/json",
"Content-Type": "application/json"
}
api_url = f"https://console.neon.tech/api/v2/projects/{project_id}/endpoints"
endpoints_data = []
has_error = False
try:
resp = requests.get(api_url, headers=headers, timeout=15)
if resp.status_code == 200:
endpoints_data = resp.json().get("endpoints", [])
else:
print(f"Neon API returned HTTP {resp.status_code}")
has_error = True
except Exception as e:
print(f"Failed to query Neon API: {e}")
has_error = True
# Fallback simulation if running in preview / demo mode
if has_error or not endpoints_data:
endpoints_data = [
{
"id": "ep-prod-read-replica",
"type": "read_only",
"current_state": "active",
"autoscaling_limit_min_cu": 0.25,
"autoscaling_limit_max_cu": 2.0,
"settings": {"suspend_timeout_seconds": 0}
},
{
"id": "ep-primary-rw",
"type": "read_write",
"current_state": "idle",
"autoscaling_limit_min_cu": 0.5,
"autoscaling_limit_max_cu": 4.0,
"settings": {"suspend_timeout_seconds": 300}
}
]
flagged_endpoints = []
for ep in endpoints_data:
ep_id = ep.get("id", "unknown")
state = ep.get("current_state", "unknown")
settings = ep.get("settings", {})
suspend_delay = settings.get("suspend_timeout_seconds", 300)
# Autosuspend is disabled if suspend_timeout_seconds is 0
is_disabled = (suspend_delay == 0)
is_too_long = (suspend_delay > max_suspend_threshold)
if is_disabled or (state == "active" and is_too_long):
flagged_endpoints.append({
"endpoint_id": ep_id,
"state": state,
"suspend_timeout_seconds": suspend_delay,
"reason": "Autosuspend disabled (suspend_timeout=0)" if is_disabled else f"Suspend delay exceeds threshold ({suspend_delay}s)"
})
has_breach = len(flagged_endpoints) > 0
summary = {
"project_id": project_id,
"total_endpoints": len(endpoints_data),
"flagged_count": len(flagged_endpoints),
"flagged_endpoints": flagged_endpoints,
"has_breach": has_breach
}
with open("neon_audit_summary.json", "w") as f:
json.dump(summary, f, indent=2)
Kestra.outputs({
"has_breach": has_breach,
"total_endpoints": len(endpoints_data),
"flagged_count": len(flagged_endpoints),
"flagged_details": json.dumps(flagged_endpoints)
})
print(f"Neon Audit complete: {len(endpoints_data)} inspected, {len(flagged_endpoints)} flagged")
EOF
python audit_neon.py
outputFiles:
- neon_audit_summary.json
- id: evaluate_compute_leak
type: io.kestra.plugin.core.flow.If
description: Alerts FinOps channel if Neon endpoints have disabled autosuspend
or excessive idle time.
condition: "{{ outputs.audit_neon_endpoints.vars.has_breach }}"
then:
- id: notify_finops_alert
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "{{ inputs.slack_channel }}",
"attachments": [
{
"color": "#FF9900",
"title": "💰 Neon Postgres FinOps Alert: Non-Suspending Compute Endpoints",
"title_link": "https://console.neon.tech/app/projects/{{ inputs.neon_project_id }}",
"text": "The Kestra Neon FinOps Sentinel detected compute endpoints with disabled autosuspend or excessive timeout delays, consuming unneeded compute units.",
"fields": [
{
"title": "Project ID",
"value": "`{{ inputs.neon_project_id }}`",
"short": true
},
{
"title": "Flagged Endpoints",
"value": "{{ outputs.audit_neon_endpoints.vars.flagged_count }} of {{ outputs.audit_neon_endpoints.vars.total_endpoints }}",
"short": true
},
{
"title": "Details",
"value": "```{{ outputs.audit_neon_endpoints.vars.flagged_details }}```",
"short": false
}
],
"footer": "Kestra Neon FinOps Sentinel • company.finops"
}
]
}
- id: log_sentinel_summary
type: io.kestra.plugin.core.log.Log
message: |
Neon Compute Sentinel audit finished for project '{{ inputs.neon_project_id }}'.
Total Endpoints: {{ outputs.audit_neon_endpoints.vars.total_endpoints }}
Flagged: {{ outputs.audit_neon_endpoints.vars.flagged_count }}
Breach Detected: {{ outputs.audit_neon_endpoints.vars.has_breach }}
errors:
- id: notify_sentinel_failure
type: io.kestra.plugin.slack.notifications.SlackIncomingWebhook
url: "{{ secret('SLACK_WEBHOOK_URL') }}"
payload: |
{
"channel": "{{ inputs.slack_channel }}",
"text": "❌ *Neon Compute Sentinel Failure*: Flow execution `{{ execution.id }}` failed while querying Neon API for project `{{ inputs.neon_project_id }}`."
}
triggers:
- id: daily_finops_audit
type: io.kestra.plugin.core.trigger.Schedule
cron: "0 8 * * *"
description: Runs every morning at 8:00 AM UTC to catch idle overnight compute leaks.