WHY BUILDING IN AWS MADE ME APPRECIATE GCPS IAM MODEL

A platform engineer’s guide to cross-boundary access and delegating role creation without losing your mind.

There is a specific kind of appreciation for Google Cloud’s IAM model that you only really develop after you’ve had to build a cross-account data upload pipeline in AWS.

If you are a platform engineer, you know the exact tension I am talking about. You want to give your application teams the autonomy to deploy their own workloads and configure their own compute identities. You want them moving fast. But you absolutely cannot hand them iam:CreateRole and just hope they don’t attach AdministratorAccess to a container running a Next.js app.

You need guardrails.

In AWS, solving this means navigating a complex web of trust policies, identity policies, resource policies, and Permissions Boundaries. It works, and it is highly secure, but it requires a lot of mental overhead.

Having started my cloud journey in GCP before spending significant time architecting in AWS, the contrast in how the two platforms handle identity delegation is stark. AWS asks, “What is this identity allowed to do?” GCP asks, “Who is allowed to use this resource?”

That single philosophical difference changes everything about how you build platform guardrails. Here is how delegating identity creation works across both clouds—and why GCP treating identities as resources is an absolute game-changer.


The AWS Reality: The Two-Sided Handshake And The Boundary

Let’s look at a standard architecture: you are building a secure cross-account data upload pipeline. Your application team is running a containerized Next.js frontend in Account B (Compute), and it needs to drop files securely into an S3 bucket in Account A (Data).

Because you are building this in a hardened environment, the networking and IAM are heavily scrutinized. To make this work, AWS requires a two-sided handshake.

  1. Account B (Identity Policy): The compute instance assumes an IAM Role. That role must have an identity policy granting s3:PutObject to the specific bucket ARN.
  2. Account A (Resource Policy): The S3 bucket must have a bucket policy explicitly granting access to Account B’s role ARN.

Without both sides agreeing, the connection fails.

But the real headache for platform engineers isn’t the handshake itself—it’s delegation. When the application team updates their deployment manifests and needs a new IAM role for their compute environment, how do you let them create it without submitting a ticket to the platform team?

If you just give the developers iam:CreateRole, they could theoretically attach AdministratorAccess to their new role. To prevent this privilege escalation, AWS requires you to use a Permissions Boundary.

As a platform engineer, you write a managed policy that defines the absolute maximum permissions allowed in that environment. You then give developers the ability to create roles, but only if they attach your boundary policy to it:

{
  "Condition": {
    "StringEquals": {
      "iam:PermissionsBoundary": "arn:aws:iam::111122223333:policy/AppDevBoundary"
    }
  }
}

It is a bulletproof system. But between the trust policies, the inline identity policies, the bucket policies, and the permissions boundaries, the mental overhead is massive.


The GCP Pivot: Identities As Resources

When you move this exact architecture into Google Cloud—Project B (Compute) needing to write to a Cloud Storage bucket in Project A (Data)—the mental model shifts entirely.

In AWS, an identity is just an identity. In GCP, a Service Account is both an identity and a resource.

This dual-nature fundamentally changes how you delegate access. You don’t need an equivalent to a Permissions Boundary because you don’t need developers to create roles from scratch. Instead, you use the “Service Account User” pattern.

Here is the golden path for delegation in GCP:

  1. Pre-provision: The platform team provisions the Service Account (app-runner@project-b.iam.gserviceaccount.com) with the baseline permissions required.
  2. Delegate Resource Usage: You grant the application team the roles/iam.serviceAccountUser role, bound strictly to that Service Account, not the whole project.

Now, the developers can attach that Service Account to their compute workloads. They have the autonomy to deploy, but they absolutely cannot edit the Service Account’s roles or elevate its permissions.

Because GCP treats the Service Account as a globally resolvable email address, the cross-project connection to the data bucket is a one-sided handshake. An administrator in Project A simply adds an IAM binding directly to the Cloud Storage bucket targeting the Service Account’s email.


Setting Up The Guardrails: A CEL Walkthrough

There are times, particularly in sandbox or non-prod environments, when a platform team wants to give a developer lead the actual ability to assign IAM roles.

Instead of dealing with boundaries attached to identities, GCP handles this restriction at the point of assignment using IAM Conditions. These are written in Common Expression Language (CEL).

Let’s walk through how a platform engineer sets this up. Our goal: Grant the developer lead the Project IAM Admin role, but restrict them so they can only assign specific, safe roles (like Viewer or Metric Writer).

Step 1: Define The Condition Logic

We use the iam.googleapis.com/modifiedGrantsByRole attribute. This evaluates the specific roles the user is attempting to grant or revoke in their API request.

The CEL expression looks like this:

api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly([
  'roles/compute.viewer',
  'roles/monitoring.metricWriter',
  'roles/logging.logWriter'
])

Step 2: Apply The Conditional Binding

As a platform engineer, you apply this binding to the Project. If you are using Terraform (which you likely are for platform configurations), it looks remarkably clean:

resource "google_project_iam_member" "delegated_admin" {
  project = "my-dev-project-b"
  role    = "roles/resourcemanager.projectIamAdmin"
  member  = "user:dev-lead@mycompany.com"

  condition {
    title       = "restrict_role_grants"
    description = "Only allow granting specific safe roles"
    expression  = "api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/compute.viewer', 'roles/monitoring.metricWriter', 'roles/logging.logWriter'])"
  }
}

If you prefer the gcloud CLI to test this out locally:

gcloud projects add-iam-policy-binding my-dev-project-b \
  --member="user:dev-lead@mycompany.com" \
  --role="roles/resourcemanager.projectIamAdmin" \
  --condition="expression=api.getAttribute('iam.googleapis.com/modifiedGrantsByRole', []).hasOnly(['roles/compute.viewer', 'roles/monitoring.metricWriter']),title=restrict_role_grants"

The Result

The developer lead now has IAM Admin privileges. But if they try to run a command or apply a Terraform state that grants roles/owner to themselves or their Service Account, the GCP API intercepts the request, evaluates the CEL condition, and outright denies the operation.


The Architect’s Takeaway

Working in AWS forces you to become an absolute expert in identity constraints. It is incredibly powerful, but it requires weaving together multiple policy types to ensure a secure perimeter.

Appreciating GCP comes down to recognizing the elegance of its resource hierarchy. By treating Service Accounts as resources that can be governed and attached, and by utilizing CEL conditions to evaluate grants in real-time, GCP removes the need for developers to dynamically generate custom IAM roles for every new workload.

Both platforms give platform engineers the tools to secure multi-tenant environments. But when you want to get out of the way of your application teams while still sleeping soundly at night, GCP’s resource-centric model feels like a breath of fresh air.

Written on September 22, 2026