Chris Demeke/Notes
← Back to the article

Deploy a website with Cloud Run — a two-persona developer-experience study

Firsthand execution · 26 July 2026

The Google Codelab "Deploy a website with Cloud Run" (last updated 20 March 2026) was executed end to end, twice, by two deliberately different personas, against production Google Cloud. Nine steps, two fresh projects, every command run as printed.


1. Executive summary

Both personas finished. Neither finished the way the codelab says they will.

Persona A — new to Google Cloud Persona B — experienced
Environment Cloud Shell, as instructed local gcloud
Steps completed 9 / 9 9 / 9
Time 30m 26s (40m 07s raw) 6m 04s
Blocks 1, needed outside help 4, all self-recovered
Concepts to absorb 56 ~5
Believed remaining spend "a handful of MB" 356 MB + orphans

The central finding is not that the codelab is hard. It is that the codelab is accurate about everything except what happens when something goes wrong — and something goes wrong for both personas, at the same step, for the same reason.

Three results are worth a product owner's attention:

  1. A new project cannot reliably run step 4. Both personas hit PERMISSION_DENIED on their first Cloud Build. A controlled probe proved the cause is IAM propagation delay, not a missing role — and the error message actively prescribes the wrong fix.
  2. The cleanup step does not clean up. It reports success while leaving 356 MB billing in Artifact Registry. Persona A finished believing it had left behind "a handful of MB of tarballs."
  3. The newcomer got the worse error message. For the identical failure, Cloud Shell returned The caller does not have permission. while local gcloud named the permission, the principal, the resource, and supplied a troubleshooter URL.

Point 3 is the one that generalises. The person with the least context received the least information.


2. Method

Two personas, two isolated projects, one codelab.

Persona A was executed by a separate, smaller model in its own context under a written clean-room protocol: act only on what the page says; escape a failure only by re-reading the step, reading the literal error, and searching the exact error string; log every ambiguity before resolving it; hard cap ~10 minutes per blocker, then stop and request help. It logged a wall-clock timestamp at every step boundary.

Persona B was executed firsthand by the analyst, locally, with full documentation access — because that is what an experienced developer actually does. Persona B ran each command as printed first, then recovered using expert knowledge, so both personas are measured against identical starting commands.

Isolation was by project (cl-dx-a-0726, cl-dx-b-0726), created fresh. Without this, one persona enabling an API silently erases the other's step.

Measurement is two-axis: measured wall-clock seconds against a count of concepts the step requires the persona to hold. The concept inventory was built from the codelab text before either run, so the counts cannot have been shaped by knowing what broke.

Limits, stated up front


3. The two-axis result

Time cost against cognitive load, per step, per persona. Thresholds stated: high time >120s, high load >4 concepts.

Two structural results fall out of the data:

Every Persona B cell is low-load. The codelab asks an experienced developer to understand nothing they do not already understand. Their entire cost is time.

Persona A has no clean cells after step 1. Seven of nine steps are high-load and high-time.

And the sharpest single cell is step 1 — the "Before you begin" page, which asks the reader to do nothing at all and delivers 12 undefined concepts, the largest block in the codelab. It is cleared in 25 seconds and the vocabulary debt is carried into everything that follows. It reads as a friendly preamble and functions as the steepest cliff in the document.

Concept coverage

Of 56 distinct concepts the codelab introduces, exactly one is defined at first appearance (Project ID). Four more are partially explained. 51 are used without explanation — a definition coverage of 8.9%.

That is not automatically a defect: a codelab may reasonably assume prerequisites. But the codelab states its prerequisite as "General familiarity with Docker" and nothing else. Under its own stated contract, Knative, Cloud Build, Container Registry, revisions, concurrency, IAM service accounts, regions, and gcr.io are all in scope and all undefined.


4. Finding 1 — A new project cannot reliably run step 4

Both personas failed their first gcloud builds submit. The obvious conclusion is that the codelab omits an IAM step. That conclusion is wrong, and this study nearly published it.

The tell: the failing service account already held roles/editor, which already grants the exact permission the error named.

A controlled probe settled it. A third, independent project was created and the same operation attempted immediately:

Time Event
18:46:52 project created
18:47:35 attempt 1 — FAILED, iam.serviceAccounts.get denied
no IAM changes whatsoever
18:52:20 attempt 2 — SUCCESS, 24s build

The service account still held exactly one role, roles/editor, unchanged. And the two projects failed on different permissionsstorage.objects.get on one, iam.serviceAccounts.get on the other. A missing role fails identically every time; eventual consistency does not.

A new Google Cloud project has a window of roughly 3–5 minutes after creation during which Cloud Build fails with permission errors naming permissions the account demonstrably already has.

Why this matters more than the failure

The error message prescribes the wrong action. It names a permission, names a principal, and supplies an IAM Troubleshooter URL. Every signal points at editing IAM policy. The correct action is to wait sixty seconds.

The second failure mode is worse because it is silent and it scales. The analyst made exactly this error and caught it only because a pre-existing roles/editor binding happened to be visible in the output.

