GCP permissions¶
Three plugins here talk to Google Cloud: the GCP catalog provider reads resource inventories, Bruno reads report artifacts from a bucket, and Secure Share reads and writes encrypted chunks in a bucket. All three authenticate the same way, through Application Default Credentials, so one service account can serve all of them.
This guide covers the identity, the permissions each plugin actually uses, and the GKE-specific setup that the Kubernetes plugin needs on top.
Choosing an identity¶
| Where Backstage runs | Credential | Notes |
|---|---|---|
| GKE | Workload Identity | Preferred. No key material anywhere |
| Cloud Run / GCE | Attached service account | Also keyless |
| Elsewhere | Service account key file via GOOGLE_APPLICATION_CREDENTIALS |
A long-lived secret to rotate |
| Local development | gcloud auth application-default login |
Your own identity, so grants must match |
The catalog provider reads GOOGLE_APPLICATION_CREDENTIALS itself and parses the key file; when
the variable is unset every client falls back to ambient credentials, resolved through
GoogleAuth — Workload Identity, the metadata server or gcloud application default
credentials, whichever applies. Nothing else needs to change between the two.
export PROJECT_ID=my-project
gcloud iam service-accounts create backstage \
--project="${PROJECT_ID}" \
--display-name="Backstage catalog ingestion"
resource "google_service_account" "backstage" {
project = var.project_id
account_id = "backstage"
display_name = "Backstage catalog ingestion"
}
Permissions per provider¶
Every provider fails independently, so a missing role degrades one resource type rather than stopping ingestion. These are read-only permissions throughout — nothing in the catalog provider mutates anything in GCP.
| Provider | API calls made | Permissions | Predefined role equivalent |
|---|---|---|---|
bigquery |
list datasets | bigquery.datasets.get |
roles/bigquery.metadataViewer |
storage |
list buckets | storage.buckets.list, storage.buckets.get |
— (see below) |
cloudsql |
list instances | cloudsql.instances.list, cloudsql.instances.get |
roles/cloudsql.viewer |
pubsub |
list topics and subscriptions | pubsub.topics.list, pubsub.topics.get, pubsub.subscriptions.list, pubsub.subscriptions.get |
roles/pubsub.viewer |
secretmanager |
list secrets | secretmanager.secrets.list, secretmanager.secrets.get |
roles/secretmanager.viewer |
service-account |
list service accounts | iam.serviceAccounts.list, iam.serviceAccounts.get |
roles/iam.serviceAccountViewer |
clusters |
list clusters | container.clusters.list, container.clusters.get |
roles/container.clusterViewer |
Cloud Asset Inventory, for the access graph¶
One grant matters more than the rest. The IAM edges — who can reach which resource, and which
Kubernetes account impersonates which Google one — come from a single
searchAllIamPolicies call per project, which needs the Cloud Asset API and one role:
gcloud services enable cloudasset.googleapis.com --project="${PROJECT_ID}"
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:backstage@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/cloudasset.viewer
Without it every resource still ingests and the catalog is simply flat: no dependsOn between
service accounts and the resources they use, and no Workload Identity entities. The provider logs
one line per project and carries on.
roles/cloudasset.viewer reads policy metadata, not data — it grants no access to the contents of
any bucket, secret or database it can describe.
The providers added since, all read-only in the same way:
| Provider | API calls made | Permissions | Predefined role equivalent |
|---|---|---|---|
vpc |
list networks | compute.networks.list, compute.networks.get |
roles/compute.networkViewer |
subnets |
aggregated list of subnetworks | compute.subnetworks.list |
roles/compute.networkViewer |
firewall |
list firewall rules | compute.firewalls.list |
roles/compute.networkViewer |
routers |
aggregated list of routers (NAT is nested) | compute.routers.list, compute.routers.get |
roles/compute.networkViewer |
dns |
list managed zones | dns.managedZones.list |
roles/dns.reader |
instances |
aggregated list of instances | compute.instances.list |
roles/compute.viewer |
instance-groups |
aggregated list of instance group managers | compute.instanceGroupManagers.list |
roles/compute.viewer |
images |
list images | compute.images.list, compute.images.get |
roles/compute.viewer |
spanner |
list instances and their databases | spanner.instances.list, spanner.databases.list |
roles/spanner.viewer |
redis |
list instances across locations | redis.instances.list, redis.instances.get |
roles/redis.viewer |
alloydb |
list clusters and their instances | alloydb.clusters.list, alloydb.instances.list |
roles/alloydb.viewer |
bigtable |
list instances | bigtable.instances.list, bigtable.instances.get |
roles/bigtable.viewer |
firestore |
list databases | datastore.databases.list, datastore.databases.get |
roles/datastore.viewer |
managedkafka |
list clusters and their topics | managedkafka.clusters.list, managedkafka.topics.list |
roles/managedkafka.viewer |
eventarc |
list triggers | eventarc.triggers.list, eventarc.triggers.get |
roles/eventarc.viewer |
cloudtasks |
list locations, then queues | cloudtasks.queues.list, cloudtasks.locations.list |
roles/cloudtasks.viewer |
scheduler |
list locations, then jobs | cloudscheduler.jobs.list, cloudscheduler.locations.list |
roles/cloudscheduler.viewer |
run |
list services and jobs | run.services.list, run.jobs.list |
roles/run.viewer |
functions |
list functions | cloudfunctions.functions.list |
roles/cloudfunctions.viewer |
artifactregistry |
list repositories | artifactregistry.repositories.list |
roles/artifactregistry.reader |
And the rest — IAM, load balancing, CI/CD, security, observability, data and ML:
| Provider | API calls made | Permissions | Predefined role equivalent |
|---|---|---|---|
workload-identity |
search IAM policies, list clusters | cloudasset.assets.searchAllIamPolicies, container.clusters.list |
roles/cloudasset.viewer |
iam-roles |
list custom roles | iam.roles.list, iam.roles.get |
roles/iam.roleViewer |
loadbalancers |
list forwarding rules, URL maps, backends | compute.forwardingRules.list, compute.urlMaps.list, compute.backendServices.list |
roles/compute.networkViewer |
sslcertificates |
list SSL certificates | compute.sslCertificates.list |
roles/compute.networkViewer |
armor |
list security policies | compute.securityPolicies.list |
roles/compute.networkViewer |
addresses |
list addresses | compute.addresses.list, compute.globalAddresses.list |
roles/compute.networkViewer |
vpn |
list VPN gateways and tunnels | compute.vpnGateways.list, compute.vpnTunnels.list |
roles/compute.networkViewer |
cloudbuild |
list triggers | cloudbuild.builds.list, cloudbuild.builds.get |
roles/cloudbuild.builds.viewer |
clouddeploy |
list pipelines and targets | clouddeploy.deliveryPipelines.list, clouddeploy.targets.list |
roles/clouddeploy.viewer |
workflows |
list workflows | workflows.workflows.list |
roles/workflows.viewer |
composer |
list environments | composer.environments.list |
roles/composer.user |
dataproc |
list clusters per region | dataproc.clusters.list |
roles/dataproc.viewer |
kms |
list key rings and crypto keys | cloudkms.keyRings.list, cloudkms.cryptoKeys.list |
roles/cloudkms.viewer |
certificatemanager |
list certificates and maps | certificatemanager.certs.list, certificatemanager.certmaps.list |
roles/certificatemanager.viewer |
binaryauthorization |
get policy, list attestors | binaryauthorization.policy.get, binaryauthorization.attestors.list |
roles/binaryauthorization.policyViewer |
alerts |
list alert policies and uptime checks | monitoring.alertPolicies.list, monitoring.uptimeCheckConfigs.list |
roles/monitoring.viewer |
slos |
list services and SLOs | monitoring.services.list, monitoring.slos.list |
roles/monitoring.viewer |
logsinks |
list sinks | logging.sinks.list |
roles/logging.viewer |
filestore |
list instances | file.instances.list |
roles/file.viewer |
vpcconnectors |
list connectors | vpcaccess.connectors.list |
roles/vpcaccess.viewer |
memcache |
list instances | memcache.instances.list |
roles/memcache.viewer |
appengine |
list services | appengine.services.list |
roles/appengine.appViewer |
disks |
list disks and snapshots | compute.disks.list, compute.snapshots.list |
roles/compute.viewer |
bqtransfers |
list transfer configs | bigquery.transfers.get |
roles/bigquery.metadataViewer |
bqreservations |
list reservations | bigquery.reservations.list |
roles/bigquery.resourceViewer |
analyticshub |
list exchanges and listings | analyticshub.dataExchanges.list, analyticshub.listings.list |
roles/analyticshub.viewer |
datastream |
list streams | datastream.streams.list |
roles/datastream.viewer |
dataplex |
list lakes | dataplex.lakes.list |
roles/dataplex.viewer |
dataflow |
aggregated list of jobs | dataflow.jobs.list |
roles/dataflow.viewer |
vertex |
list endpoints and models per region | aiplatform.endpoints.list, aiplatform.models.list |
roles/aiplatform.viewer |
workbench |
list instances | notebooks.instances.list |
roles/notebooks.viewer |
Each of these also needs its API enabled in every project the provider enumerates. A project
whose API is off answers 403 SERVICE_DISABLED, which the provider logs as
Skipping project … the API is disabled and treats as empty — the other projects are still
ingested, and no entities are deleted for the ones that answered.
gcloud services enable \
cloudasset.googleapis.com \
compute.googleapis.com dns.googleapis.com spanner.googleapis.com redis.googleapis.com \
alloydb.googleapis.com bigtableadmin.googleapis.com firestore.googleapis.com \
managedkafka.googleapis.com eventarc.googleapis.com cloudtasks.googleapis.com \
cloudscheduler.googleapis.com run.googleapis.com cloudfunctions.googleapis.com \
artifactregistry.googleapis.com \
--project="${PROJECT_ID}"
Two things worth stating plainly:
- Secret Manager metadata only.
roles/secretmanager.viewerdoes not includesecretmanager.versions.access, so the plugin can enumerate secrets and never read their payloads. Do not substituteroles/secretmanager.secretAccessor. - Cloud Storage has no narrow predefined role for listing buckets.
storage.buckets.listat project level lives in broad roles such asroles/viewerandroles/storage.admin. Use a custom role instead of either.
One custom role¶
The least-privilege option, and the one to prefer: a single custom role covering every provider you enable.
# backstage-catalog-reader.yaml
title: Backstage catalog reader
description: Read-only inventory access for Backstage GCP ingestion
stage: GA
includedPermissions:
- bigquery.datasets.get
- storage.buckets.get
- storage.buckets.list
- cloudsql.instances.get
- cloudsql.instances.list
- pubsub.topics.get
- pubsub.topics.list
- pubsub.subscriptions.get
- pubsub.subscriptions.list
- secretmanager.secrets.get
- secretmanager.secrets.list
- iam.serviceAccounts.get
- iam.serviceAccounts.list
- container.clusters.get
- container.clusters.list
# Networking and compute
- compute.networks.get
- compute.networks.list
- compute.subnetworks.list
- compute.firewalls.list
- compute.routers.get
- compute.routers.list
- compute.instances.list
- compute.instanceGroupManagers.list
- compute.images.get
- compute.images.list
- dns.managedZones.list
# Databases
- spanner.instances.list
- spanner.databases.list
- redis.instances.get
- redis.instances.list
- alloydb.clusters.list
- alloydb.instances.list
- bigtable.instances.get
- bigtable.instances.list
- datastore.databases.get
- datastore.databases.list
# Messaging and eventing
- managedkafka.clusters.list
- managedkafka.topics.list
- eventarc.triggers.get
- eventarc.triggers.list
- cloudtasks.queues.list
- cloudtasks.locations.list
- cloudscheduler.jobs.list
- cloudscheduler.locations.list
# Serverless and artifacts
- run.services.list
- run.jobs.list
- cloudfunctions.functions.list
- artifactregistry.repositories.list
# The access graph
- cloudasset.assets.searchAllIamPolicies
- iam.roles.get
- iam.roles.list
# Load balancing and the network edge
- compute.forwardingRules.list
- compute.globalForwardingRules.list
- compute.urlMaps.list
- compute.backendServices.list
- compute.sslCertificates.list
- compute.securityPolicies.list
- compute.addresses.list
- compute.globalAddresses.list
- compute.vpnGateways.list
- compute.vpnTunnels.list
# CI/CD and orchestration
- cloudbuild.builds.list
- clouddeploy.deliveryPipelines.list
- clouddeploy.targets.list
- workflows.workflows.list
- composer.environments.list
- dataproc.clusters.list
# Security and keys
- cloudkms.keyRings.list
- cloudkms.cryptoKeys.list
- certificatemanager.certs.list
- certificatemanager.certmaps.list
- binaryauthorization.policy.get
- binaryauthorization.attestors.list
# Observability
- monitoring.alertPolicies.list
- monitoring.uptimeCheckConfigs.list
- monitoring.services.list
- monitoring.slos.list
- logging.sinks.list
# Misc infrastructure, data and ML
- file.instances.list
- vpcaccess.connectors.list
- memcache.instances.list
- appengine.services.list
- compute.disks.list
- bigquery.transfers.get
- bigquery.reservations.list
- analyticshub.dataExchanges.list
- datastream.streams.list
- dataplex.lakes.list
- dataflow.jobs.list
- aiplatform.endpoints.list
- aiplatform.models.list
- notebooks.instances.list
Trim the list to the providers you actually enable — every permission here maps to one row of the tables above.
gcloud iam roles create backstageCatalogReader \
--project="${PROJECT_ID}" \
--file=backstage-catalog-reader.yaml
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:backstage@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="projects/${PROJECT_ID}/roles/backstageCatalogReader"
resource "google_project_iam_custom_role" "backstage_catalog_reader" {
project = var.project_id
role_id = "backstageCatalogReader"
title = "Backstage catalog reader"
description = "Read-only inventory access for Backstage GCP ingestion"
permissions = [
"bigquery.datasets.get",
"storage.buckets.get",
"storage.buckets.list",
"cloudsql.instances.get",
"cloudsql.instances.list",
"pubsub.topics.get",
"pubsub.topics.list",
"pubsub.subscriptions.get",
"pubsub.subscriptions.list",
"secretmanager.secrets.get",
"secretmanager.secrets.list",
"iam.serviceAccounts.get",
"iam.serviceAccounts.list",
"container.clusters.get",
"container.clusters.list",
# Networking and compute
"compute.networks.get",
"compute.networks.list",
"compute.subnetworks.list",
"compute.firewalls.list",
"compute.routers.get",
"compute.routers.list",
"compute.instances.list",
"compute.instanceGroupManagers.list",
"compute.images.get",
"compute.images.list",
"dns.managedZones.list",
# Databases
"spanner.instances.list",
"spanner.databases.list",
"redis.instances.get",
"redis.instances.list",
"alloydb.clusters.list",
"alloydb.instances.list",
"bigtable.instances.get",
"bigtable.instances.list",
"datastore.databases.get",
"datastore.databases.list",
# Messaging and eventing
"managedkafka.clusters.list",
"managedkafka.topics.list",
"eventarc.triggers.get",
"eventarc.triggers.list",
"cloudtasks.queues.list",
"cloudtasks.locations.list",
"cloudscheduler.jobs.list",
"cloudscheduler.locations.list",
# Serverless and artifacts
"run.services.list",
"run.jobs.list",
"cloudfunctions.functions.list",
"artifactregistry.repositories.list",
]
}
# One binding per project the provider enumerates.
resource "google_project_iam_member" "backstage_catalog_reader" {
for_each = toset(var.ingested_project_ids)
project = each.value
role = google_project_iam_custom_role.backstage_catalog_reader.id
member = "serviceAccount:${google_service_account.backstage.email}"
}
The role is defined once, in the project that owns it, and bound in every project listed under a
provider's projects. A custom role defined at the organization level works the same way and
saves repeating the definition per project.
Predefined roles instead¶
If custom roles are awkward in your organization, bind the predefined equivalents — accepting
that storage then needs something broader than it should:
for ROLE in \
roles/bigquery.metadataViewer \
roles/cloudsql.viewer \
roles/pubsub.viewer \
roles/secretmanager.viewer \
roles/iam.serviceAccountViewer \
roles/container.clusterViewer \
roles/compute.viewer \
roles/dns.reader \
roles/spanner.viewer \
roles/redis.viewer \
roles/alloydb.viewer \
roles/bigtable.viewer \
roles/datastore.viewer \
roles/managedkafka.viewer \
roles/eventarc.viewer \
roles/cloudtasks.viewer \
roles/cloudscheduler.viewer \
roles/run.viewer \
roles/cloudfunctions.viewer \
roles/artifactregistry.reader
do
gcloud projects add-iam-policy-binding "${PROJECT_ID}" \
--member="serviceAccount:backstage@${PROJECT_ID}.iam.gserviceaccount.com" \
--role="${ROLE}"
done
Bucket access for Bruno and Secure Share¶
Both bucket-backed plugins are granted at the bucket, not the project, so they cannot reach any other bucket the catalog provider can see.
| Plugin | Needs | Role |
|---|---|---|
| Bruno | list and read report objects | roles/storage.objectViewer |
| Secure Share | create, read and delete chunks | roles/storage.objectAdmin |
gcloud storage buckets add-iam-policy-binding gs://my-ci-artifacts \
--member="serviceAccount:backstage@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/storage.objectViewer
gcloud storage buckets add-iam-policy-binding gs://example-secure-share \
--member="serviceAccount:backstage@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/storage.objectAdmin
resource "google_storage_bucket_iam_member" "bruno_reports" {
bucket = google_storage_bucket.ci_artifacts.name
role = "roles/storage.objectViewer"
member = "serviceAccount:${google_service_account.backstage.email}"
}
resource "google_storage_bucket_iam_member" "secure_share_chunks" {
bucket = google_storage_bucket.secure_share.name
role = "roles/storage.objectAdmin"
member = "serviceAccount:${google_service_account.backstage.email}"
}
Secure Share genuinely needs delete: the purge task removes ciphertext for expired and consumed pastes, and without it the bucket accumulates orphaned chunks whose database rows are long gone.
GKE and the Kubernetes plugin¶
Ingesting GKE clusters and reading what runs inside them are two different grants. The catalog provider only does the first.
The clusters provider emits a Resource per cluster annotated with the API server URL, the
cluster CA certificate and kubernetes.io/auth-provider: googleServiceAccount. That is exactly
what the Kubernetes plugin's catalog cluster locator consumes:
With that in place, cluster entities ingested from GCP become clusters the Kubernetes plugin can
query — no per-cluster block in app-config.yaml, and a new cluster appears on the next refresh.
1. Discovery¶
container.clusters.list and container.clusters.get, already covered by the custom role or by
roles/container.clusterViewer. This is enough for cluster entities to appear in the catalog.
2. Talking to the API server¶
Reading workloads needs authorization inside the cluster as well. Two ways, in order of preference:
In-cluster RBAC (least privilege). Bind the service account's email as a Kubernetes user.
GKE maps a Google identity to a User subject named by its email:
# backstage-reader.yaml
apiVersion: rbac.authorization.k8s.io/v1
kind: ClusterRoleBinding
metadata:
name: backstage-reader
subjects:
- kind: User
name: backstage@my-project.iam.gserviceaccount.com
apiGroup: rbac.authorization.k8s.io
roleRef:
kind: ClusterRole
name: view
apiGroup: rbac.authorization.k8s.io
gcloud container clusters get-credentials my-cluster \
--region=europe-west1 --project="${PROJECT_ID}"
kubectl apply -f backstage-reader.yaml
resource "kubernetes_cluster_role_binding" "backstage_reader" {
metadata {
name = "backstage-reader"
}
subject {
kind = "User"
name = google_service_account.backstage.email
api_group = "rbac.authorization.k8s.io"
}
role_ref {
kind = "ClusterRole"
name = "view" # or a narrower custom ClusterRole
api_group = "rbac.authorization.k8s.io"
}
}
The built-in view ClusterRole excludes Secrets, which is the right default. Replace it with a
custom ClusterRole if you want to narrow it further — the Kubernetes plugin reads Deployments,
Pods, Services, Ingresses, HPAs, StatefulSets, CronJobs, Jobs, ReplicaSets and Events.
Bind it in every cluster you expect to browse. A cluster that is ingested but never bound shows up in the catalog and returns authorization errors on its Kubernetes tab, which is a clearer failure than being silently absent.
Project-wide IAM (blunt). roles/container.viewer grants read access to Kubernetes objects
across every cluster in the project, with no per-cluster step. Convenient, and it applies to
clusters you did not intend to expose — including their ConfigMaps.
3. Reachability¶
The provider prefers the cluster's DNS-based control plane endpoint over the IP endpoint when one is configured. Either way the Backstage backend has to be able to reach it: a private cluster whose control plane is not reachable from where Backstage runs ingests fine and fails on every query. Authorized networks, Private Service Connect or running Backstage inside the same VPC all resolve that; the catalog entity is not the problem.
Workload Identity¶
The keyless option when Backstage itself runs on GKE. Bind the Kubernetes service account Backstage runs under to the Google service account:
gcloud iam service-accounts add-iam-policy-binding \
"backstage@${PROJECT_ID}.iam.gserviceaccount.com" \
--role=roles/iam.workloadIdentityUser \
--member="serviceAccount:${PROJECT_ID}.svc.id.goog[backstage/backstage]"
kubectl annotate serviceaccount backstage \
--namespace backstage \
"iam.gke.io/gcp-service-account=backstage@${PROJECT_ID}.iam.gserviceaccount.com"
resource "google_service_account_iam_member" "backstage_workload_identity" {
service_account_id = google_service_account.backstage.name
role = "roles/iam.workloadIdentityUser"
member = "serviceAccount:${var.project_id}.svc.id.goog[backstage/backstage]"
}
resource "kubernetes_service_account" "backstage" {
metadata {
name = "backstage"
namespace = "backstage"
annotations = {
"iam.gke.io/gcp-service-account" = google_service_account.backstage.email
}
}
}
[backstage/backstage] is [namespace/serviceaccount] — both have to match the deployment.
Leave GOOGLE_APPLICATION_CREDENTIALS unset so the clients pick up the workload identity token.
Key files, if you must¶
gcloud iam service-accounts keys create backstage-sa.json \
--iam-account="backstage@${PROJECT_ID}.iam.gserviceaccount.com"
Mount it as a secret and point the environment variable at the path:
It is a long-lived credential granting read access to your whole inventory — rotate it, keep it out of the image, and prefer Workload Identity wherever it is available.
Verifying¶
Check the grants without deploying anything, by impersonating the service account. This needs
roles/iam.serviceAccountTokenCreator on it for your own user:
SA="backstage@${PROJECT_ID}.iam.gserviceaccount.com"
gcloud storage buckets list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud sql instances list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud pubsub topics list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud secrets list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud container clusters list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud iam service-accounts list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
# BigQuery has no gcloud surface for this; use bq with an impersonated token.
CLOUDSDK_AUTH_ACCESS_TOKEN="$(gcloud auth print-access-token --impersonate-service-account="${SA}")" \
bq ls --project_id="${PROJECT_ID}"
# Networking and compute
gcloud compute networks list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud compute networks subnets list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud compute firewall-rules list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud compute routers list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud compute instances list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud compute instance-groups managed list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud compute images list --no-standard-images --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud dns managed-zones list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
# Databases
gcloud spanner instances list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud redis instances list --region=- --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud alloydb clusters list --region=- --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud bigtable instances list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud firestore databases list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
# Messaging, serverless and artifacts
gcloud managed-kafka clusters list --location=- --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud eventarc triggers list --location=- --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud tasks queues list --location=europe-west1 --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud scheduler jobs list --location=europe-west1 --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud run services list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud functions list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
gcloud artifacts repositories list --project="${PROJECT_ID}" --impersonate-service-account="${SA}"
Each command maps to one provider. Whichever fails is the provider that will log
error fetching GCP resources at the same point — or, for a 403, the narrower
Skipping project … the API is disabled that leaves the other projects alone.
For the Kubernetes side, confirm the RBAC binding resolves: