feat: add scripts/cost-audit.sh — billable resource snapshot
This commit is contained in:
parent
7a6a75983b
commit
8fa078d61d
50
scripts/cost-alerts.sh
Normal file
50
scripts/cost-alerts.sh
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# cost-alerts.sh — Nightly check: exits non-zero + prints red warning
|
||||
# if any always-on resources are found.
|
||||
# Designed for cron or quick end-of-day terminal check.
|
||||
# Usage: source .env && bash scripts/cost-alerts.sh
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
: "${PROJECT_ID:?Set PROJECT_ID}"
|
||||
: "${REGION:?Set REGION}"
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
NC='\033[0m'
|
||||
|
||||
RISK=0
|
||||
|
||||
check() {
|
||||
local LABEL="$1"; local CMD="$2"
|
||||
RESULT=$(eval "$CMD" 2>/dev/null || true)
|
||||
if [[ -n "$RESULT" ]]; then
|
||||
printf "${RED}[!!] %-22s ACTIVE: %s${NC}\n" "$LABEL" "$(echo "$RESULT" | head -3 | tr '\n' ' ')"
|
||||
RISK=1
|
||||
fi
|
||||
}
|
||||
|
||||
printf "\n OSVauco Cost Alert Check — $(date '+%Y-%m-%d %H:%M')\n\n"
|
||||
|
||||
check "Compute VMs" \
|
||||
"gcloud compute instances list --project=${PROJECT_ID} --format='value(name,status)' | grep RUNNING"
|
||||
|
||||
check "Cloud SQL" \
|
||||
"gcloud sql instances list --project=${PROJECT_ID} --format='value(name,state)' | grep RUNNABLE"
|
||||
|
||||
check "Composer" \
|
||||
"gcloud composer environments list --project=${PROJECT_ID} --locations=${REGION} --format='value(name)'"
|
||||
|
||||
check "Memory Bank" \
|
||||
"gcloud ai reasoning-engines list --project=${PROJECT_ID} --region=${REGION} --format='value(name)'"
|
||||
|
||||
check "Cloud Run min>0" \
|
||||
"gcloud run services list --project=${PROJECT_ID} --region=${REGION} --format='value(name)' | xargs -I{} gcloud run services describe {} --project=${PROJECT_ID} --region=${REGION} --format='value(spec.template.metadata.annotations.autoscaling\.knative\.dev/minScale)' 2>/dev/null | grep -v '^$' | grep -v '^0$'"
|
||||
|
||||
if [[ $RISK -eq 0 ]]; then
|
||||
printf " ${GREEN}[OK] No always-on resources detected. Sleep well.${NC}\n\n"
|
||||
else
|
||||
printf "\n ${RED}Run: bash scripts/cost-audit.sh for full details.${NC}\n\n"
|
||||
exit 1
|
||||
fi
|
||||
196
scripts/cost-audit.sh
Normal file
196
scripts/cost-audit.sh
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# cost-audit.sh — OSVauco billable resource snapshot
|
||||
# Read-only, safe to run anytime.
|
||||
# Usage: source .env && bash scripts/cost-audit.sh
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
: "${PROJECT_ID:?Set PROJECT_ID}"
|
||||
: "${REGION:?Set REGION}"
|
||||
|
||||
GREEN='\033[0;32m'
|
||||
RED='\033[0;31m'
|
||||
YELLOW='\033[1;33m'
|
||||
CYAN='\033[0;36m'
|
||||
DIM='\033[2m'
|
||||
BOLD='\033[1m'
|
||||
NC='\033[0m'
|
||||
|
||||
OK="${GREEN}[OK]${NC}"
|
||||
WARN="${YELLOW}[!!]${NC}"
|
||||
FAIL="${RED}[XX]${NC}"
|
||||
DOT="${DIM}[--]${NC}"
|
||||
|
||||
clear
|
||||
printf "${CYAN}${BOLD}"
|
||||
echo " -------------------------------------------------------"
|
||||
echo " OSVauco COST AUDIT"
|
||||
printf " -------------------------------------------------------${NC}\n"
|
||||
printf " ${DIM}Project : ${PROJECT_ID} | Region : ${REGION}${NC}\n"
|
||||
printf " ${DIM}$(date '+%Y-%m-%d %H:%M %Z')${NC}\n"
|
||||
echo ""
|
||||
|
||||
ANY_RISK=false
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 1. CLOUD RUN
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ CLOUD RUN ]${NC} ${DIM}(scale-to-zero = no idle cost)${NC}\n"
|
||||
CR=$(gcloud run services list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--region="${REGION}" \
|
||||
--format="value(name,status.url)" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$CR" ]]; then
|
||||
printf " ${OK} No services deployed\n"
|
||||
else
|
||||
while IFS=$'\t' read -r NAME URL; do
|
||||
# Check min-instances annotation
|
||||
MIN=$(gcloud run services describe "$NAME" \
|
||||
--project="${PROJECT_ID}" --region="${REGION}" \
|
||||
--format="value(spec.template.metadata.annotations.'autoscaling.knative.dev/minScale')" \
|
||||
2>/dev/null || echo "0")
|
||||
MIN=${MIN:-0}
|
||||
if [[ "$MIN" -gt 0 ]]; then
|
||||
printf " ${WARN} %-30s ${YELLOW}min-instances=%s (idle cost!)${NC}\n" "$NAME" "$MIN"
|
||||
ANY_RISK=true
|
||||
else
|
||||
printf " ${OK} %-30s ${DIM}scale-to-zero${NC}\n" "$NAME"
|
||||
fi
|
||||
done <<< "$CR"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 2. COMPUTE ENGINE
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ COMPUTE ENGINE ]${NC} ${DIM}(VMs always-on = always costs)${NC}\n"
|
||||
VMS=$(gcloud compute instances list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--format="value(name,zone,status,machineType.basename())" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$VMS" ]]; then
|
||||
printf " ${OK} No VM instances\n"
|
||||
else
|
||||
ANY_RISK=true
|
||||
while IFS=$'\t' read -r NAME ZONE STATUS MTYPE; do
|
||||
if [[ "$STATUS" == "RUNNING" ]]; then
|
||||
printf " ${FAIL} %-24s ${RED}RUNNING${NC} zone:%-20s type:%s\n" "$NAME" "$ZONE" "$MTYPE"
|
||||
else
|
||||
printf " ${WARN} %-24s ${YELLOW}%-8s${NC} zone:%-20s type:%s\n" "$NAME" "$STATUS" "$ZONE" "$MTYPE"
|
||||
fi
|
||||
done <<< "$VMS"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 3. CLOUD SQL
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ CLOUD SQL ]${NC} ${DIM}(instances always-on when RUNNABLE)${NC}\n"
|
||||
SQL=$(gcloud sql instances list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--format="value(name,region,databaseVersion,settings.tier,state)" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$SQL" ]]; then
|
||||
printf " ${OK} No Cloud SQL instances\n"
|
||||
else
|
||||
ANY_RISK=true
|
||||
while IFS=$'\t' read -r NAME SQLREGION DBVER TIER STATE; do
|
||||
if [[ "$STATE" == "RUNNABLE" ]]; then
|
||||
printf " ${FAIL} %-22s ${RED}RUNNABLE${NC} tier:%-14s db:%s\n" "$NAME" "$TIER" "$DBVER"
|
||||
else
|
||||
printf " ${WARN} %-22s ${YELLOW}%-10s${NC} tier:%-14s db:%s\n" "$NAME" "$STATE" "$TIER" "$DBVER"
|
||||
fi
|
||||
done <<< "$SQL"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 4. CLOUD COMPOSER
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ CLOUD COMPOSER ]${NC} ${DIM}(managed Airflow = very expensive)${NC}\n"
|
||||
COMP=$(gcloud composer environments list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--locations="${REGION}" \
|
||||
--format="value(name,location,state)" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$COMP" ]]; then
|
||||
printf " ${OK} No Composer environments\n"
|
||||
else
|
||||
ANY_RISK=true
|
||||
while IFS=$'\t' read -r NAME LOC STATE; do
|
||||
printf " ${FAIL} %-24s ${RED}%-10s${NC} loc:%s\n" "$NAME" "$STATE" "$LOC"
|
||||
done <<< "$COMP"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 5. MEMORY BANK / REASONING ENGINES
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ MEMORY BANK ]${NC} ${DIM}(\$0.0994/vCPU-hr continuous billing)${NC}\n"
|
||||
ENGINES=$(gcloud ai reasoning-engines list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--region="${REGION}" \
|
||||
--format="value(name,displayName)" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$ENGINES" ]]; then
|
||||
printf " ${OK} No active Memory Bank instances\n"
|
||||
else
|
||||
ANY_RISK=true
|
||||
ENGINE_COUNT=$(echo "$ENGINES" | grep -c . || true)
|
||||
printf " ${FAIL} ${RED}${BOLD}${ENGINE_COUNT} INSTANCE(S) RUNNING — DELETE WHEN NOT NEEDED${NC}\n"
|
||||
while IFS=$'\t' read -r ENAME EDISPLAY; do
|
||||
SHORT=$(echo "$ENAME" | awk -F'/' '{print $NF}')
|
||||
printf " ${WARN} %-22s ${DIM}id:%s${NC}\n" "${EDISPLAY:-unnamed}" "$SHORT"
|
||||
printf " ${DIM} Delete: gcloud ai reasoning-engines delete %s --project=%s --region=%s --quiet${NC}\n" "$SHORT" "${PROJECT_ID}" "${REGION}"
|
||||
done <<< "$ENGINES"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 6. FIRESTORE
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ FIRESTORE ]${NC}\n"
|
||||
gcloud firestore databases list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--format="table[no-heading](name,type,locationId,freeTier)" 2>/dev/null | while read -r LINE; do
|
||||
if echo "$LINE" | grep -q "True\|true"; then
|
||||
printf " ${OK} %-40s ${DIM}(free tier)${NC}\n" "$LINE"
|
||||
else
|
||||
printf " ${WARN} %-40s ${YELLOW}(paid tier — watch usage)${NC}\n" "$LINE"
|
||||
fi
|
||||
done || printf " ${DOT} No Firestore databases\n"
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# 7. ARTIFACT REGISTRY
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${BOLD}${CYAN} [ ARTIFACT REGISTRY ]${NC} ${DIM}(storage cost per GB)${NC}\n"
|
||||
REPOS=$(gcloud artifacts repositories list \
|
||||
--project="${PROJECT_ID}" \
|
||||
--location="${REGION}" \
|
||||
--format="value(name.basename(),format,sizeBytes)" 2>/dev/null || true)
|
||||
|
||||
if [[ -z "$REPOS" ]]; then
|
||||
printf " ${OK} No Artifact Registry repos\n"
|
||||
else
|
||||
while IFS=$'\t' read -r NAME FMT SIZE; do
|
||||
SIZE_MB=$(( ${SIZE:-0} / 1048576 ))
|
||||
printf " ${OK} %-28s fmt:%-8s size:${SIZE_MB}MB\n" "$NAME" "$FMT"
|
||||
done <<< "$REPOS"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# SUMMARY
|
||||
# -----------------------------------------------------------------------------
|
||||
printf "${DIM} -------------------------------------------------------${NC}\n"
|
||||
if [[ "$ANY_RISK" == true ]]; then
|
||||
printf " ${WARN} ${YELLOW}${BOLD}ATTENTION: Billable resources detected above — review!${NC}\n"
|
||||
else
|
||||
printf " ${OK} ${GREEN}${BOLD}All clear — no unexpected idle resources detected.${NC}\n"
|
||||
fi
|
||||
printf " ${DIM}Full billing: https://console.cloud.google.com/billing?project=${PROJECT_ID}${NC}\n"
|
||||
printf "${DIM} -------------------------------------------------------${NC}\n"
|
||||
echo ""
|
||||
Loading…
Reference in New Issue
Block a user