The asymmetry in the error text

Same underlying condition, two environments:

Cloud Shell   (Persona A):  PERMISSION_DENIED: The caller does not have permission.

local gcloud  (Persona B):  Error 403: 131568237952-compute@developer.gserviceaccount.com
                            does not have storage.objects.get access to ...
                            buckets/cl-dx-b-0726_cloudbuild/objects/source/...tgz
                            Remediate access with this Troubleshooter URL - https://...

The newcomer, in the environment the codelab prescribes, received no permission name, no principal, no resource, and no troubleshooter link.

Recommendation R1. Retry transient permission failures automatically in gcloud builds submit on projects younger than ~10 minutes, or add a first-class error: "This project was created recently. Permissions may still be propagating — wait 60s and retry." The information needed to produce that message (project creation time) is already available to the caller.

Recommendation R2. Bring Cloud Shell's error verbosity to parity with local gcloud. The current asymmetry inverts the relationship between need and information.


5. Finding 2 — Clean up does not clean up

Persona B ran all four cleanup commands exactly as printed. All four reported success. The post-cleanup audit:

Resource State after "successful" cleanup
Cloud Run service deleted — correct
Container image tags deleted — correct
Artifact Registry gcr.io repo 356.162 MB still stored
Orphaned source tarball 1.5 MB, unreachable by the cleanup
Enabled APIs 29, none disabled
Project never mentioned

The Artifact Registry figure is not stale metadata: the repository's updateTime (18:50:24) is after both image deletions. Deleting the two tags did not reclaim the underlying layers.

The orphaned tarball is a compounding defect. Cleanup command 3 derives its list from gcloud builds list — but the failed submit at 18:44:46 uploaded a tarball without ever creating a build record. It is therefore invisible to the cleanup and remains permanently. Every failed build leaves an unreachable object behind, and this codelab reliably produces at least one failed build (Finding 1).

The cleanup command fails in the environment the codelab prescribes

For Persona B, on a wide local terminal, cleanup command 3 worked and deleted two objects.

For Persona A, in Cloud Shell, the identical command failed 11 times and deleted nothing:

CommandException: The rm command (without -I) expects at least one URL.

Persona A diagnosed it using only on-page resources — the codelab's own commented-out diagnostic line — and found the cause: in Cloud Shell's narrow terminal pane, gcloud builds list renders one line per field (a vertical KEY: value block) rather than one line per build. The command's awk 'NR > 1 {print $4}' extracts column 4 of a table that isn't there, yielding empty strings.

The codelab's cleanup one-liner is terminal-width dependent. It fails in Cloud Shell — the environment the codelab instructs you to use — and works locally, which it never mentions.

What the newcomer believed

Persona A's log, written before any audit, records its honest belief about remaining spend:

"some small ongoing Cloud Storage charge (a handful of MB of tarballs from two builds) is the only likely leftover"

Actual: 356 MB in Artifact Registry, an unreachable orphan, and 29 enabled APIs. The mental model is wrong by roughly two orders of magnitude, and it is wrong in the direction that costs money.

Recommendation R3. Replace the awk pipeline with a format-stable command (gcloud builds list --format="value(source.storageSource.bucket,source.storageSource.object)"). Text-table scraping is fragile by construction and this instance breaks in the prescribed environment.

Recommendation R4. Add Artifact Registry repository deletion to the cleanup step, and state the true end state. The step's implicit contract is "you will not be charged." It does not deliver that.

Recommendation R5. Recommend deleting the project as the terminal cleanup action. It is the only operation that reliably reclaims everything, and it is the one thing the step never mentions.


6. Finding 3 — The codelab is written for Cloud Shell and silently punishes anyone else

The codelab's commands interpolate ${GOOGLE_CLOUD_PROJECT}, an environment variable injected by Cloud Shell that does not exist on a local machine. Verified locally: it is empty.

gcr.io/${GOOGLE_CLOUD_PROJECT}/monolith:1.0.0 collapses to gcr.io//monolith:1.0.0. The shell does not warn. The failure surfaces much later, from a different system, in this form:

Permission "artifactregistry.repositories.downloadArtifacts" denied on resource
"projects/monolith/locations/us/repositories/gcr.io" (or it may not exist)

The collapsed path parses monolith as the project name. So the error blames your account for lacking permission on a project you never created, never mentions the variable that caused it, and hedges with "or it may not exist." There is no path from this message to the actual cause.

The same Cloud-Shell assumption breaks the repo's own setup script. setup.sh branches on if [ -z "$CLOUD_SHELL" ] — it explicitly intends to support local execution — and then hardcodes Cloud Shell's own NVM location:

export NVM_DIR="/usr/local/nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
nvm install --lts

Off Cloud Shell the guarded source is skipped, nvm is never defined, the unguarded call fails, and set -eEo pipefail aborts the script before a single dependency installs. The local path is broken by construction: the script tests for the non-Cloud-Shell case and then does something only Cloud Shell can satisfy.

