Use when working with Gcp — cost anti-hallucination rules, MANDATORY parallel execution patterns (30x speedup), monitoring aligners, reusable billing/pricing scripts, VAT/tax handling, and filtering/pagination.
Scanned 9/8/2026
Install to Claude Code
npx -y skills add cloudthinker-ai/CloudSkills --skill gcp --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Gcp?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/cloudthinker-ai-gcp)More formats (shields.io, HTML) on the badges page.
---
name: gcp
description: |
Use when working with Gcp — cost anti-hallucination rules, MANDATORY parallel
execution patterns (30x speedup), monitoring aligners, reusable
billing/pricing scripts, VAT/tax handling, and filtering/pagination.
connection_type: gcp
preload: false
---
# GCP CLI Skill
Execute GCP CLI commands with proper credential injection.
## CRITICAL: Billing Data Interpretation Rules (Anti-Hallucination)
**These rules are MANDATORY when analyzing GCP billing data. Violating them produces wildly incorrect cost reports.**
### Rule 1: NET COST is the Only Real Cost
The `cost` column in billing exports is **NOT** your actual bill. It shows usage priced at contract/on-demand rates **before credits are applied**. Credits (promotional, SUDs, CUDs, free tier) are stored separately in the `credits` array with **negative** amounts.
```
-- Pre-tax net cost (filter cost_type = 'regular'):
NET COST = SUM(cost WHERE cost_type='regular') + SUM(credits.amount)
-- Tax-inclusive net cost (include all cost_types):
NET COST WITH TAX = SUM(cost) + SUM(credits.amount)
```
**NEVER report `SUM(cost)` alone as the cost.** Always compute net cost. In actual SQL, always use `CAST(... AS NUMERIC)` — see Rule 8.
### Rule 2: ALWAYS Filter by project.id
The billing export table is at the **billing account level** and contains costs for **ALL projects** under that billing account. If you query without filtering by `project.id`, you aggregate costs across 10+ unrelated projects.
```sql
-- WRONG: Aggregates ALL projects in the billing account
SELECT service.description, SUM(cost) FROM `{BILLING_TABLE}` GROUP BY 1
-- CORRECT: Scoped to the target project with net cost
SELECT service.description,
SUM(CAST(cost AS NUMERIC))
+ SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) c), 0)) AS net_cost
FROM `{BILLING_TABLE}`
WHERE project.id = '{PROJECT_ID}'
GROUP BY 1
```
### Rule 3: NEVER Aggregate SUM(cost) Alongside LEFT JOIN UNNEST(credits)
If a row has 3 credit entries, `LEFT JOIN UNNEST(credits)` duplicates that row 3 times, **tripling** the `SUM(cost)`. This is the most common cause of inflated cost reports.
```sql
-- WRONG: Inflates cost by N times (N = number of credits per row)
SELECT SUM(cost), SUM(credits.amount)
FROM `{BILLING_TABLE}` LEFT JOIN UNNEST(credits) AS credits
-- CORRECT: Subquery aggregates credits without duplicating cost rows
SELECT
SUM(CAST(cost AS NUMERIC)) AS gross_cost,
SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) c), 0)) AS total_credits
FROM `{BILLING_TABLE}`
WHERE project.id = '{PROJECT_ID}'
```
**Note:** `LEFT JOIN UNNEST(credits)` is safe when you are ONLY aggregating `credits.amount` and NOT also aggregating `cost` — e.g., when filtering by credit type. The danger is combining it with `SUM(cost)` in the same query.
### Rule 4: Sanity-Check Costs Against Known GCP Pricing
Before reporting any cost figure, verify it's physically possible.
**Step 1: Check the currency.** GCP billing accounts can use ANY currency (USD, VND, EUR, BRL, JPY, etc.). Run `gcp_billing_currency` or check the `currency` column BEFORE interpreting any numbers. A value of `5,000,000` in VND (~$200 USD) is very different from `5,000,000` in USD.
**Step 2: Verify magnitude against known pricing (in the account's currency):**
| Machine Type | Region | Monthly On-Demand Price (USD) |
| ------------- | --------------- | ----------------------------- |
| e2-micro | asia-southeast1 | ~$8/mo |
| e2-small | asia-southeast1 | ~$15/mo |
| e2-medium | asia-southeast1 | ~$30/mo |
| e2-standard-2 | asia-southeast1 | ~$60/mo |
| n1-standard-2 | asia-southeast1 | ~$60/mo (before SUD) |
| n2-standard-2 | asia-southeast1 | ~$70/mo |
**Red flags that indicate a query error or currency mismatch:**
- A single VM shows >$1,000 USD/week (e2-standard-2 costs ~$60/month)
- Total project cost exceeds $100,000 USD/week for a standard workload
- AI/API costs look 1,000x-25,000x higher than expected (likely VND/JPY reported as USD)
**If numbers seem unreasonably high, STOP and verify the currency before reporting.** Do NOT present cost numbers to users without confirming the currency unit.
### Rule 5: Detect Credit Programs Before Alerting
If `net_cost` is consistently $0 (or near-zero) while `gross_cost` is large, the account is on a **promotional credit program** (free trial, startup credits, enterprise credits). This is normal — not an anomaly.
**MANDATORY first query** before any billing analysis:
```sql
-- Step 0: Detect credit program status
SELECT
c.type AS credit_type,
c.full_name AS credit_name,
COUNT(*) AS line_items,
SUM(CAST(c.amount AS NUMERIC)) AS total_credit_amount
FROM `{BILLING_TABLE}`, UNNEST(credits) AS c
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
AND project.id = '{PROJECT_ID}'
GROUP BY 1, 2
ORDER BY total_credit_amount ASC
```
**Interpretation:**
- `PROMOTION` credits with large amounts → account is on promotional/trial program
- `SUSTAINED_USAGE_DISCOUNT` → automatic discounts on N1/N2/N2D/C2/M1/M2 instances
- `COMMITTED_USAGE_DISCOUNT` → organization has CUD commitments
- `FREE_TIER` → usage within free tier limits
**If PROMOTION credits fully offset costs:** Report that the account is on a credit program. Do NOT generate alarmist alerts about "projected costs when credits expire" unless the user specifically asks for that analysis.
### Rule 6: Detect Anomalies on NET Cost, Not Gross Cost
Gross cost fluctuates when credits are added/removed/adjusted. Only net cost reflects actual spending changes.
```sql
-- Pseudocode (not valid SQL — see Anomaly Detection query in BigQuery section for working version)
-- WRONG: Anomaly on gross cost → false positive from credit changes
HAVING SUM(cost) > threshold
-- CORRECT: Anomaly on net cost → real spending change
HAVING (SUM(cost) + SUM(credits_subquery)) > threshold
```
### Rule 7: Verify Services Actually Exist Before Alerting
If billing data shows charges for a service (e.g., App Engine, Load Balancer) but `gcloud` commands show that service doesn't exist in the project, the charges are likely from **another project in the same billing account** that leaked into your unfiltered query. Go back to Rule 2 and add `WHERE project.id = ...`.
### Rule 8: Use NUMERIC Casting for Financial Precision
The `cost` and `credits.amount` fields are Float type. Summing millions of rows accumulates floating-point errors. Always cast:
```sql
SUM(CAST(cost AS NUMERIC)) -- not SUM(cost)
```
### Rule 9: Understand invoice.month vs usage_start_time
- `invoice.month` (YYYYMM): The invoice this line item belongs to. **Use for invoice reconciliation.**
- `usage_start_time`: When usage actually occurred. **Use for trend analysis.**
- These can differ: late-reported usage from month N may appear on month N+1's invoice.
- See the **Invoice Reconciliation** query in the BigQuery Billing Export Patterns section for a working example.
### Rule 10: Data Has 24-48 Hour Delay
Billing export data takes up to 24-48 hours to fully propagate. Do NOT alert on "missing data" for the current day or yesterday.
### Rule 11: Always Detect and Report Currency
GCP billing accounts can be configured in **any currency** (USD, EUR, VND, BRL, JPY, GBP, etc.). The `currency` column in the billing export table identifies the billing currency. **NEVER assume USD.**
**MANDATORY**: Run `gcp_billing_currency` (or check `SELECT DISTINCT currency FROM TABLE`) as part of the first billing query. Include the currency in every cost report.
```sql
-- Check billing currency
SELECT DISTINCT currency FROM `{BILLING_TABLE}` WHERE project.id = '{PROJECT_ID}'
```
**Reporting rules:**
- **ALWAYS include the currency code** when presenting costs (e.g., "5,368,844 VND" not "$5,368,844")
- **NEVER use `$` symbol** without confirming the currency is USD
- If the user needs USD conversion, state the approximate exchange rate used and note it may not be current
- **Do NOT hardcode exchange rates in SQL queries.** Report costs in their native currency and provide approximate USD equivalent separately if needed
**Common non-USD currencies and approximate rates (for sanity-checking only):**
- VND: ~25,000 VND = 1 USD (costs appear 25,000x larger than USD equivalent)
- JPY: ~150 JPY = 1 USD (costs appear 150x larger)
- EUR: ~0.92 EUR = 1 USD
- BRL: ~5 BRL = 1 USD
### Mandatory Pre-Analysis Checklist
**Before writing ANY billing query, verify ALL of the following:**
- [ ] **Currency has been detected** via `gcp_billing_currency` or equivalent query
- [ ] Query includes `WHERE project.id = '{PROJECT_ID}'` filter
- [ ] Net cost is computed as `SUM(CAST(cost AS NUMERIC)) + SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) c), 0))`
- [ ] NO `LEFT JOIN UNNEST(credits)` used alongside `SUM(cost)`
- [ ] Cost fields use `CAST(... AS NUMERIC)` for precision
- [ ] Credit program detection query has been run FIRST
- [ ] Any per-resource cost is sanity-checked against known GCP pricing **in the detected currency**
- [ ] Anomaly detection uses net cost, not gross cost
- [ ] `cost_type` is considered (regular vs tax vs adjustment vs rounding_error)
- [ ] **All cost figures in the report include the currency unit**
## CLI Tips
### Parallel Execution Requirement (CRITICAL)
**ALL independent operations MUST run in parallel using background jobs (&) and wait**
ENFORCEMENT RULES:
- **FORBIDDEN**: Sequential loops like `for item in $items; do cmd $item; done` (causes O(n) runtime)
- **MANDATORY**: Every independent operation spawns a background job: `{ cmd1 } & { cmd2 } & { cmd3 } & wait`
- **DETECTION**: If your script processes N resources/metrics/regions and N > 1, the script MUST contain at least N background jobs
- **TIME IMPACT**: Sequential execution with 30 instances x 2 seconds per call = 60 seconds. Parallel = 2 seconds (30x faster)
- **VALIDATION CHECKLIST** (agent must mentally verify before output):
- Count independent operations: \_\_\_
- Count background jobs (&): \_\_\_
- These numbers MUST match, or script will be REJECTED
- Do all operations depend on each other? (Only valid exception to parallel requirement)
PARALLEL PATTERN (CORRECT):
```bash
for instance in $instances; do
operation "$instance" & # ← Spawn as background job
done
wait # ← Wait for all to complete
```
SEQUENTIAL PATTERN (FORBIDDEN - ONLY if operations have data dependencies):
```bash
result=$(operation1)
operation2 "$result" # ← Only valid if operation2 requires operation1's output
```
### Agent Output Rules
- The script output is for the agent itself to read and process, NOT for human reading
- Do NOT add visual formatting, icons, or decorative elements (no emojis, borders, or separators)
- NEVER USE echo statements for section breaks, headers, or formatting (no "--------", "====", or similar)
- Focus on raw data extraction and minimal, parseable output
- Use plain text format with consistent delimiters for easy parsing
- Prioritize machine-readability over human presentation
- NEVER run commands or scripts that print, log, or expose environment variables, credentials, or GCP keys (e.g., GOOGLE_APPLICATION_CREDENTIALS)
### Execution Guidelines
- **PARALLEL EXECUTION IS MANDATORY**: Always use background jobs (`&`) and `wait` for independent operations
- Process multiple instances/resources in parallel: `{ ... } &` with `wait` at the end
- Fetch multiple metrics for the same resource in parallel, then `wait` before processing
- Sequential loops are FORBIDDEN unless operations have strict dependencies
- Parallel execution reduces runtime from O(n \* time_per_operation) to O(max_operation_time) - use it always
- GCP region and project ID are already preconfigured in the environment - no need to set them manually
- Always consolidate related steps into single CLI Bash script if possible
- Only use read-only commands (e.g., list, describe, get) - never modify resources
- Always format CLI output as plain text (never JSON or table) so that it's easy for the agent to parse. For GCP use --format=text or --format="value(...)" as appropriate. Also use filtering/query flags to limit output to what is needed. These practices are crucial for efficiency and accuracy.
- **NON-INTERACTIVE MODE**: Use `--quiet` (`-q`) flag or `export CLOUDSDK_CORE_DISABLE_PROMPTS=1` to disable prompts in scripts
### Filtering Guidance
**FILTERING ORDER MATTERS - Understand server-side vs client-side**
1. **`--filter` (VARIES BY COMMAND)** - Can be server-side OR client-side
- Some commands send filter to API (server-side) → reduces network payload
- Other commands filter locally (client-side) → full data still transferred
- Use `--log-http` to verify: if filter appears in API request, it's server-side
- Server-side is MORE efficient for large datasets
2. **`--format` (ALWAYS CLIENT-SIDE)** - Formatting after data retrieval
- Use `--format="value(...)"` for clean, parseable output
- Use projections to select specific fields
- Always applied AFTER --filter
**PERFORMANCE IMPACT**:
- Server-side --filter: API returns only matching records
- Client-side --filter: API returns ALL records, filtered locally
- Use `--log-http` to check which mode your command uses
**EXAMPLES**:
```bash
# Efficient: --filter with --format for minimal output
gcloud compute instances list --filter="status=RUNNING" \
--format="value(name,zone.scope(zones),machineType.scope(machineTypes))"
# Verify if filter is server-side (look for filter in HTTP request)
gcloud compute instances list --filter="status=RUNNING" --log-http 2>&1 | grep -i filter
# Multiple filter conditions
gcloud compute instances list \
--filter="status=RUNNING AND machineType~n1-standard" \
--format="value(name,zone)"
```
**COMMON FILTER OPERATORS**:
- `=` exact match, `!=` not equal, `~` regex match, `!~` regex not match
- `:` substring match (HAS operator)
- `>`, `>=`, `<`, `<=` for comparisons
- `AND`, `OR`, `NOT` for boolean logic
### Pagination Guidelines
**PAGINATION FOR LARGE DATASETS - Prevent timeouts and memory issues**
**KEY PARAMETERS**:
- `--limit=N`: Maximum total items to return (stops early)
- `--page-size=N`: Items per API call (internal pagination, still returns all unless --limit set)
- `--sort-by=FIELD`: Sort results (prefix with ~ for descending)
**ORDER OF OPERATIONS** (gcloud applies in this order):
1. `--flatten` → 2. `--sort-by` → 3. `--filter` → 4. `--limit`
**EXAMPLES**:
```bash
# Get first 10 instances only
gcloud compute instances list --limit=10 --format="value(name,zone)"
# Paginate with smaller chunks (memory efficiency)
gcloud compute instances list --page-size=50 --limit=200 \
--format="value(name,zone)"
# Sort by creation time, newest first
gcloud compute instances list --sort-by=~creationTimestamp --limit=5 \
--format="value(name,creationTimestamp)"
```
**BEST PRACTICE**: Combine --filter + --limit to minimize data transfer:
```bash
# Filter server-side, limit client-side
gcloud compute instances list --filter="status=RUNNING" --limit=100 \
--format="value(name,zone)"
```
### Format Projections
**USEFUL PROJECTION FUNCTIONS - Reduce post-processing with built-in transforms**
**EXTRACTION FUNCTIONS**:
- `.scope(segment)` - Extract last URL segment (e.g., zone name from full URL)
- `.basename()` - Get filename from path
- `.segment(n)` - Get nth segment from URL
**DATE/TIME FUNCTIONS**:
- `.date(format)` - Format timestamp (e.g., `.date('%Y-%m-%d')`)
- `.date(tz=LOCAL)` - Convert to local timezone
**STRING FUNCTIONS**:
- `.yesno(yes, no)` - Convert boolean to custom strings
- `.list()` - Format as comma-separated list
**EXAMPLES**:
```bash
# Extract zone name from full URL
gcloud compute instances list \
--format="value(name,zone.scope(zones),machineType.scope(machineTypes))"
# Output: my-vm us-central1-a n1-standard-1
# Format creation date
gcloud compute instances list \
--format="table(name,creationTimestamp.date('%Y-%m-%d'),status)"
# Boolean formatting
gcloud compute instances list \
--format="value(name,scheduling.preemptible.yesno('preemptible','on-demand'))"
# Get single value (no headers)
gcloud config get-value project --format="value(.)"
```
**REFERENCE**: Run `gcloud topic projections` for full documentation
### Efficient CLI Script Example
**ANTI-PATTERN EXAMPLE (SEQUENTIAL - SLOW - UNACCEPTABLE)**
```bash
#!/bin/bash
# RUNTIME: ~60 seconds for 30 instances (2 sec per call x 30)
END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
START_TIME=$(date -u -d "30 days ago" +"%Y-%m-%dT%H:%M:%SZ")
echo "GCP VM Metrics Summary ($START_TIME to $END_TIME)"
PROJECT_ID=$(gcloud config get-value project)
echo "Project: $PROJECT_ID"
# Get zones and instances
gcloud compute zones list --format="value(name)" | while read zone; do
instances=$(gcloud compute instances list --zones="$zone" --format="value(name)" --project="$PROJECT_ID")
if [ -n "$instances" ]; then
echo "Zone: $zone"
# This SEQUENTIAL loop is FORBIDDEN
echo "$instances" | while read instance_name; do
echo " Instance: $instance_name"
# Sequential metric fetches - UNACCEPTABLE
gcloud monitoring time-series list \
--filter="resource.labels.project_id='$PROJECT_ID' AND resource.labels.zone='$zone' AND resource.labels.instance_id='$instance_name' AND metric.type='compute.googleapis.com/instance/cpu/utilization'" \
--interval.start-time="$START_TIME" \
--interval.end-time="$END_TIME" \
--aggregation.alignment-period=3600s \
--aggregation.per-series-aligner="ALIGN_MEAN" \
--format="value(points[].value.doubleValue)" \
--project="$PROJECT_ID"
gcloud monitoring time-series list \
--filter="resource.labels.project_id='$PROJECT_ID' AND resource.labels.zone='$zone' AND resource.labels.instance_id='$instance_name' AND metric.type='compute.googleapis.com/instance/network/received_bytes_count'" \
--interval.start-time="$START_TIME" \
--interval.end-time="$END_TIME" \
--aggregation.alignment-period=3600s \
--aggregation.per-series-aligner="ALIGN_RATE" \
--format="value(points[].value.doubleValue)" \
--project="$PROJECT_ID"
done
fi
done
# TOTAL TIME: ~60 seconds (UNACCEPTABLE for 30+ instances)
```
**CORRECT EXAMPLE (PARALLEL - FAST - REQUIRED)**
```bash
#!/bin/bash
# RUNTIME: ~2 seconds for 30 instances (all run simultaneously)
END_TIME=$(date -u +"%Y-%m-%dT%H:%M:%SZ")
START_TIME=$(date -u -d "30 days ago" +"%Y-%m-%dT%H:%M:%SZ")
PROJECT_ID=$(gcloud config get-value project 2>/dev/null)
# Fetch a single metric (called in parallel)
get_metric() {
local instance=$1 zone=$2 metric=$3 aligner=$4
gcloud monitoring time-series list \
--filter="resource.labels.instance_id='$instance' AND metric.type='$metric'" \
--interval.start-time="$START_TIME" --interval.end-time="$END_TIME" \
--aggregation.alignment-period=3600s \
--aggregation.per-series-aligner="$aligner" \
--format="value(points[].value.doubleValue)" \
--project="$PROJECT_ID" \
| awk -v i="$instance" -v m="$metric" '{sum+=$1; count++} END {if(count>0) printf "%s\t%s\t%.2f\n", i, m, sum/count}'
}
# Process one instance: fetch multiple metrics in parallel
process_instance() {
local instance=$1 zone=$2
get_metric "$instance" "$zone" "compute.googleapis.com/instance/cpu/utilization" "ALIGN_MEAN" &
get_metric "$instance" "$zone" "compute.googleapis.com/instance/network/received_bytes_count" "ALIGN_RATE" &
wait # Wait for all metrics of this instance
}
# Process ALL instances in parallel
instances=$(gcloud compute instances list --format="value(name,zone.scope(zones))" --project="$PROJECT_ID")
echo "$instances" | while read instance zone; do
process_instance "$instance" "$zone" &
done
wait # Wait for all instances to complete
```
**PERFORMANCE COMPARISON TABLE**
| Pattern | Instances | Time/Call | Total Time | Speed |
|---------|-----------|-----------|-----------|-------|
| Sequential (❌) | 30 | 2 sec | ~60 sec | Baseline |
| Parallel (✅) | 30 | 2 sec | ~2 sec | **30x faster** |
| Sequential (❌) | 100 | 2 sec | ~200 sec | Baseline |
| Parallel (✅) | 100 | 2 sec | ~2 sec | **100x faster** |
**KEY DIFFERENCES IN THIS SCRIPT (What Makes It Parallel)**
- In the main loop: `process_instance "$instance" "$zone" &` - spawns each instance as background job
- After the loop: `wait` - waits for all instances to complete
- Inside `process_instance`: metrics are fetched in parallel (with `&` and inner `wait`)
- Uses `--format="value(name,zone.scope(zones))"` for efficient extraction
**VALIDATION CHECKLIST FOR AGENT**
Before outputting ANY script, check every item:
- [ ] Count number of independent resources/metrics/zones to process: \_\_\_
- [ ] Count number of `&` background job spawns in script: \_\_\_
- [ ] If these counts don't match, the script is WRONG - REJECT it and rewrite
- [ ] Verify each background job block is followed by a `wait` statement
- [ ] Check that NO sequential loops exist for independent operations
- [ ] Confirm expected runtime is ~2-10 seconds (not ~30+ seconds)
- [ ] Verify GCP monitoring aligners are correct (ALIGN_MEAN, ALIGN_MAX, ALIGN_RATE, etc.)
- [ ] Confirm script uses --format=text or --format="value(...)" with proper filtering
### Common GCP CLI Patterns
- List all projects: `gcloud projects list --format="value(projectId,name,lifecycleState)"`
- List compute instances: `gcloud compute instances list --format="value(name,zone,machineType.scope(machineTypes),status)"`
- Get instance details: `gcloud compute instances describe myInstance --zone=us-central1-a --format="value(name,machineType,status,scheduling.preemptible)"`
- List Cloud Storage buckets: `gcloud storage buckets list --format="value(name,location,storageClass)"`
- List Cloud SQL instances: `gcloud sql instances list --format="value(name,databaseVersion,region,tier,state)"`
- Get current project (preconfigured): `gcloud config get-value project --format="value(.)"`
- List App Engine services: `gcloud app services list --format="value(id,split.allocations.keys())"`
- List Cloud Functions: `gcloud functions list --format="value(name,status,trigger.eventTrigger.eventType)"`
- Get billing info: `gcloud billing accounts list --format="value(name,displayName,open)"`
- List Kubernetes clusters: `gcloud container clusters list --format="value(name,location,status,currentMasterVersion)"`
### GCP Service Naming Patterns
- Compute Engine: e2-micro, n1-standard-1, c2-standard-4, n2-highmem-2
- Cloud Storage: STANDARD, NEARLINE, COLDLINE, ARCHIVE
- Cloud SQL: db-f1-micro, db-n1-standard-1, db-n1-highmem-2
- App Engine: F1, F2, F4, F4_1G (for automatic scaling)
- Cloud Functions: Various memory sizes (128MB, 256MB, 512MB, 1GB, 2GB, 4GB, 8GB)
### Billing CLI Commands
**BILLING ACCOUNT & BUDGET MANAGEMENT - gcloud billing commands**
**LIST BILLING ACCOUNTS** (accounts you have access to):
```bash
# List all billing accounts with key fields
gcloud billing accounts list --format="value(name,displayName,open)"
# Get billing account ID only (for scripting)
gcloud billing accounts list --format="value(name)" --filter="open=true"
```
**DESCRIBE BILLING ACCOUNT**:
```bash
# Get account details (check if sub-account)
gcloud billing accounts describe BILLING_ACCOUNT_ID \
--format="value(displayName,masterBillingAccount,open)"
# If masterBillingAccount is set, this is a reseller sub-account
```
**LIST PROJECTS UNDER BILLING ACCOUNT**:
```bash
# List all projects linked to a billing account
gcloud billing projects list --billing-account=BILLING_ACCOUNT_ID \
--format="value(projectId,billingEnabled)"
# Filter to only enabled projects
gcloud billing projects list --billing-account=BILLING_ACCOUNT_ID \
--filter="billingEnabled=true" --format="value(projectId)"
```
**CHECK PROJECT BILLING STATUS**:
```bash
# Check if specific project has billing enabled
gcloud billing projects describe PROJECT_ID --format="value(billingEnabled)"
# Returns: True or False
# Get billing account linked to project
gcloud billing projects describe PROJECT_ID \
--format="value(billingAccountName)"
```
**BUDGET MANAGEMENT**:
```bash
# List all budgets for a billing account
gcloud billing budgets list --billing-account=BILLING_ACCOUNT_ID \
--format="value(displayName,amount.specifiedAmount.units,amount.specifiedAmount.currencyCode)"
# Describe specific budget details
gcloud billing budgets describe BUDGET_ID --billing-account=BILLING_ACCOUNT_ID \
--format="value(displayName,amount,budgetFilter,thresholdRules)"
```
**PARALLEL PATTERN FOR MULTI-ACCOUNT ANALYSIS**:
```bash
# Fetch billing info for multiple accounts in parallel
accounts=$(gcloud billing accounts list --format="value(name)" --filter="open=true")
for account in $accounts; do
{
projects=$(gcloud billing projects list --billing-account="$account" \
--format="value(projectId)" --filter="billingEnabled=true")
echo "$account: $(echo "$projects" | wc -l) projects"
} &
done
wait
```
### VAT/Tax Handling
**CRITICAL: VAT/TAX AWARENESS - Why your costs may differ from Console**
**IMPORTANT**: GCP Pricing API and BigQuery billing exports return **TAX-EXCLUSIVE** (pre-tax) prices.
The GCP Console dashboard shows **TOTAL costs INCLUDING taxes** (VAT, GST, sales tax, etc.).
**THIS CAUSES DISCREPANCIES** between API results and what customers see in their Console!
**HOW GCP HANDLES TAXES**:
1. **Pricing API**: Returns base list prices WITHOUT tax
2. **BigQuery Billing Export**: `cost` column is PRE-TAX; taxes are separate rows with `cost_type = "tax"`
3. **Console Dashboard**: Shows aggregated totals WITH taxes included
4. **Invoices**: Show taxes as separate line items by project
**TAX RATES BY REGION** (examples - varies by location and changes over time):
- EU countries: 19-27% VAT (varies by country)
- UK: 20% VAT
- Australia: 10% GST
- India: 18% GST
- Canada: 5-15% (varies by province)
- Bahrain: 10% VAT (since Feb 2022)
- US: State sales tax varies (0-10%+)
**TO GET TAX-INCLUSIVE TOTALS FROM BIGQUERY**:
```sql
-- Get total cost INCLUDING taxes (matches Console dashboard)
-- Note: project.id filter required per Rule 2
SELECT
invoice.month AS invoice_month,
SUM(CASE WHEN cost_type != 'tax' THEN CAST(cost AS NUMERIC) ELSE 0 END) AS pre_tax_cost,
SUM(CASE WHEN cost_type = 'tax' THEN CAST(cost AS NUMERIC) ELSE 0 END) AS tax_amount,
SUM(CAST(cost AS NUMERIC))
+ SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) c), 0)) AS net_cost_with_tax
FROM `{BILLING_TABLE}`
WHERE project.id = '{PROJECT_ID}'
AND invoice.month = FORMAT_DATE('%Y%m', CURRENT_DATE())
GROUP BY 1
-- Get tax breakdown by project (intentionally no project.id filter: multi-project breakdown)
SELECT
project.id AS project_id,
SUM(CASE WHEN cost_type != 'tax' THEN CAST(cost AS NUMERIC) ELSE 0 END) AS usage_cost,
SUM(CASE WHEN cost_type = 'tax' THEN CAST(cost AS NUMERIC) ELSE 0 END) AS tax_cost,
SUM(CAST(cost AS NUMERIC))
+ SUM(IFNULL((SELECT SUM(CAST(c.amount AS NUMERIC)) FROM UNNEST(credits) c), 0)) AS net_total_with_tax
FROM `{BILLING_TABLE}`
WHERE DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY 1
ORDER BY net_total_with_tax DESC
-- Get tax types (VAT, GST, sales tax, etc.)
SELECT
sku.description AS tax_type,
SUM(CAST(cost AS NUMERIC)) AS tax_amount
FROM `{BILLING_TABLE}`
WHERE project.id = '{PROJECT_ID}'
AND cost_type = 'tax'
AND DATE(usage_start_time) >= DATE_SUB(CURRENT_DATE(), INTERVAL 30 DAY)
GROUP BY 1
ORDER BY 2 DESC
```
**WHEN REPORTING COSTS TO USERS**:
- ALWAYS clarify whether costs are pre-tax or post-tax
- If comparing to Console, include taxes: "Total cost (including VAT): $X"
- For budgeting, use tax-inclusive figures to match what they'll be invoiced
- Include tax breakdown when relevant: "Usage: $X + VAT: $Y = Total: $Z"
**COST COMPARISON FORMULA** (pseudocode — use CAST/UNNEST subquery pattern in actual SQL):
```
Console Total = Usage Cost + Credits + Taxes
= SUM(cost WHERE cost_type='regular')
+ SUM(credits.amount) -- credits are negative
+ SUM(cost WHERE cost_type='tax')
= SUM(cost) + SUM(credits.amount) -- simplified: all cost_types
```
### Pricing Script (`get_pricing_gcp.sh`)
**DO NOT read or modify the script file.** Only source and call the function.
**SETUP** (at the start of your script):
```bash
source ./_skills/connections/gcp/gcp/scripts/get_pricing_gcp.sh
```
**FUNCTION**: `get_gcp_cost RESOURCE REGION`
Auto-detects the GCP service from the resource name prefix and returns on-demand pricing in TOON format.
**NOTE**: All prices are TAX-EXCLUSIVE. See VAT/Tax Handling for tax-inclusive calculations.
**Supported resource prefixes:**
| Prefix | Service | Example |
|--------|---------|---------|
| `e2-`, `n1-`, `n2-`, `c2-`, `c3-` | Compute Engine | `e2-standard-2` |
| `cloudsql-`, `db-` | Cloud SQL | `cloudsql-db-n1-standard-2` |
| `gcs-` | Cloud Storage | `gcs-standard` |
| `functions-` | Cloud Functions | `functions-256mb` |
| `cloudrun-` | Cloud Run | `cloudrun-1cpu-512mb` |
| `redis-` | Memorystore | `redis-basic-1gb` |
| `bq-` | BigQuery | `bq-ondemand` |
| `pd-` | Persistent Disk | `pd-ssd-100gb` |
| `lb-` | Load Balancer | `lb-forwarding-rule` |
| `cloudnat-` | Cloud NAT | `cloudnat-standard` |
| `gke-` | GKE | `gke-standard` |
**Compute Engine detail**: GCP bills vCPU and RAM separately. The script has a built-in machine spec table and queries both Core and Ram SKUs, combining them into a total hourly/monthly estimate.
**Examples:**
```bash
source ./_skills/connections/gcp/gcp/scripts/get_pricing_gcp.sh
get_gcp_cost e2-standard-2 asia-southeast1
get_gcp_cost n2-standard-4 us-central1
get_gcp_cost gcs-standard us-central1
get_gcp_cost cloudsql-db-n1-standard-2 asia-southeast1
```
### Monitoring and Metrics
**COMMON METRICS**:
- CPU utilization: compute.googleapis.com/instance/cpu/utilization
- Network traffic: compute.googleapis.com/instance/network/received_bytes_count, sent_bytes_count
- Disk I/O: compute.googleapis.com/instance/disk/read_bytes_count, write_bytes_count
- Memory usage (with monitoring agent): agent.googleapis.com/memory/percent_used
**ALIGNERS** (per-series-aligner):
- ALIGN_MEAN: Average value over alignment period (use for utilization metrics)
- ALIGN_MAX: Maximum value (use for peak detection)
- ALIGN_MIN: Minimum value
- ALIGN_RATE: Rate of change (use for counter metrics like bytes_count)
- ALIGN_SUM: Sum of values (use for uptime, request counts)
**CRITICAL: alignment-period MUST be >= 60 seconds**
If you specify a per-series-aligner other than ALIGN_NONE, alignment-period is REQUIRED and must be at least 60 seconds.
**CROSS-SERIES REDUCERS** (aggregate across multiple resources):
- REDUCE_MEAN: Average across all time series
- REDUCE_MAX: Maximum across all time series
- REDUCE_SUM: Sum across all time series
- REDUCE_COUNT: Count of time series
**CROSS-SERIES EXAMPLE** (aggregate CPU across all instances in a zone):
```bash
gcloud monitoring time-series list \
--filter="metric.type='compute.googleapis.com/instance/cpu/utilization'" \
--aggregation.alignment-period=3600s \
--aggregation.per-series-aligner=ALIGN_MEAN \
--aggregation.cross-series-reducer=REDUCE_MEAN \
--aggregation.group-by-fields="resource.labels.zone" \
--format="value(points[].value.doubleValue)"
```
### Billing Script (`get_billing_gcp.sh`)
**DO NOT read or modify the script file.** Only source and call the functions.
**SETUP** (at the start of your script):
```bash
source ./_skills/connections/gcp/gcp/scripts/get_billing_gcp.sh
```
**All functions enforce anti-hallucination rules**: `WHERE project.id` filter, `CAST(... AS NUMERIC)` on financial fields, net cost via UNNEST subquery (never LEFT JOIN UNNEST + SUM(cost)), `cost_type = 'regular'` where appropriate. Output is TOON format (tab-separated).
**TABLE NAMING CONVENTION**:
- Standard usage: `dataset.gcp_billing_export_v1_<BILLING_ACCOUNT_ID_NO_DASHES>`
- Detailed usage: `dataset.gcp_billing_export_resource_v1_<BILLING_ACCOUNT_ID_NO_DASHES>`
- Billing Account ID `012ABC-456DEF-789GHI` becomes table suffix `012ABC456DEF789GHI` (dashes removed)
**FUNCTION REFERENCE**:
| Function | Purpose | Signature |
|----------|---------|-----------|
| `gcp_billing_currency` | Detect billing currency (MANDATORY first) | `TABLE PROJECT_ID` |
| `gcp_billing_credits` | Credit program detection (MANDATORY second) | `TABLE PROJECT_ID` |
| `gcp_billing_summary` | Top services by net cost | `TABLE PROJECT_ID [--days N]` |
| `gcp_billing_trend` | Daily net cost trend | `TABLE PROJECT_ID [--days N]` |
| `gcp_billing_anomalies` | Z-score anomaly on net cost | `TABLE PROJECT_ID` |
| `gcp_billing_by_resource` | Resource-level breakdown (detailed export) | `TABLE PROJECT_ID [--days N]` |
| `gcp_billing_by_sku` | SKU-level breakdown | `TABLE PROJECT_ID [--days N]` |
| `gcp_billing_invoice` | Invoice reconciliation (uses `invoice.month`) | `TABLE PROJECT_ID [--month YYYYMM]` |
| `gcp_billing_compare` | Multi-project comparison (no project filter) | `TABLE [--days N]` |
**MANDATORY WORKFLOW** (every billing analysis):
1. **Always run `gcp_billing_currency` first** to detect the billing currency (see Rule 11)
2. **Always run `gcp_billing_credits` second** to detect credit programs
3. Interpret `credit_coverage_ratio`: close to -1.0 = fully covered by credits (do NOT alarm); -0.3 to -0.01 = partial discounts (normal); ~0 = minimal credits
4. Then run `gcp_billing_summary` or other functions as needed
5. Sanity-check costs against known GCP pricing **in the detected currency** (see Rule 4)
**Examples:**
```bash
source ./_skills/connections/gcp/gcp/scripts/get_billing_gcp.sh
TABLE="dataset.gcp_billing_export_v1_012ABC456DEF789GHI"
PROJECT="my-project-id"
# Step 0a: Detect billing currency (MANDATORY - see Rule 11)
gcp_billing_currency "$TABLE" "$PROJECT"
# Step 0b: Detect credit programs (MANDATORY)
gcp_billing_credits "$TABLE" "$PROJECT"
# Top services by net cost (last 30 days)
gcp_billing_summary "$TABLE" "$PROJECT" --days 30
# Daily trend
gcp_billing_trend "$TABLE" "$PROJECT" --days 14
# Anomaly detection
gcp_billing_anomalies "$TABLE" "$PROJECT"
# Resource-level breakdown (requires detailed export table)
gcp_billing_by_resource "$TABLE" "$PROJECT" --days 7
# SKU-level breakdown
gcp_billing_by_sku "$TABLE" "$PROJECT" --days 7
# Invoice reconciliation (current month)
gcp_billing_invoice "$TABLE" "$PROJECT"
# Invoice reconciliation (specific month)
gcp_billing_invoice "$TABLE" "$PROJECT" --month 202601
# Multi-project comparison (no project filter)
gcp_billing_compare "$TABLE" --days 7
```
**BILLING EXPORT SCHEMA REFERENCE** (key columns):
| Column | Description |
| ------------------- | ---------------------------------------------------------------------------------------- |
| `currency` | ISO 4217 currency code (USD, VND, EUR, etc.). **Check this FIRST — never assume USD.** |
| `cost` | Usage cost at contract/on-demand rate, BEFORE credits. NOT your actual bill. |
| `cost_at_list` | Cost at public list price (before negotiated discounts). |
| `credits` | Array of credit entries. Each has `type`, `amount` (always negative), `full_name`. |
| `credits.type` | PROMOTION, SUSTAINED_USAGE_DISCOUNT, COMMITTED_USAGE_DISCOUNT, FREE_TIER, DISCOUNT, etc. |
| `cost_type` | "regular", "tax", "adjustment", or "rounding_error". |
| `project.id` | GCP project ID. ALWAYS filter by this. |
| `invoice.month` | YYYYMM string. Use for invoice reconciliation. |
| `usage_start_time` | When usage occurred. Use for trend analysis. |
| `resource.name` | Resource identifier (detailed export only). |
**CREDIT TYPES**:
| Credit Type | Meaning | Typical Coverage |
| -------------------------------------- | ---------------------------------------------------------- | ------------------------- |
| `PROMOTION` | Trial credits, startup credits, enterprise promotional | Can be 100% (full offset) |
| `SUSTAINED_USAGE_DISCOUNT` | Auto-discount for N1/N2/N2D/C2/M1/M2 running >25% of month | Up to 30% for N1 |
| `COMMITTED_USAGE_DISCOUNT` | Resource-based CUD commitment | 37-55% depending on term |
| `COMMITTED_USAGE_DISCOUNT_DOLLAR_BASE` | Spend-based CUD commitment | Varies by commitment |
| `FREE_TIER` | Always-free tier usage | Small amounts |
| `DISCOUNT` | Other negotiated discounts | Varies |
| `RESELLER_MARGIN` | Reseller margin credits | Varies |
**NOTE on SUDs**: E2 machine types are NOT eligible for Sustained Use Discounts. Only N1, N2, N2D, C2, M1, M2 families receive SUDs.
### gcloud Topic References
**BUILT-IN HELP - Learn filter/format/projection syntax**
```bash
gcloud topic filters # Filter expression syntax and operators
gcloud topic formats # Output format options and projections
gcloud topic projections # Projection functions (.scope(), .date(), etc.)
```
### Parallel vs Sequential Rules Summary
**PARALLEL vs SEQUENTIAL - Quick Reference**
- **ALWAYS PARALLEL**: Multiple instances, metrics, zones, or projects
- **ONLY SEQUENTIAL**: When operation B requires output from operation A
- **PATTERN**: `for item in $items; do operation "$item" & done; wait`
**FORBIDDEN ANTI-PATTERNS** (see detailed examples above):
- ❌ Sequential loops: `for x in $list; do cmd; done` → use `& done; wait`
- ❌ Sequential commands: `result1=$(cmd1); result2=$(cmd2)` → use `cmd1 & cmd2 & wait`
- ❌ Individual describe calls when list is available
## Output Format
Present results as a structured report:
```
Gcp Report
══════════
Resources discovered: [count]
Resource Status Key Metric Issues
──────────────────────────────────────────────
[name] [ok/warn] [value] [findings]
Summary: [total] resources | [ok] healthy | [warn] warnings | [crit] critical
Action Items: [list of prioritized findings]
```
Target ≤50 lines of output. Use tables for multi-resource comparisons.
## Counter-Rationalizations
| Shortcut | Counter | Why |
|----------|---------|-----|
| "I'll skip discovery and check known resources" | Always run Phase 1 discovery first | Resource names change, new resources appear — assumed names cause errors |
| "The user only asked for a quick check" | Follow the full discovery → analysis flow | Quick checks miss critical issues; structured analysis catches silent failures |
| "Default configuration is probably fine" | Audit configuration explicitly | Defaults often leave logging, security, and optimization features disabled |
| "Metrics aren't needed for this" | Always check relevant metrics when available | API/CLI responses show current state; metrics reveal trends and intermittent issues |
| "I don't have access to that" | Try the command and report the actual error | Assumed permission failures prevent useful investigation; actual errors are informative |
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!