Become SWE learning journal | 21 September 2026
Overview
We deployed a Python API to Google Cloud Run, connected GitHub Actions to Google Cloud without a downloaded service-account key, and loaded protected lesson data in the learner dashboard. The website remains on Cloudflare Pages. Auth0 identifies the learner, while the gateway and API check whether that learner may retrieve the data.
This milestone ends with a working lesson-reading API. The two practice lessons still come from a list in the Python application. Firestore progress storage was drafted, but its code was not deployed and a live database connection was not verified when this lesson was published. A successful API response is not evidence that a database exists.
What you will learn
- Distinguish a webpage, an API, a deployment pipeline and a database.
- Deploy a tested container from GitHub to Cloud Run.
- Separate the deployment identity from the running application's identity.
- Understand why Google Cloud IAM and Auth0 solve different access problems.
- Diagnose failures using command output, deployment checks and the actual browser runtime.
- Define a concrete test for the next database milestone.
Starting point
Lesson 8 introduced Auth0 login and role-aware navigation. Subsequent access-protection work added a Cloudflare Worker that protects workspace HTML and checks API access. We began this session with that website and a need for a real backend endpoint.
Earlier architecture lessons explored several hosting and database options. In this session we chose Cloud Run for the backend and planned Firestore for progress storage. This updates the earlier proposal to defer Google Cloud and use D1; it does not mean D1 or Firestore was already in use. The source repositories were private at publication, while the lesson pages and downloads were intentionally public.
How the application fits together
The browser signs in through Auth0. To load practice lessons, it sends an access token to the same-origin Cloudflare gateway. The gateway verifies access and forwards the request to a fixed Cloud Run endpoint. FastAPI validates the token again and returns JSON. The browser turns that JSON into lesson buttons and a detail view.
| Component | Responsibility in this milestone |
|---|---|
| Cloudflare Pages | Serves the HTML, CSS and browser JavaScript |
| Cloudflare Worker | Protects workspace pages and proxies allowed API requests |
| Auth0 | Issues the learner's access token after sign-in |
| FastAPI on Cloud Run | Validates identity and permission, then returns lesson JSON |
| GitHub Actions | Tests, builds, uploads and deploys backend changes |
| Artifact Registry | Stores the versioned container images |
| Firestore | Planned persistent storage; not verified live in this milestone |
Two flows to keep separate
The deployment flow begins when a tested change reaches the backend's main branch. GitHub Actions builds a container, authenticates to Google Cloud, pushes the image to Artifact Registry and deploys a Cloud Run revision. A health check compares the running version with the commit being deployed.
The learner flow begins in the browser and uses Auth0. A GitHub deployment identity never becomes the learner's identity. Likewise, a learner's Auth0 access token does not automatically grant Google Cloud IAM permissions.
The first API contract
| Route | Purpose | Expected result |
|---|---|---|
| GET /health | Check that the application is running | 200 with status and version |
| GET /api/v1/lessons | Read the two practice lessons | 200 JSON with valid learner access |
| GET /api/v1/lessons/{lesson_id} | Read one practice lesson | 200 for a known ID; 404 otherwise |
The dashboard calls /api/learner/lessons on its own origin. The gateway maps this to /api/v1/lessons on Cloud Run. These practice records, backend-01 and backend-02, are separate from the numbered public journal lessons.
Create the Google Cloud foundation
We created a dedicated development project under the organization, linked it to an existing billing account and opened Cloud Shell. The billing page confirmed that the project was linked. Cloud Shell then requested authorization to use the signed-in Google credentials; we authorized it before running the setup commands.
The following commands are sanitized examples of the setup. Replace YOUR_PROJECT with your own project ID. The original project numbers, account emails and repository IDs are deliberately omitted.
SWE_PROJECT=YOUR_PROJECT
SWE_REGION=europe-west1
gcloud services enable run.googleapis.com \
artifactregistry.googleapis.com iam.googleapis.com \
iamcredentials.googleapis.com sts.googleapis.com \
--project="$SWE_PROJECT"
gcloud artifacts repositories create learning-backend \
--repository-format=docker --location="$SWE_REGION" \
--description="Learning API container images" \
--project="$SWE_PROJECT"
These APIs support running containers, storing images and exchanging identities. The workflow builds with Docker in GitHub Actions, so this path did not require adding a Cloud Build trigger.
We created two service accounts: one for GitHub deployment and one for the API at runtime. Keeping them separate makes it easier to explain and constrain what each process can do.
| Identity | Permission granted | Scope used |
|---|---|---|
| Deployment account | Artifact Registry Writer | The container repository |
| Deployment account | Cloud Run Developer | The development project |
| Deployment account | Service Account User | The runtime account |
| Deployment account | Cloud Run Invoker | The development project, for checks |
| GitHub federated identity | Workload Identity User | The deployment account |
The deployment account was not given project-owner privileges. For a larger project, invocation and deployment permissions should be reviewed for narrower resource scopes. Minimum instances were set to zero and maximum instances to two for this learning service; those settings are not a spending cap.
Connect GitHub without storing a cloud key
We created a Workload Identity Federation pool and a GitHub OpenID Connect provider. GitHub supplies an identity assertion for a workflow run; Google checks its issuer and configured conditions before permitting the deployment service account to be used. This gives the workflow short-lived access without committing a service-account JSON key.
The provider used GitHub's token issuer and mapped the subject, repository ID, repository-owner ID and branch reference. Its condition required the expected numeric owner ID, numeric repository ID and refs/heads/main. Numeric IDs avoid relying only on names that could be reused. The service-account binding selected the expected repository ID within the pool.
Issuer
https://token.actions.githubusercontent.com
Condition with example placeholders
assertion.repository_owner_id == 'OWNER_ID' &&
assertion.repository_id == 'REPOSITORY_ID' &&
assertion.ref == 'refs/heads/main'
GitHub repository variables recorded the project, region, container repository, Cloud Run service, deployment account, runtime account, frontend origin and provider resource name. These identifiers locate resources; they are not a replacement for IAM authorization.
The workflow grants id-token: write only to the deployment job. That permission lets the job request GitHub's identity token. It does not give unrestricted write access to Google Cloud. The relevant authentication step is:
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@v4
- uses: google-github-actions/auth@v3
with:
project_id: ${{ vars.GCP_PROJECT_ID }}
workload_identity_provider: ${{ vars.WIF_PROVIDER }}
service_account: ${{ vars.DEPLOY_SERVICE_ACCOUNT }}
This is an excerpt, not a complete workflow. In the implemented pipeline, tests run before deployment; pull requests run tests without deploying. Deployments use commit-tagged images and the dedicated runtime service account. See the Google federation guide in the references for the complete trust configuration.
Manage the Google Cloud restrictions
GitHub access did not include cloud administration
The assistant could inspect and update the connected GitHub repositories, but did not have an authenticated Google Cloud administration connection for the project. Signing into Cloud Shell in my own browser did not give the assistant access to that shell.
We handled this by splitting the work. The assistant prepared code, workflow changes and precise commands. I ran the project setup, service-account creation, IAM bindings and federation commands in my authorized Cloud Shell session, then shared the results. After that setup, GitHub Actions could perform the permitted deployments automatically.
Deployment permission did not include every IAM change
Cloud Run Developer allowed application deployment but did not include the IAM-policy permission needed to disable the Invoker IAM check. We used an account with the necessary permissions in Cloud Shell for that service setting rather than expanding the deployment account to project owner.
Cloud Run's default invoker check expects a Google identity authorized through IAM. Our learner application uses Auth0 access tokens. To let those requests reach FastAPI, we disabled the Cloud Run Invoker IAM check for this service and retained token validation and learner permission checks inside the application.
gcloud run services update YOUR_SERVICE \
--project=YOUR_PROJECT --region=europe-west1 \
--no-invoker-iam-check
This makes the service reachable without a Google IAM identity; it does not remove the application's Auth0 checks. The health route is public. The lesson routes still reject missing or forged Auth0 tokens. This setting belongs on an application designed to enforce its own access checks.
Google documents this option for projects with domain-restricted sharing, where an allUsers binding can be unsuitable. We did not establish that a specific organization policy caused our failure, so the lesson does not attribute it to one. An organization can also require the Invoker IAM check; such a policy needs administrator review.
A copy and paste error was a separate problem
The first attempt contained a duplicated command and the incomplete flag --no-invoker-iam-chec. Cloud Shell reported unrecognized arguments. Pasting the corrected command once, with the complete --no-invoker-iam-check flag, produced the confirmation that the service had been updated. Syntax errors and permission errors need different fixes.
Protect the API and connect the dashboard
FastAPI uses the Auth0 integration to verify the token signature, issuer and audience. The application also checks expiry, issue time, optional not-before time, a nonempty subject, the authorized client and the explicit learner:access permission. Merely decoding a token or hiding a menu item would not perform these checks.
The production API attaches the verified access dependency to both lesson routes. This short excerpt shows the route structure; learner_access is the full validating dependency in the downloadable API example.
@app.get(
"/api/v1/lessons",
response_model=list[Lesson],
dependencies=[Depends(learner_access)],
)
def lessons():
return LESSONS
The Cloudflare gateway independently verifies the access token and explicit learner permission. It forwards requests to a fixed backend host, passes the Authorization header and omits unrelated browser cookies. Authenticated responses use private, no-store caching. CORS limits supported browser origins; it is not the authorization mechanism.
Fix the cached browser script
The dashboard initially continued to use an older auth.js URL. Updating the script's version query made the browser request the revised integration. We then checked the signed-in page again, including lesson loading, selecting a lesson and reloading the page.
Test the real Cloudflare runtime
The proxy originally used redirect: "error". Cloudflare's runtime rejected that option, even though familiar browser or Node fetch behavior could make it look reasonable. We reproduced the failure in Miniflare using workerd and changed the proxy to redirect: "manual".
The proxy now accepts only its expected status codes and JSON responses. It rejects redirects instead of forwarding the learner token to another location. A regression test exercises the assembled Worker and checks 301, 302, 307 and 308 responses.
// Excerpt after gateway authentication succeeds.
const upstream = await fetch(fixedBackendUrl, {
headers: { Authorization: `Bearer ${token}` },
redirect: "manual"
});
// Validate allowed status and JSON before returning.
The last comment is essential behavior implemented in the production proxy, not an instruction to return every upstream response. Runtime-specific tests caught a problem that a successful build alone did not settle.
Check what actually worked
The backend workflow passed its API tests, built and deployed the container, checked the running commit version and verified that missing or forged Auth0 tokens were rejected. The frontend workflow passed navigation and session tests, gateway tests, browser tests, the matching Cloudflare preview checks and production smoke checks.
| Check | Evidence and result |
|---|---|
| API token validation | Automated tests cover wrong signature, issuer, audience, client, expiry and future claim times |
| API permission enforcement | Automated tests reject missing learner permission with 403 |
| Lesson routes | Automated tests cover the catalogue, one lesson and a missing lesson |
| Deployed authentication | Workflow confirms missing and forged tokens receive 401 from the application |
| Worker runtime | workerd regression test covers a successful proxy request and rejected redirects |
| Learner browser | Live signed-in session loaded both practice lessons, opened details and survived reload |
| Database persistence | Not tested live; Firestore was still the next milestone |
The successful backend run reviewed for this lesson was 35599379625 at commit 87ec177. The frontend run was 35604700590 at commit 57eac9a. These are historical evidence references, not a claim that future deployments will pass. Run links are in the private repository evidence note.
An earlier Cloud Shell request used a Google identity token to reach the IAM-protected service and returned the two practice lessons. That proved that the deployed route responded at that stage. It did not prove the later Auth0 browser integration or a database write; those require their own tests.
Repeat the API exercise
Sign in to the learner dashboard and open the browser's Network tab. Select the request ending in /api/learner/lessons. Observe GET, the response status and the JSON response. Open one practice lesson and inspect its detail request. Reload and confirm the catalogue appears again.
Do not copy the Authorization header, cookies or tokens into notes or screenshots. To test the unauthenticated boundary, open the gateway API URL without an authenticated API request and expect 401. To test permission denial or malformed tokens, use the automated synthetic-token tests rather than editing a real credential.
What remains for the database milestone
Firestore is a Google Cloud document database. It was selected for the next exercise so that completion can survive application restarts and new browser sessions. At publication, progress code and tests had been drafted locally; they were not in the deployed backend or website. No live Firestore completion record had been verified.
The planned API uses the learner identity from the verified token, never a user ID supplied by the page. GET /api/v1/me/progress will read that learner's progress. PUT /api/v1/me/progress/{lesson_id} will accept a strict completed boolean and require progress:manage as well as learner access. These are planned routes, not available features of the published API milestone.
The remaining cloud setup is to enable the Firestore API, create or verify the intended Native-mode database and grant the runtime account the necessary data-access role. The proposed role is roles/datastore.user. The deployment account's existing role list does not grant database provisioning or project IAM administration. That setup still requires an appropriately authorized cloud account.
The completion test for the next lesson
- Deploy and test the progress API and dashboard controls.
- Mark a practice lesson complete and read it back from the database.
- Reload, sign out and sign in again; confirm the record remains.
- Use a second learner to confirm each account sees only its own progress.
- Undo completion and confirm the next read reflects the change.
- Test a database outage and ensure the page reports failure rather than invented zero progress or a successful save.
Emulator tests can verify ownership and persistence through separate database clients before deployment. They do not prove that production IAM and the real database are configured correctly. The live round-trip test remains necessary.
Takeaways and checklist
- Completed: a protected lesson-reading API, automated GitHub deployment and a working browser connection.
- Completed: separate deployment and runtime identities, with GitHub federation rather than a downloaded cloud key.
- Completed: Cloud Shell setup and the service access setting, followed by application-level denial checks.
- Still pending: a live database, deployed progress controls and the per-learner persistence test.
The next lesson is saving and retrieving learner progress with Firestore. The key learning goal is to trace one deliberate change from the browser request through identity checks to a database record, then verify that a new session reads the same state.
References and reusable files
The command examples use placeholders and the API example uses environment variables. They contain no production project numbers, account addresses, keys or tokens. Inspect the examples and supply your own configuration before running them.
- Cloud Run public access and Invoker IAM checks
- Workload Identity Federation for deployment pipelines
- Google GitHub Actions authentication
- Cloudflare Worker Request redirect behavior
- Firestore overview
- Firestore server access through IAM
- Firestore emulator
Read Lesson 9 online or return to Lesson 8.
The lesson folder contains this Markdown source, an editable Word document, its PDF, a sanitized FastAPI example and a short command reference. Repository access is required to browse the private source; the public website provides the lesson and its downloads.