Recommendation R6. Replace ${GOOGLE_CLOUD_PROJECT} with $(gcloud config get-value project), which is correct in both environments and self-documenting.

Recommendation R7. Either fix setup.sh's local branch or delete it and state that Cloud Shell is required. A branch that advertises support it does not deliver is worse than no branch.


7. Finding 4 — Documentation drift is visible on the page

Captured from the live page on 26 July 2026, from a document dated 20 March 2026:

What the page shows What actually happens
Region prompt listing 5 regions 44 flat alphabetical entries, no marked default, no geo hint
Prompt order: region, then service name Service name asked first, then region
gcloud run services list as a single-line table vertical KEY: value block in Cloud Shell
"Select [1] Cloud Run (fully managed)" no platform prompt appears at all
Expected output timestamped 2019-09-16
"It may take a few moments for the Pod status to be Running" Cloud Run fully managed has no Pods — Kubernetes vocabulary left over from Anthos
**Note:**You can also view... markdown bold markers render literally, no space after colon

The region drift is the one that costs real time. The page says "select the region closest to you." The actual prompt offers 44 alphabetically-ordered entries with no default, no latency hint, and no geolocation. Persona A, having no basis to choose, fell back on matching the region family the codelab itself had used in an earlier step — a reasonable inference the page never invites.

This is worth noting precisely because concept-count scores it as zero load: "region" was already introduced. The metric cannot see a decision that introduces no new noun. It is carried here as a qualitative finding instead.

Recommendation R8. Mark a default region in the prompt, or order by measured latency from the caller. A 44-item alphabetical list is not a choice architecture.

Recommendation R9. Re-capture the codelab's terminal examples. Several show 2019-era output. The Pod status sentence is actively misleading for the product being taught.


8. What the codelab does well

Stated with equal weight, because a study that only finds faults is not measuring, it is prosecuting.


9. Persona-specific reading

Persona A — unfamiliar with Google Cloud

The codelab works for you, slowly, until it doesn't — and when it doesn't, you have no instruments. Your hardest moments are not the conceptually hard steps; they are the steps where the product tells you something untrue about why it failed. You will finish in roughly the advertised time if nothing goes wrong, and you will stop dead at step 4 if it does, because the error tells you to fix a permission you cannot fix and that was never the problem.

Your worst outcome is not failure. It is finishing successfully with a wrong model of what you are being billed for.

Persona B — experienced with Google Cloud

The codelab costs you nothing to understand and a surprising amount to execute — 6 minutes, but four separate blocks in those 6 minutes, none of which the page warns about. Every one is caused by the codelab assuming Cloud Shell while you work locally.

Your specific risk is the opposite of Persona A's: you are fast enough to recover from all four blocks without ever diagnosing them correctly. This study's own analyst "fixed" the IAM failure with three role grants that were almost certainly irrelevant, and would have published that as a finding.


10. Recommendations, consolidated

# Recommendation Owner Effort
R1 Auto-retry or explain transient permission failures on new projects Cloud Build / gcloud Medium
R2 Bring Cloud Shell error verbosity to parity with local gcloud Cloud Shell Medium
R3 Replace the awk-scraping cleanup with --format="value(...)" Codelab Trivial
R4 Add Artifact Registry deletion to cleanup; state the true end state Codelab Trivial
R5 Recommend project deletion as terminal cleanup Codelab Trivial
R6 Replace ${GOOGLE_CLOUD_PROJECT} with $(gcloud config get-value project) Codelab Trivial
R7 Fix or remove setup.sh's local branch Sample repo Low
R8 Mark a default region, or order by latency gcloud Medium
R9 Re-capture 2019-era terminal examples; remove Pod status Codelab Low

Five of nine are trivial edits to the codelab text. The two that require product work — R1 and R2 — are also the two that decide whether a newcomer finishes or abandons.


11. Open questions

  1. Is the propagation window bounded? The probe waited ~5 minutes and did not bisect. The 3–5 minute figure is bounded by observation, not measured.
  2. Does this reproduce under an Organization? Both projects were individual-account, no org policies. Enterprise IAM could change Finding 1 materially.
  3. Is the Cloud Shell error asymmetry deliberate? It could be an intentional verbosity setting rather than an oversight. Worth confirming before filing.
  4. How many other codelabs use the same awk-scraping cleanup? If the pattern is copied across the codelab corpus, R3 is a corpus-wide fix rather than a single edit.
  5. Would a human newcomer stop at step 4? Persona A stopped because its protocol told it to. A human might grind, might search more effectively, or might quit entirely. This is the question only a human study answers.

Environment

Date 2026-07-26
Codelab "Deploy a website with Cloud Run", last updated 2026-03-20
CLI Google Cloud SDK 577.0.0
Account individual Google account, no Organization
Projects cl-dx-a-0726, cl-dx-b-0726, cl-dx-probe-0726 — all created for this study
Node v26.0.0, npm 12.0.1