If you've spent years in AWS and now find yourself connecting to a GCP project for the first time, service accounts are usually the first thing that trips people up.
They look like IAM roles. They aren't quite IAM roles. And the defaults GCP nudges you toward — a JSON key with broad project access — are almost never what you actually want for something like database connectivity.
Here's what a service account actually is, how the permission model works, and how to scope one down to exactly what a tool (or a person) needs to read your Cloud SQL or Spanner instances — nothing more.
What a service account actually is
A GCP service account is an identity, not a person's login. It's meant for a program, a script, or a third-party platform to authenticate as — the equivalent of an AWS IAM role, but with a few structural differences that matter in practice:
-
It has its own email-like identifier (
something@project-id.iam.gserviceaccount.com), and you grant it IAM roles the same way you'd grant a human user roles. - It authenticates with a key, not a password. Usually a JSON key file you generate and download once, or short-lived credentials if you're running inside GCP already (a GKE pod, a Compute Engine VM).
- It's scoped to a project by default, which is the detail that catches people coming from AWS's account-wide IAM model off guard — see below.
The JSON key is the part to treat carefully. Anyone who has that file can authenticate as the service account until you rotate or revoke it. Unlike an AWS access key pair, there's no secondary secret — the whole file is the credential.
The permission model: roles, not policies
AWS IAM policies are JSON documents you write by hand, attaching specific actions and resources. GCP's model is role-based: you pick predefined roles (or build a custom one) and grant them to the service account, scoped to a project, folder, or individual resource.
For anything touching a database, three roles come up constantly:
roles/cloudsql.viewer — read instance metadata (engine, region, tier, status)
roles/cloudsql.client — permission to connect to Cloud SQL instances
roles/spanner.viewer — read Spanner instance metadata
Notice what's missing: nothing here grants the ability to create, modify, or delete an instance, and nothing grants write access to the data inside one. That's on purpose — cloudsql.client lets a service account connect, it doesn't touch data or schema by itself. Whatever connects using that credential still needs its own database-level permissions (a MySQL user, a Postgres role) to actually run queries once connected, and those should be read-only too if that's all the use case calls for.
The mistake people make here isn't malicious — it's convenience. roles/cloudsql.admin or, worse, roles/editor at the project level "just works" on the first try, so it's what ends up in a lot of scripts and internal tools. It also means that JSON key, if it ever leaks, can modify or delete infrastructure — not just read it.
Project scoping: the thing that's different from AWS
In AWS, one set of IAM credentials can usually see an entire account — all regions, all services, unless you've written a policy that restricts it. GCP inverts this: everything lives inside a project, and a service account only has the roles you've explicitly granted in that project (or inherited from a folder/org above it).
This is good for isolation — a compromised service account in a staging project can't touch production by default. It's bad for visibility: if your Cloud SQL instances are spread across five projects (production, staging, three regional projects), you need that service account's roles granted in every one of them, or you'll get an incomplete picture and no error telling you why.
This is the exact gap that shows up when teams try to build their own "list every database we have" script — it works fine until someone spins up a sixth project and the discovery script silently stops covering it.
Generating and handling the key
The mechanics are simple; the discipline around them is where most of the risk actually lives:
- In the GCP Console, go to IAM & Admin → Service Accounts, create one, and grant it only the roles it needs (start from
cloudsql.viewerandcloudsql.client, add more only if something concrete requires it). - Generate a key under the Keys tab — this downloads a JSON file. This is the only time you'll see the private key in plaintext.
- Store it in a secrets manager, not a repo, not a Slack message, not a shared drive folder. Treat it the way you'd treat an AWS access key with production read access, because functionally that's what it is.
- Set a reminder to rotate it. GCP doesn't force key rotation on you, which means it's easy to have a two-year-old key nobody remembers issuing.
If you're running the workload inside GCP itself (Compute Engine, GKE, Cloud Run), skip the JSON key entirely and attach the service account directly to the resource — you get short-lived, automatically rotated credentials instead of a static file to protect.
Applying this to database access specifically
For read-only database inventory and querying — which is the common case for any tool that needs to see what Cloud SQL or Spanner instances exist and connect to them — the minimum viable service account looks like this:
-
roles/cloudsql.viewerandroles/cloudsql.clientfor Cloud SQL -
roles/spanner.viewerfor Spanner, if you're using it - Granted at the project level, repeated for every project you need visibility into
- No
editor, noowner, nocloudsql.admin
That's the same scoping model 1DataCloud asks for when you connect a GCP project — a service account JSON limited to those viewer and client roles, nothing that can create, modify, or delete an instance. It's a useful baseline to hold any tool to, including ones you build in-house: if a script only needs to read instance metadata and connect for querying, its service account should never be able to do more than that.
The short version
- A service account is an identity for machines, authenticated by a key instead of a password — protect the key like a production credential, because it is one.
- Grant specific roles (
cloudsql.viewer,cloudsql.client,spanner.viewer), nevereditororadmin, for anything that only needs to read and connect. - Remember that GCP's permissions are project-scoped — a service account needs to be granted in every project you want it to see, unlike AWS's account-wide default.
- Rotate keys on a schedule, since GCP won't do it for you.
Get the scoping right once, and every tool you point at your GCP databases afterward — whether it's a script you wrote or something you connect — inherits the same minimal blast radius.
Coming from AWS to GCP and hit a permissions surprise I didn't cover here? Drop it in the comments — curious what else trips people up in that transition.
Top comments (0)