GCP Cloud Run Deployment¶
Argus runs as a Cloud Run Job triggered by Cloud Scheduler on a weekly schedule.
Prerequisites¶
gcloudCLI installed and authenticated- Application Default Credentials:
gcloud auth application-default login - APIs enabled (the deploy script enables them automatically):
- Cloud Run, Cloud Scheduler, Artifact Registry
- Cloud Asset Inventory, Cloud Monitoring, Cloud Logging, BigQuery
Deploy¶
export GOOGLE_CLOUD_PROJECT=my-project-id
export SLACK_WEBHOOK_URL=https://hooks.slack.com/services/T.../B.../...
bash deploy/gcp/deploy.sh
Optional variables¶
export REGION=us-central1 # default: us-central1
export BILLING_BQ_TABLE=my-project.billing.gcp_billing_export_v1_XXX
export SCHEDULE="0 9 * * 1" # default: Mondays 9am UTC (cron)
export DRY_RUN=true # skip Slack post
# HTML report storage (optional — enables "Full report" button in Slack)
export REPORT_GCS_BUCKET=my-argus-reports-bucket
export REPORT_URL_EXPIRY=604800 # 7 days (default)
When REPORT_GCS_BUCKET is set, the deploy script automatically:
- Creates the GCS bucket (if it doesn't exist)
- Grants the service account
storage.objectCreator+storage.objectViewer - Grants
iam.serviceAccountTokenCreatoron the service account itself (required for v4 signed URLs) - Sets
REPORT_GCS_BUCKETin the Cloud Run Job environment
What gets created¶
| Resource | Purpose |
|---|---|
| Cloud Run Job | Runs the scan |
| Cloud Scheduler job | Triggers the job weekly |
| Service account | argus-sa@<project>.iam.gserviceaccount.com |
| IAM bindings | See IAM permissions below |
Trigger a manual scan¶
View logs¶
gcloud logging read \
'resource.type=cloud_run_job AND resource.labels.job_name=argus' \
--project=my-project-id \
--limit=50 \
--format='value(textPayload)'
IAM permissions¶
The deploy script creates argus-sa@<project>.iam.gserviceaccount.com and binds these roles automatically. All permissions are read-only — Argus never writes to any cloud resource.
Minimum required roles¶
| Role | IAM permissions granted | Used by | Required |
|---|---|---|---|
roles/cloudasset.viewer | cloudasset.assets.listAssets, cloudasset.assets.searchAllResources | Asset Inventory — list all resources across the project | Yes |
roles/monitoring.viewer | monitoring.timeSeries.list, monitoring.metricDescriptors.list | Cloud Monitoring — CPU, memory, request metrics per resource | Yes |
roles/logging.viewer | logging.logEntries.list | Cloud Audit Logs — last-activity timestamps (Admin Activity + Data Access logs) | Yes |
roles/bigquery.dataViewer | bigquery.tables.getData, bigquery.tables.list | Read the billing export table for cost data | Optional¹ |
roles/bigquery.jobUser | bigquery.jobs.create | Run the cost query job | Optional¹ |
roles/aiplatform.user | aiplatform.endpoints.predict | Invoke Vertex AI models for AI analysis | Optional² |
roles/storage.objectCreator | storage.objects.create | Write JSON + HTML reports to GCS | Optional³ |
roles/storage.objectViewer | storage.objects.get, storage.objects.list | Read reports, generate signed URLs | Optional³ |
roles/iam.serviceAccountTokenCreator | iam.serviceAccounts.signBlob | Sign v4 GCS URLs (self-reference on the SA itself) | Optional³ |
¹ Required only when
BILLING_BQ_TABLEis set. Without it, cost fields show$0.00.
² Required only whenAI_PROVIDER=vertexai(the default for Cloud Run). SetAI_PROVIDER=anthropic+ANTHROPIC_API_KEYto skip this role entirely.
³ Required only whenREPORT_GCS_BUCKETis set.
Custom role (minimum permission surface)¶
If you prefer a custom role over predefined roles, this is the exact minimum:
# argus-custom-role.yaml (no cost data, no GCS, Anthropic for AI)
title: "Argus Scanner"
description: "Minimum read-only permissions for Argus cost optimizer"
stage: GA
includedPermissions:
- cloudasset.assets.listAssets
- monitoring.timeSeries.list
- monitoring.metricDescriptors.list
- logging.logEntries.list
# Add these for cost data (BILLING_BQ_TABLE):
# - bigquery.jobs.create
# - bigquery.tables.getData
# Add this for Vertex AI (AI_PROVIDER=vertexai):
# - aiplatform.endpoints.predict
gcloud iam roles create ArgusScanner \
--project=my-project-id \
--file=argus-custom-role.yaml
gcloud projects add-iam-policy-binding my-project-id \
--member="serviceAccount:argus-sa@my-project-id.iam.gserviceaccount.com" \
--role="projects/my-project-id/roles/ArgusScanner"
Minimum viable setup (no cost data, no GCS reports, Anthropic API for AI)¶
If you want the smallest possible permission surface:
SA="argus-sa@my-project-id.iam.gserviceaccount.com"
PROJECT="my-project-id"
gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:$SA" \
--role="roles/cloudasset.viewer"
gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:$SA" \
--role="roles/monitoring.viewer"
gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:$SA" \
--role="roles/logging.viewer"
Then set AI_PROVIDER=anthropic and ANTHROPIC_API_KEY — no Vertex AI role needed.
Full setup (all features enabled)¶
SA="argus-sa@my-project-id.iam.gserviceaccount.com"
PROJECT="my-project-id"
for ROLE in \
roles/cloudasset.viewer \
roles/monitoring.viewer \
roles/logging.viewer \
roles/bigquery.dataViewer \
roles/bigquery.jobUser \
roles/aiplatform.user \
roles/storage.objectCreator \
roles/storage.objectViewer; do
gcloud projects add-iam-policy-binding $PROJECT \
--member="serviceAccount:$SA" \
--role="$ROLE"
done
# Self-referential binding for GCS signed URLs
gcloud iam service-accounts add-iam-policy-binding $SA \
--member="serviceAccount:$SA" \
--role="roles/iam.serviceAccountTokenCreator" \
--project=$PROJECT
Verify permissions¶
gcloud projects get-iam-policy my-project-id \
--flatten="bindings[].members" \
--filter="bindings.members:argus-sa@my-project-id.iam.gserviceaccount.com" \
--format="table(bindings.role)"
Terraform equivalent¶
locals {
argus_sa = "serviceAccount:argus-sa@${var.project_id}.iam.gserviceaccount.com"
core_roles = [
"roles/cloudasset.viewer",
"roles/monitoring.viewer",
"roles/logging.viewer",
]
cost_roles = [
"roles/bigquery.dataViewer",
"roles/bigquery.jobUser",
]
}
resource "google_project_iam_member" "argus_core" {
for_each = toset(local.core_roles)
project = var.project_id
role = each.value
member = local.argus_sa
}
resource "google_project_iam_member" "argus_cost" {
for_each = toset(local.cost_roles)
project = var.project_id
role = each.value
member = local.argus_sa
}
# Only needed when AI_PROVIDER=vertexai
resource "google_project_iam_member" "argus_vertex" {
project = var.project_id
role = "roles/aiplatform.user"
member = local.argus_sa
}
Multi-project setup¶
To scan multiple GCP projects in one run, see the Multi-project guide — it covers:
- Granting the service account roles across all target projects (copy-paste
gcloudcommands) - Configuring
GCP_PROJECT_IDSorACCOUNTS_CONFIG - Terraform alternative
Cost data setup¶
For per-resource cost data, enable BigQuery billing export:
- GCP Console → Billing → Billing export → BigQuery export
- Note the table name (format:
project.dataset.gcp_billing_export_v1_XXXXXX) - Set
BILLING_BQ_TABLEin the Cloud Run Job environment variables
Without this, cost fields show $0.00 — the agent still finds idle resources via metrics and audit logs.
Enable required APIs¶
The deploy script runs this automatically. To enable manually: