Lesson 09 · Backend and deployment

Deploy a backend API from GitHub to Google Cloud.

Build a protected API, connect it to the learner dashboard, and work through cloud access restrictions. See what is running and what the database milestone still needs.

Read this lesson online. Sign in or create an account to download the PDF.

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.

ComponentResponsibility in this milestone
Cloudflare PagesServes the HTML, CSS and browser JavaScript
Cloudflare WorkerProtects workspace pages and proxies allowed API requests
Auth0Issues the learner's access token after sign-in
FastAPI on Cloud RunValidates identity and permission, then returns lesson JSON
GitHub ActionsTests, builds, uploads and deploys backend changes
Artifact RegistryStores the versioned container images
FirestorePlanned 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

RoutePurposeExpected result
GET /healthCheck that the application is running200 with status and version
GET /api/v1/lessonsRead the two practice lessons200 JSON with valid learner access
GET /api/v1/lessons/{lesson_id}Read one practice lesson200 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.

IdentityPermission grantedScope used
Deployment accountArtifact Registry WriterThe container repository
Deployment accountCloud Run DeveloperThe development project
Deployment accountService Account UserThe runtime account
Deployment accountCloud Run InvokerThe development project, for checks
GitHub federated identityWorkload Identity UserThe 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.

CheckEvidence and result
API token validationAutomated tests cover wrong signature, issuer, audience, client, expiry and future claim times
API permission enforcementAutomated tests reject missing learner permission with 403
Lesson routesAutomated tests cover the catalogue, one lesson and a missing lesson
Deployed authenticationWorkflow confirms missing and forged tokens receive 401 from the application
Worker runtimeworkerd regression test covers a successful proxy request and rejected redirects
Learner browserLive signed-in session loaded both practice lessons, opened details and survived reload
Database persistenceNot 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

  1. Deploy and test the progress API and dashboard controls.
  2. Mark a practice lesson complete and read it back from the database.
  3. Reload, sign out and sign in again; confirm the record remains.
  4. Use a second learner to confirm each account sees only its own progress.
  5. Undo completion and confirm the next read reflects the change.
  6. 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.

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.

Lesson downloads

The API works. Persistence comes next.

The next exercise will save one learner’s completion record, reload it in a new session, and test that another learner cannot change it.

Download the complete lesson PDF →

Contact · Budapest

Let’s build a conversation.

Collaborations, partnerships, or a question about the journey.

info@tickerbell.biz

Népfürdő u. 22B · 1138 Budapest, Hungary
Monday–Friday, 9:00–17:00 CET / CEST

Find our office