GCP for AWS engineers: how your AWS instincts translate (and where they break)
You already know how to organize accounts, hand out permissions, and bootstrap state in AWS. Here's how to do the same things in Google Cloud - the concepts that map cleanly, and the four places your AWS muscle memory will actively mislead you.
If you're fluent in AWS and now staring at Google Cloud, the good news is that ~80% of your instincts transfer directly: there's a hierarchy to organize things, a role-based way to grant access, an OIDC story for keyless CI, and a "cold-start your state backend" bootstrap dance. The bad news is the other 20% - and it's concentrated in a few high-traffic spots where doing the AWS thing produces a confusing denial rather than an obvious error. This post is the translation layer: the same jobs you do in AWS, done in GCP, with the traps called out.
The short version, if you only read one paragraph: GCP's hierarchy (organization -> folder -> project -> resource) maps cleanly onto AWS Organizations -> OU -> account -> resource, but three things break your muscle memory. IAM is an allow-only policy bound to the resource tree and inherited downward - there is no policy attached to a user. A project's service APIs all start switched off, and nothing works until you enable each one. And you don't assume roles, you impersonate service accounts, which are themselves resources with their own access policy. Everything below expands those.
The biggest shift: permissions live on the tree, not on identities
In AWS, the mental model is "attach a policy to an identity." You create a user or a role, staple JSON to it, and the permission travels with the principal. Resource policies exist, but the identity-attached policy is the center of gravity.
GCP inverts that. An IAM policy is a set of bindings - (member, role) pairs - attached to a node in the resource
hierarchy (organization, folder, project, or an individual resource). The policy lives on the thing being accessed,
not on the identity doing the accessing. Your effective access at any point is the union of every binding inherited
from the root down to that node. Grant roles/compute.admin to a group at the folder level and every project under
that folder inherits it.
If you've touched Azure RBAC this will feel familiar - it's role-at-a-scope with downward inheritance. The rule that saves you:
When GCP denies something, stop hunting for "the policy on the user." Look for the binding on the org, folder, project, or resource - and remember it inherits down, so the grant that matters may be three levels up.
A few specifics that differ from AWS:
- Roles come in three grades. Primitive roles (
roles/owner,roles/editor,roles/viewer) are the coarse legacy trio - avoid them in real estates. Predefined roles are the fine-grained per-service roles you should actually use (roles/storage.objectViewer,roles/compute.instanceAdmin.v1, ...). Custom roles are yours to author when the predefined set is too broad. - The base model is additive and allow-only. There is no per-statement
Effect: Denywoven into every policy the way there is in AWS. Effective access is just the union of allows. - Deny is a separate layer. When you do need a hard "never, regardless of grants," you write an IAM deny
policy - a distinct object attached to org/folder/project that is evaluated before allows. It's the closest thing
to an inline
Denystatement, but it's deliberately out-of-band. - Guardrails are a third thing again: the Organization Policy Service. Constraints like
constraints/compute.vmExternalIpAccessorconstraints/iam.disableServiceAccountKeyCreationare the GCP analog of SCPs. They attach at org/folder/project, inherit down, and restrict what can exist or be configured rather than who can call what. Think "SCP-shaped guardrail," not "IAM role."
So where AWS gives you one allow/deny grammar inside IAM, GCP spreads the job across allow bindings, deny policies, and org-policy constraints. Same outcomes, three mechanisms.
Everything is a project - and its APIs start switched off
The project is GCP's fundamental unit. It's the rough analog of an AWS account: an isolation boundary, an IAM scope,
and a billing target all at once. But a project is far lighter than an account - cheap to create, easy to delete,
meant to be made by the dozen. The idiomatic pattern is one project per workload per environment (app-qa, app-prod),
not a handful of shared accounts carved up by tags.
Three things about projects have no clean AWS equivalent:
- A project has three identifiers, and the differences bite. The project ID is a globally unique, human-chosen,
immutable string (
acme-app-prod-7f3a) - it's what you put in almost every command and resource path. The project number is a globally unique integer GCP assigns. The display name is mutable and cosmetic. Pick the ID carefully; you can never change it. - Service APIs are off by default. This is the single most common first-day face-plant. Before you can create a VM,
you enable
compute.googleapis.com; before a bucket,storage.googleapis.com; and so on, per project. Your first "denial" on GCP is usually not a missing IAM role at all - it'sAPI [compute.googleapis.com] not enabled on project. In AWS, services are simply there; in GCP, every project is a blank slate and you switch on exactly the surface you intend to use. (In Terraform this isgoogle_project_service; from the CLI,gcloud services enable.) - Projects are the natural blast-radius and quota boundary. Quotas, budgets, and most defaults are per project, so spinning up a fresh project for an experiment is the normal, cheap move - not the ceremony an AWS account is.
Service accounts: you impersonate, you don't assume
In AWS, a role is a set of permissions that a principal assumes through STS, gated by a trust policy. In GCP the equivalent workhorse is the service account (SA), and it works differently in a way that trips up every AWS engineer.
A service account is both an identity and a resource. It has an email
(deployer@acme-app-prod.iam.gserviceaccount.com), it lives inside a project, and - crucially - it has its own IAM
policy governing who may use it. That second half is the part with no AWS reflex:
- To act as a service account, a principal needs a role on the SA itself -
roles/iam.serviceAccountTokenCreator(to mint short-lived tokens and impersonate it) orroles/iam.serviceAccountUser(to attach it to a resource you're creating, like a VM or a Cloud Run service). This is the two-sided grant AWS folks forget: giving your CI principal broad project rights is useless if it can't become the deployer SA. - You impersonate an SA by asking for a token (
generateAccessToken), not by assuming a role with a trust-policy handshake. The permission to impersonate lives as an ordinary IAM binding on the SA resource - there's no separate trust document. - Service account keys exist and you should mostly avoid them. A downloaded JSON key is a long-lived credential and a classic leak vector; many orgs disable key creation org-wide via the constraint mentioned earlier. The keyless alternatives are the ones to reach for.
- Keyless CI is Workload Identity Federation - the same OIDC idea as AWS's OIDC federation. Your GitHub Actions or external workload presents an OIDC token, a workload identity pool trusts the issuer, and GCP hands back short-lived credentials for an SA. No stored secret. (Inside GKE, the analog is Workload Identity, which binds a Kubernetes service account to a Google service account - the direct counterpart of AWS's IRSA.)
The one-line translation: an AWS role you assume via trust policy becomes a GCP service account you impersonate via a token-creator binding on the SA.
The identity domain: Cloud Identity, Workspace, and the org
GCP does separate identity from resources, but far more gently than Azure does. The organization node is created
against a verified domain that is owned by a Cloud Identity or Google Workspace account. That directory - users
and groups - is administered in the Admin console (admin.google.com), a different surface from the Cloud console where
you manage resources.
- Users and groups are minted in the directory; access is granted in IAM. You don't create a "GCP user" the way you create an IAM user in AWS. The human exists in Cloud Identity/Workspace (or is federated in from your IdP), and you reference them - ideally as a group - in IAM bindings on the resource tree. Best practice is bindings to groups, never to individual users.
- There's no free-standing "IAM users" store like AWS has. External IdPs (Okta, Entra ID, and so on) federate into Cloud Identity; that's the norm for workforce access.
- Two footguns to memorize: the special members
allUsers(literally anyone on the internet, unauthenticated) andallAuthenticatedUsers(anyone with any Google account). Binding a role to either is how buckets accidentally go public. Treat them the way you treat aPrincipal: "*"on an S3 bucket policy.
The loose analogy: the organization plus its Cloud Identity domain is roughly "an AWS Organization fused with IAM Identity Center's directory" - but the day-to-day resource work happens through IAM bindings on the tree, not through identity-attached policy.
Billing is a separate object you link to projects
A billing account in GCP is its own resource - it is not a node in the org -> folder -> project hierarchy. Projects link to a billing account (each project to exactly one), and a single billing account can fund many projects.
- It has its own IAM.
roles/billing.admin,roles/billing.user,roles/billing.creatorlive on the billing account, separate from your resource roles. In particular, to attach a new project to a billing account you needroles/billing.useron that billing account - broad project rights alone won't do it. - "I can deploy" tells you nothing about "I can create a chargeable project." To stand up a project that can incur
charges you need both
resourcemanager.projectCreator(at the org/folder) andbilling.user(on the billing account). Miss the second and project creation half-succeeds into a project that can't actually run anything.
This is softer than Azure's hard commerce-plane wall - GCP billing is wired into Cloud IAM, so it shows up in the same
gcloud and Terraform surfaces rather than a wholly separate world. But it's still a distinct object with distinct
roles, and it's the second most common "but I'm an admin" surprise after unenabled APIs.
Names, IDs, and one networking surprise
Every GCP resource has a relative resource name like projects/acme-app-prod/zones/us-central1-a/instances/web-1
(and a fully qualified //compute.googleapis.com/... form). The project ID travels with every log line and API call -
the same job an ARN's account does in AWS - so, as in Azure, your resource names can omit what the project already
encodes. The same short names can safely repeat across your -dev, -qa, and -prod projects.
The networking gotcha worth flagging up front, because it silently violates AWS instincts: a VPC network in GCP is a global resource, and its subnets are regional. In AWS a VPC is scoped to one region; in GCP one VPC spans every region, and you carve regional subnets inside it. A single global VPC with regional subnets is the default, not an exotic multi-region setup - don't reach for VPC peering to do what a subnet already gives you.
The AWS -> GCP quick map
Keep this next to you for the first month:
- Organization -> organization. Same idea, but the GCP org is bound to your Cloud Identity/Workspace domain.
- Organizational Unit (OU) -> folder. Nestable grouping node; folders can contain folders.
- Account -> project. The isolation, IAM, and billing unit - but lightweight and disposable, made by the dozen.
- SCP (guardrail) -> Organization Policy constraint at an org/folder/project scope - restricts what can exist or be configured, inherits down.
- IAM identity-attached policy -> IAM binding
(member, role)on a tree node. No policy on the user; the grant lives on the resource and inherits downward. - IAM role you assume (via STS + trust policy) -> service account you impersonate (via a token-creator binding on the SA). Remember the two-sided grant.
- Explicit
Denystatement -> IAM deny policy - a separate object, evaluated before allows. - IRSA (IAM Roles for Service Accounts) -> Workload Identity (GKE) / Workload Identity Federation (external CI) - the same OIDC idea, keyless.
- "The service is just available" -> enable its API per project first (
google_project_service). No AWS equivalent. - ARN in logs -> the resource name (
projects/<id>/...); project context is structured, not name-encoded.
Bootstrapping Terraform state, translated
Here's a place where your AWS runbook almost works, then breaks at step zero.
The AWS bootstrap you know: cold-start a state backend in the management account (local state, then migrate the backend into itself), keep the per-OU backends' state in that root backend, and create member accounts using their own backends. The invariant that makes it clean is that you can always bootstrap from the management account, because it always exists and can hold an S3 bucket.
GCP breaks that invariant at genesis the same way Azure does: the always-exists object is the organization, but an org
node can't hold resources. A GCS bucket lives in a project, and a project needs a parent and (to do anything real) a
billing link and enabled APIs. So GCP's "root account" is a seed project you crown consciously - the idiomatic name
is something like prj-bootstrap or a Cloud Foundation Toolkit seed project.
The translated flow, when the org and a billing account already exist (the common case):
- One cold-start, ever: create the seed project imperatively (
gcloud projects create), enable the bootstrap APIs on it (cloudresourcemanager,cloudbilling,serviceusage,iam,storage), link billing, and create the GCS state bucket with local Terraform state - theninit -migrate-stateinto that bucket. - A bootstrap service account with org-level roles: grant it
resourcemanager.projectCreator,billing.user, and your org-policy/IAM admin roles at the organization node, so it can stamp out and govern every downstream project. - Everything else is a normal apply: additional projects, folders, and their resources are created by Terraform
impersonating that bootstrap SA, each project's state living under a
prefixin the one GCS backend.
From absolute scratch, the ordering is forced - seed project first, state bucket second - because a backend is a GCS
bucket and a bucket needs a project to live in. And there's a small chicken-and-egg wrinkle just like Azure's: the
google provider needs a project and enabled APIs to do anything, so you make that very first project and enable its
APIs with an imperative gcloud call, then adopt it into Terraform afterward. (The terraform-google-modules/bootstrap
module packages exactly this seed-project-plus-SA dance.)
And here's where GCP is genuinely simpler than AWS muscle memory: backend-in-seed-project plus resources-in-member
needs none of the AWS hub-and-spoke trust machinery - no backend role_arn, no provider assume_role, no two-sided
trust policies. An org-level service account with the right roles works org-wide because IAM inherits down; the provider
"hops" between projects by simply setting project; and it becomes the deployer identity by setting
impersonate_service_account. Cross-project is a parameter, not a trust negotiation. (You'll tighten this later with
custom roles, per-project SAs, and dedicated CI identities - but even then it's role bindings at scopes, never a
trust-policy handshake.) If you want to go deep on codifying either side, the
HashiCorp Terraform Authoring & Operations Pro exam is built around exactly these state and
provider patterns.
The four places your AWS instincts will actively mislead you
If you forget everything else, remember these:
- There is no policy on the user. Stop looking for the JSON stapled to an identity. Permissions are
(member, role)bindings on the org, folder, project, or resource, and they inherit down. The grant you need may be three levels up. - Nothing works until you enable the API. Your first 403 on a new project is usually
API not enabled, not a missing role. Every service is off until you switch it on, per project. - You impersonate service accounts, you don't assume roles. And the SA is a resource with its own access policy - giving your principal broad project rights is useless without a token-creator binding on the SA.
- Billing is a separate object. "I can deploy anything" says nothing about "I can create a chargeable project or
link a billing account." Different resource, different roles -
billing.useron the billing account, not project admin.
Which certifications drill each side
Cloud governance - hierarchy, identity, guardrails, and state - is the backbone of the architecture and administration exams on both clouds, so studying for them is also the fastest way to make the concepts above stick. If you already hold the AWS credential, the Google Cloud one on the same row is your natural next step.
On the AWS side (the knowledge you're translating from):
- AWS Certified Cloud Practitioner (CLF-C02) - the foundations of accounts, IAM, and Organizations.
- AWS Certified Solutions Architect - Associate (SAA-C03) - IAM, multi-account structure, and core architecture.
- AWS Certified Solutions Architect - Professional (SAP-C02) - multi-account Organizations, SCPs, and cross-account access at scale.
- AWS Certified Security - Specialty (SCS-C03) - IAM depth, SCPs, and key management.
On the Google Cloud side (the knowledge you're translating to):
- Google Cloud Digital Leader - organizations, projects, IAM, and billing at a foundational level.
- Google Cloud Associate Cloud Engineer - the day-to-day plane: projects, IAM bindings, service
accounts, and
gcloud. - Google Cloud Professional Cloud Architect - the org/folder/project hierarchy, landing-zone design, and governance at scale.
- Google Cloud Professional Cloud Security Engineer - IAM depth, org policy, service-account security, and key management.
A pragmatic path if you're AWS-certified today: start at the Cloud Digital Leader to map the vocabulary, then jump to the Associate Cloud Engineer (which lives in the project-and-IAM plane you'll use most), and add the Professional Cloud Architect or Professional Cloud Security Engineer depending on whether your work leans architecture or security.
The bottom line
GCP isn't harder than AWS - it's factored differently. AWS staples permissions to identities, hands you a heavyweight account with everything switched on, and folds most authority into IAM. GCP hangs permissions on a resource tree that inherits downward, gives you cheap disposable projects that start as blank slates, and makes you impersonate service accounts instead of assuming roles. Translate the concepts once - policies move from identities to the tree, accounts become projects with their APIs off, roles become service accounts you impersonate, and billing is a separate object you link - and the rest is vocabulary. Drill it with the certs above, and the parts that felt like arbitrary denials start reading as a coherent, deliberate design.