You have the problem chosen, the scope bounded, the cost limit set and the repository created. What you do not have yet is a single resource in Google Cloud, and that is deliberate.
This is the lesson people skip most often and the one that is most expensive to skip. Designing before typing is not an academic ritual: it is the rational response to an uncomfortable technical fact about the cloud. There are decisions you take in thirty seconds and pay for over years.
The projectId cannot be changed. Ever. If you create mi-proyecto-test-2 because you were in a hurry, that identifier will appear in every command, in every console URL, in every log, in every line of the bill and in the screenshot you show in an interview. Changing it means recreating everything inside it.
A subnet's CIDR range cannot be shrunk. A bucket's region cannot be changed. Migrating from Firestore to Cloud SQL — or the other way round — means rewriting the entire data layer. The schema of a partitioned BigQuery table lets you add columns but not change the partition key without recreating it.
None of those decisions is hard. All of them are irreversible. And that is why they are taken with a sheet of paper in front of you, not with the console open.
By the end of this lesson you will have the complete blueprint of your system: which logical components exist, which GCP service implements each one and why that one and not the alternative, what everything is called, who can talk to whom, who has permission to do what, where each piece of data lives and for how long, how much it will cost next month, what you are going to measure to know it works, and the important decisions written down in a format that will still make sense six months from now.
Contents
- Why you design before typing, and what "enough design" is
- From requirements to logical components
- From logical components to GCP services
- The three diagrams that are worth having
- Designing the organizational structure
- Naming and labelling conventions
- Network design
- Identity design
- Data model and data lifecycle
- Up-front cost estimate
- Defining the project's SLOs
- Architecture decision records (ADRs)
- Reviewing the design before implementing
- The complete design of RefugioReserva
- Why you design before typing, and what "enough design" is
The cost-of-change argument
The reason to design is not "doing things properly". It is that the cost of changing a decision grows brutally with the moment at which you change it:
| Decision | Changing it during design | Changing it after implementing | Changing it in production |
|---|---|---|---|
| Project name | 10 seconds | Recreate everything | Impossible in practice |
| Subnet range | 10 seconds | Recreate the network and everything in it | Maintenance window |
| Firestore ↔ Cloud SQL | 1 minute | Rewrite the data layer (~15 h) | Migration with live data |
| Region | 10 seconds | Recreate everything | Full migration |
| Cloud Run ↔ GKE | 1 minute | Rewrite the deployment (~8 h) | Migration with traffic |
| BigQuery partition key | 10 seconds | Recreate the table and reload | Recreate + gaps in the dashboard |
| Adding a field to a table | 10 seconds | 20 minutes | 20 minutes |
The rows at the top are the ones that justify designing. The one at the bottom is the one that justifies not over-designing: adding a field costs the same before as after, so do not lose an hour deciding whether the notas field should exist.
What "enough design" is for a project this size
Design has very rapidly diminishing returns. For a 50-hour project:
| Level of design | Time | What it produces | Verdict |
|---|---|---|---|
| None | 0 h | You start typing | You end up redoing 40 % |
| Enough | 5-8 h | The nine artefacts in this lesson | ✅ The right amount |
| Excessive | 25 h | UML diagrams, a specification for every endpoint, a formal threat model | You never get to build |
The nine artefacts of "enough design", and none of them takes more than a page:
| # | Artefact | Answers | Time |
|---|---|---|---|
| 1 | List of logical components | What pieces does it consist of? | 30 min |
| 2 | Component → service table with justification | What implements each piece and why? | 1 h |
| 3 | Three diagrams (context, components, deployment) | How does it all fit together? | 1.5 h |
| 4 | Project structure and naming convention | What is everything called and where does it live? | 30 min |
| 5 | Addressing plan + flow matrix | Who talks to whom? | 45 min |
| 6 | Identity and role table | Who can do what? | 45 min |
| 7 | Data model + lifecycle | What is stored where and until when? | 1 h |
| 8 | Cost estimate | Does it fit within the limit? | 45 min |
| 9 | SLI/SLO + ADRs | How do I know it works, and why did I decide this? | 1 h |
The rule that decides whether a design is finished: when you can answer, without hesitating and without opening the console, these five questions:
- How much will it cost per month, and why?
- What happens if the most important component goes down?
- Who can read the users' data?
- Which service did you choose and which did you reject, and why?
- How will you know it is working properly without anybody telling you?
If you hesitate on any of them, that is where the design is missing.
- From requirements to logical components
Before naming a single Google service, describe your system in logical components: functional pieces, provider-agnostic. This step looks bureaucratic and it is the one that avoids the most common beginner mistake, which is designing the architecture as a list of products you fancy using.
Six families of components cover practically any project in this module:
flowchart TB
subgraph Frontal["Front — user-facing exposure"]
F1["Public entry point<br/>TLS, domain, cache, filtering"]
end
subgraph Servicio["Service — the logic"]
S1["Web application / API"]
S2["Asynchronous and<br/>scheduled jobs"]
end
subgraph Almacen["Storage"]
A1["Objects<br/>files, images"]
A2["Operational database<br/>business state"]
end
subgraph Datos["Data and analytics"]
D1["Event transport"]
D2["Analytical store"]
D3["Visualisation"]
end
subgraph IA["Intelligence"]
I1["AI model or API"]
end
subgraph Op["Operations"]
O1["Identity and secrets"]
O2["Observability"]
O3["Continuous delivery"]
O4["Infrastructure as code"]
end
F1 --> S1
S1 --> A1
S1 --> A2
S1 --> D1
S2 --> A2
D1 --> D2
D2 --> D3
S1 --> I1
O1 -.-> S1
O2 -.-> S1
O3 -.-> S1
O4 -.-> F1
How you fill this in for your project. Take your functional requirements from 08-01 and write, for each component, what it does in your domain:
| Logical component | What it does in my project | Mandatory? |
|---|---|---|
| Public entry point | Yes (RNF-5) | |
| Web application / API | Yes (RF-1) | |
| Asynchronous jobs | Yes (RF-4) | |
| Object storage | Yes (RF-2) | |
| Operational database | Yes (RF-3) | |
| Event transport | Yes (RF-4) | |
| Analytical store | Yes (RF-5) | |
| Visualisation | Yes (RF-5) | |
| AI | Yes (RF-6) | |
| Identity and secrets | Yes (RNF-3, RNF-4) | |
| Observability | Yes (RNF-6, RNF-7) | |
| Continuous delivery | Yes (RNF-2) | |
| IaC | Yes (RNF-1) |
If any box comes out forced — "well, I would put Pub/Sub here because something has to go in" — that is a sign that the problem you chose does not exercise that layer naturally. Two legitimate ways out: change the problem slightly so that it needs it, or implement the simplest possible version of the component and document in an ADR that it is deliberately minimal. What is not acceptable is dropping in a complex service for no reason: that scores negatively in block A of the rubric, not positively.
- From logical components to GCP services
Now for the translation. For each component, one row with four mandatory columns: service chosen, why, what you rejected and why you rejected it.
That fourth column is 50 % of block A of the rubric. An interviewer does not ask you "did you use Cloud Run?"; they ask you "why Cloud Run and not App Engine?". If you did not write it down the day you decided, you will not remember it.
The compute tree (from 02-07)
flowchart TD
A["What do I need to run?"] --> B{"Is it request-response<br/>code in a<br/>container?"}
B -- Yes --> C{"Do I need cluster<br/>control, operators<br/>or a service mesh?"}
C -- No --> D["✅ Cloud Run<br/>Scales to zero. Recommended by default"]
C -- Yes --> E{"Justified in writing<br/>and fits the budget?"}
E -- No --> D
E -- Yes --> F["GKE Autopilot"]
B -- No --> G{"Does it react to an event<br/>and is it a single function?"}
G -- Yes --> H["✅ Cloud Functions<br/>(gen 2 = Cloud Run underneath)"]
G -- No --> I{"Is it a process<br/>that starts and finishes?"}
I -- Yes --> J["✅ Cloud Run Jobs<br/>+ Cloud Scheduler"]
I -- No --> K{"Do I need the operating<br/>system, a GPU or software<br/>that cannot be containerised?"}
K -- Yes --> L["Compute Engine"]
K -- No --> D
For 90 % of the projects in this module, the answer is Cloud Run. And the main reason is not technical but economic: it scales to zero. A portfolio project gets zero traffic 99 % of the time, and anything that does not scale to zero eats the budget from 08-01 without giving anything back.
The database tree (from 02-06 and 02-03)
flowchart TD
A["What is my data like?"] --> B{"Are there relationships,<br/>and do I need transactions<br/>across several entities?"}
B -- Yes --> C{"Do I need global<br/>scale or >10 TB?"}
C -- No --> D["✅ Cloud SQL<br/>PostgreSQL / MySQL"]
C -- Yes --> E["Spanner<br/>⚠️ expensive for a portfolio"]
B -- No --> F{"Key access,<br/>documents, no<br/>complex queries?"}
F -- Yes --> G{"Do I want zero cost<br/>at rest?"}
G -- Yes --> H["✅ Firestore<br/>Really scales to zero"]
G -- No --> H
F -- No --> I{"High-volume<br/>time series?"}
I -- Yes --> J["Bigtable<br/>⚠️ expensive for a portfolio"]
I -- No --> D
The real dilemma in this module is Cloud SQL versus Firestore, and it has an economic dimension that needs putting on the table:
Cloud SQL (db-f1-micro) |
Firestore | |
|---|---|---|
| Cost at rest | ~€7-9/month, always on | €0 (generous free tier) |
| Multi-entity transactions | ✅ Native and simple | ⚠️ Possible, more limited |
Queries with JOIN and aggregation |
✅ Full SQL | ❌ No JOIN |
| Data model | Normalised relational | Documents, denormalised |
| Learning curve if you come from SQL | None | Medium |
| Export to BigQuery | Federation / dump | Native extension to BigQuery |
| Good fit if… | Your domain has integrity rules | Your domain is "documents with state" |
If your cost limit is €0, the decision is made: Firestore. If you have €10-15 and your domain has genuine referential integrity (like a booking that cannot exceed capacity), Cloud SQL makes it far easier and the decision justifies itself.
The data tree (from 04-01 and 04-04)
flowchart TD
A["What do I do with the data?"] --> B{"Do I need to react<br/>to a fact as it happens?"}
B -- Yes --> C["✅ Pub/Sub<br/>topic + subscription"]
C --> D{"Is the transformation<br/>complex or continuous?"}
D -- No --> E["✅ Cloud Function<br/>or Cloud Run push"]
D -- Yes --> F["Dataflow<br/>⚠️ streaming costs 24 h a day"]
B -- No --> G{"Is a periodic snapshot<br/>of the data enough?"}
G -- Yes --> H["✅ Cloud Run Job<br/>+ Cloud Scheduler → BigQuery"]
G -- No --> C
E --> I["✅ BigQuery<br/>partitioned tables"]
F --> I
H --> I
I --> J["✅ Looker Studio"]
The economic warning: Dataflow in streaming mode keeps workers running 24 hours a day. For a portfolio project that can be €60-100 a month for a flow that processes twelve messages. Pub/Sub + a function does exactly the same thing for €0 at this volume, and demonstrates just as well that you understand the pattern. Save Dataflow for when the transformation genuinely justifies it, and document the reason.
The translation table you must produce
| Logical component | Service chosen | Why | Rejected alternative | Why it is rejected |
| --- | --- | --- | --- | --- |
| Web application | Cloud Run | Scales to zero, standard container, no node management | GKE Autopilot | The cluster costs from minute 1 and I need nothing from Kubernetes |
| Database | ... | ... | ... | ... |One row per component. Ten to thirteen rows. It is the most quoted document in your final presentation.
- The three diagrams that are worth having
A diagram is not decoration: it is a tool for answering a specific question. And the reason most architecture diagrams are useless is that they try to answer every question at once and end up as a tangle of forty boxes.
The rule: one diagram, one level of detail, one question.
Diagram 1 — Context: who uses the system and what does it talk to?
Answers: what is this and who touches it? Your system is a single box. Around it, the actors and the external systems.
flowchart LR
U1["👤 Hiker<br/>searches and books"]
U2["👤 Refuge warden<br/>checks the day's bookings"]
U3["👤 Federation<br/>checks indicators"]
U4["👤 Developer (me)<br/>deploys and operates"]
S(("RefugioReserva<br/>Platform on GCP"))
E1["Domain registrar<br/>delegated DNS"]
E2["GitHub<br/>source code"]
U1 -->|HTTPS| S
U2 -->|HTTPS + login| S
U3 -->|Shared report| S
U4 -->|git push| E2
E2 -->|triggers build| S
S -.->|resolves| E1
Ten boxes at most. If you have twenty actors, your scope is too big (go back to 08-01).
Diagram 2 — Components: what parts does it consist of and how do they communicate?
Answers: how does a request and a piece of data flow through the inside? Here the GCP services do appear, but grouped by function, not by product.
flowchart TB
Usuario["👤 User"]
subgraph Borde["Edge"]
LB["Global HTTPS load balancer<br/>+ Cloud CDN + Cloud Armor"]
end
subgraph Aplicacion["Application"]
RUN["Cloud Run<br/>refugio-web"]
JOB["Cloud Run Job<br/>agregado-nocturno"]
FN["Cloud Function<br/>procesar-evento"]
end
subgraph Datos["Operational data"]
SQL[("Cloud SQL<br/>PostgreSQL")]
GCS[("Cloud Storage<br/>photos")]
SEC["Secret Manager"]
end
subgraph Analitica["Analytics"]
PS["Pub/Sub<br/>reservas-eventos"]
BQ[("BigQuery<br/>refugio_analitica")]
LS["Looker Studio"]
end
subgraph Inteligencia["AI"]
NL["Natural Language API"]
end
Usuario --> LB --> RUN
RUN --> SQL
RUN --> GCS
RUN --> SEC
RUN --> PS
PS --> FN --> BQ
JOB --> SQL
JOB --> BQ
RUN --> NL
NL --> BQ
BQ --> LS
LB -.->|static assets| GCS
The five rules of a readable component diagram:
- Fifteen boxes maximum. If you need more, make two diagrams.
- Arrows follow the direction of the request, not of the data. If you get confused, add the label.
- Group by responsibility (edge, application, data, analytics), not by product.
- Label the arrows that are not obvious.
RUN --> SQLneeds no label;LB -.-> GCSdoes. - No unnecessary crossings. If two lines cross, reorder the boxes.
Diagram 3 — Deployment: where does each thing physically live?
Answers: which project, which region, which network, what is public and what is not? It is the diagram a security reviewer looks at first.
flowchart TB
subgraph Internet["🌐 Internet"]
CL["Client"]
end
subgraph Org["Organization / billing account"]
subgraph ProyProd["Project: refugio-prod · europe-west1"]
subgraph VPCP["VPC refugio-vpc"]
subgraph SubP["Subnet app 10.20.0.0/24"]
CRP["Cloud Run<br/>(VPC egress connector)"]
end
PSC["Private services access<br/>10.20.10.0/24"]
SQLP[("Cloud SQL<br/>private IP · no public IP")]
end
LBP["Global HTTPS load balancer<br/>anycast IP"]
GCSP[("Buckets")]
end
subgraph ProyDev["Project: refugio-dev · europe-west1"]
DEV["Reduced copy<br/>no CDN, no own domain"]
end
subgraph ProyDatos["Project: refugio-datos"]
BQD[("BigQuery")]
end
end
CL -->|HTTPS 443| LBP --> CRP
CRP --> PSC --> SQLP
CRP --> GCSP
CRP -.-> BQD
DEV -.-> BQD
This diagram must make obvious, at a glance, what is exposed to the internet. In the example: only the load balancer. The database has no public IP, Cloud Run does not accept direct traffic (only from the load balancer), and the data project is not reachable from outside.
- Designing the organizational structure
Do I need an organization?
It depends on your account:
| Situation | What you have | What you do |
|---|---|---|
| Personal account with Gmail | No organization. Standalone projects | The projects hang off "No organization". Valid for the module |
| Account with Workspace or Cloud Identity on your own domain | Organization | Create folders and take the chance to practise 07-07 |
| Your company's account | Somebody else's organization | Do not use it. Create a personal account for the project |
Without an organization you lose organization policies and folders, but everything else works the same and the rubric does not require an organization. If you have your own domain (which you are going to need anyway for RNF-5), you can create a free Cloud Identity and have an organization; it is a bonus that adds points but is not mandatory.
How many projects?
A project is the unit of isolation, of billing and of IAM. Three reasonable options:
| Option | Projects | Advantages | Drawbacks | Recommended if |
|---|---|---|---|---|
| A. Just one | miproy |
Simple, cheap, fast | You do not demonstrate environment separation. One mistake affects everything | Your limit is €0 and you are very short of time |
| B. Two (dev + prod) | miproy-dev, miproy-prod |
The right balance. You demonstrate real promotion between environments | You duplicate some resources | ✅ By default |
| C. Four or more | -dev, -prod, -datos, -cicd |
Resembles AlpinaShop. Separates analytics and delivery | More IAM to manage, more time | If you have time to spare |
The recommendation is B, with a cheap variant: if your analytics are small, put BigQuery in the production project and save yourself the fourth project. What is always worth separating is dev from prod, because that is what makes the promotion pipeline of RNF-2 credible and because it lets you destroy dev on Fridays without fear.
flowchart TB
ORG["Organization (optional)<br/>tudominio.example"]
ORG --> P1["refugio-dev<br/>Development environment<br/>Destroyable"]
ORG --> P2["refugio-prod<br/>Production environment<br/>What you show"]
ORG --> P3["refugio-datos<br/>BigQuery + Looker<br/>(optional)"]
ORG --> P4["refugio-cicd<br/>Artifact Registry + builds<br/>(optional)"]
P4 -.->|deploys into| P1
P4 -.->|with approval| P2
P1 -.->|writes to dev dataset| P3
P2 -.->|writes to prod dataset| P3
The region decision
A single region. For a project in Spain, europe-west1 (Belgium) or europe-southwest1 (Madrid).
| Criterion | europe-west1 |
europe-southwest1 |
|---|---|---|
| Latency from Spain | ~25-35 ms | ~5-15 ms |
| Service availability | Practically everything | Very good, some services arrive later |
| Price | Reference | Slightly higher on some services |
| Data on Spanish territory | No (Belgium, still the EU) | Yes |
Write the decision and its reason. It is a three-line ADR, and it is the kind of detail that shows you think about data residency and not just about making it work. And remember from 01-05: some services are global (IAM, Cloud DNS, the global load balancer, Artifact Registry in multi-region mode), so the region does not apply to everything.
- Naming and labelling conventions
Decided once, applied always, and decided now because the projectId cannot be changed.
The pattern
<project>-<environment> for projectId
<project>-<component>[-<detail>] for resources inside the project| Resource | Pattern | Good example | Bad example |
|---|---|---|---|
| Project | <proj>-<env> |
refugio-prod |
mi-proyecto-final-2 |
| VPC | <proj>-vpc |
refugio-vpc |
default |
| Subnet | <proj>-<use>-<region> |
refugio-app-ew1 |
subnet-1 |
| Cloud Run service | <proj>-<function> |
refugio-web |
service |
| Cloud SQL | <proj>-db |
refugio-db |
instance-1 |
| Bucket | <proj>-<use>-<suffix> |
refugio-fotos-8f2a |
mis-fotos |
| Service account | sa-<workload> |
sa-refugio-web |
service-account-1 |
| BigQuery dataset | <proj>_<domain> |
refugio_analitica |
dataset1 |
| Pub/Sub topic | <entity>-<fact> |
reservas-creadas |
topic1 |
| Secret | <proj>-<what> |
refugio-db-password |
password |
The hard rules GCP imposes and that you need to know beforehand:
projectId: 6-30 characters, lowercase letters, digits and hyphens; starts with a letter; unique across all of Google Cloud and never reusable, not even after deleting the project.- Buckets: the name is globally unique. That is why they carry a random suffix:
refugio-fotoswill almost certainly be taken. Terraform solves it withrandom_id. - Service accounts: the
accountId(the part before the@) is 6-30 characters and also cannot be changed. - No underscores in DNS-compatible names (buckets, Cloud Run services). They are fine in BigQuery datasets and tables, which use
_. - No names containing
test,tmp,2,nuevo,final. All of that ends up in production and stays there.
Labels
Labels are key-value pairs that appear in the billing export. They are what lets you answer "how much does the data component cost me?" without guessing. They cost nothing and go in from the very first apply.
| Label | Values | What for |
|---|---|---|
proyecto |
refugioreserva |
Distinguishing it from other things in the same account |
entorno |
dev, prod |
Attributing cost per environment |
componente |
web, datos, analitica, ia, red |
Attributing cost per layer |
gestionado-por |
terraform, manual |
Detecting anything created by hand |
caduca |
2026-12-31 |
Temporary resources that need cleaning up |
In Terraform they are applied in one go with a shared variable:
locals {
etiquetas_base = {
proyecto = "refugioreserva"
entorno = var.entorno
gestionado-por = "terraform"
}
}
resource "google_storage_bucket" "fotos" {
name = "refugio-fotos-${var.sufijo}"
location = var.region
labels = merge(local.etiquetas_base, { componente = "web" })
}gestionado-por = terraform is the most useful of the five: any resource without that label, or created by hand, stands out in a billing query. It is your zero-cost configuration drift detector.
- Network design
Even if you use Cloud Run — which is serverless — there are network decisions to take, because the private database, the egress connector and the load balancer live in a VPC.
Addressing plan
Do not use the default network. It comes with subnets in every region in the world and permissive firewall rules you did not choose. Create a VPC in custom mode.
Reserve a space and divide it up. A private /16 is more than enough:
| Block | CIDR | Use | Notes |
|---|---|---|---|
| Project space | 10.20.0.0/16 |
Everything | Does not overlap with your home network (192.168.x) or the typical office one (10.0.x) |
| Application subnet | 10.20.0.0/24 |
VMs, GKE if there were any | 254 addresses, plenty |
| VPC egress connector | 10.20.8.0/28 |
Serverless connector | Requires an exact /28 |
| Private services access | 10.20.16.0/20 |
Cloud SQL with private IP | Google requires a reserved block, minimum /24; /20 is recommended |
| Reserved for the future | 10.20.32.0/19 |
Unused | Always leave free space |
The two traps that catch everybody the first time:
- The Serverless VPC Access connector needs its own non-overlapping
/28. Not a/29, not a/27: a/28. - Cloud SQL with a private IP needs a range reserved for private services access and a peering with Google's network. It is an extra step that the documentation mentions in passing and that produces a cryptic error if it is missing. In Terraform it is two resources:
google_compute_global_addresswithpurpose = "VPC_PEERING"andgoogle_service_networking_connection.
The allowed flow matrix
This is the most valuable artefact in the network design: a table of who can talk to whom. It is written before creating a single firewall rule, and afterwards it translates into rules almost mechanically.
| Source | Destination | Port | Protocol | Allowed? | Reason |
|---|---|---|---|---|---|
| Internet | Load balancer | 443 | TCP | ✅ | It is the entry point |
| Internet | Load balancer | 80 | TCP | ✅ | Only to redirect to 443 |
| Internet | Cloud Run directly | — | — | ❌ | It must go through the load balancer (--ingress=internal-and-cloud-load-balancing) |
| Internet | Cloud SQL | 5432 | TCP | ❌ | Never. No public IP |
| Load balancer | Cloud Run | 443 | TCP | ✅ | Serverless NEG |
| Cloud Run | Cloud SQL | 5432 | TCP | ✅ | Over private IP, via the connector |
| Cloud Run | Cloud Storage | 443 | TCP | ✅ | Google API |
| Cloud Run | Secret Manager | 443 | TCP | ✅ | Google API |
| Cloud Run | Pub/Sub | 443 | TCP | ✅ | Google API |
| Cloud Run | Open internet | — | — | ⚠️ | Only if I call an external API. Otherwise it is closed |
| Cloud Function | BigQuery | 443 | TCP | ✅ | Google API |
| Me (home IP) | Cloud SQL | 5432 | TCP | ⚠️ | Only through an authenticated proxy, never a public IP |
| Anyone | Anything | — | — | ❌ | Deny by default |
The last row is the policy: anything not explicitly allowed is denied. It is the principle from 03-01 and it is the difference between a network that was designed and a network that "works".
Minimum public exposure
Write the list of everything that will be reachable from the internet. It should fit in three lines:
## Public surface of RefugioReserva
1. `https://refugioreserva.example` → load balancer → Cloud Run (the whole app).
2. `https://refugioreserva.example/fotos/*` → bucket via CDN (public thumbnails only).
3. Nothing else. The DB has no public IP. Cloud Run does not accept direct traffic.
The originals bucket and the Terraform state bucket are private.If that list has more than five entries, review it: almost certainly something is exposed that does not need to be.
- Identity design
Same as with the network: you write the table before creating anything. And you apply the principle from 03-04 in its most practical form: one service account per workload, with the minimum roles, and no downloaded keys.
The three identity types in your project
| Type | Who it is | How it authenticates |
|---|---|---|
| Humans | You, and whoever reviews your project | Your Google account, with 2FA |
| Workloads | Cloud Run, functions, jobs | Attached service account, no key |
| External CI/CD | GitHub Actions or Cloud Build | Workload Identity Federation, no key |
The identity table
| Identity | Type | Roles | On which resource | Why exactly this |
|---|---|---|---|---|
[email protected] |
Human | roles/owner |
Projects | You are the administrator. It is the only acceptable owner |
sa-refugio-web |
Cloud Run (app) | roles/cloudsql.client |
Project | Connect to the DB, not administer it |
roles/secretmanager.secretAccessor |
Only the 2 secrets it uses | Read, not list or create | ||
roles/storage.objectAdmin |
Only the photos bucket | Upload and read photos | ||
roles/pubsub.publisher |
Only the events topic | Publish, not subscribe | ||
roles/logging.logWriter |
Project | Write logs | ||
roles/cloudtrace.agent |
Project | Send traces | ||
sa-procesar-evento |
Cloud Function | roles/bigquery.dataEditor |
Only the analytical dataset | Insert rows |
roles/language.user (per the API) |
Project | Call the NL API | ||
sa-job-nocturno |
Cloud Run Job | roles/cloudsql.client |
Project | Read the DB |
roles/bigquery.jobUser + dataEditor |
Dataset | Load aggregates | ||
sa-deploy |
CI/CD (WIF) | roles/run.developer |
Project | Deploy revisions |
roles/artifactregistry.writer |
Repository | Publish images | ||
roles/iam.serviceAccountUser |
Only on sa-refugio-web |
Being able to attach that SA to the service |
The five principles you must respect and that the rubric checks:
- Never
roles/editororroles/owneron a service account. They are the primitive roles from 03-04 and they grant thousands of permissions. - The narrowest possible scope.
secretAccessoron the secret, not on the project. In Terraform that isgoogle_secret_manager_secret_iam_member, notgoogle_project_iam_member. - One SA per workload. If the function and the app share an SA, they share permissions, and the blast radius of a failure doubles.
- Zero JSON keys. Workloads inside GCP use the attached SA; external CI/CD uses WIF. If you have a
.json, you have failed. serviceAccountUseris the permission everybody forgets. Forsa-deployto be able to deploy a service that runs assa-refugio-web, it needs to act as that account. It is the most frequent 403 of the first automated deployment.
And the one that gets discovered late: Compute Engine's default service account has the editor role. If you let your resources use it, your whole least-privilege design is decorative. Create explicit SAs for everything.
- Data model and data lifecycle
What is stored where, and why
The table that avoids the most expensive mistake in these projects, which is putting everything into the operational database:
| Data | Where it lives | Why there | What does NOT go here |
|---|---|---|---|
| Business state (bookings, users, catalogue) | Cloud SQL | It needs transactions and integrity | Event history, files |
| Binary files (images, PDFs) | Cloud Storage | Cheap, servable by CDN, does not bloat the DB | Anything structured you need to query |
| Business events (immutable history) | BigQuery | Analytics, aggregation, cost per query | Data you need to read in <100 ms |
| Sessions and ephemeral state | Firestore or memory | Key access, TTL | Anything that must survive |
| Passwords, keys, connection strings | Secret Manager | Encrypted, versioned, audited | Never in plain environment variables or in the repo |
| Non-sensitive configuration | Service environment variables | Changes without rebuilding the image | Secrets |
| Terraform state | Dedicated bucket with versioning | Locking and recovery | Anything else |
The operational schema
Write it as DDL, even if it is a draft. It is what forces you to think about keys, constraints and types:
-- Operational schema of RefugioReserva (Cloud SQL PostgreSQL)
-- All data is FICTIONAL.
CREATE TABLE refugio (
id SERIAL PRIMARY KEY,
nombre TEXT NOT NULL UNIQUE,
altitud_m INTEGER NOT NULL CHECK (altitud_m BETWEEN 500 AND 3500),
capacidad INTEGER NOT NULL CHECK (capacidad > 0),
activo BOOLEAN NOT NULL DEFAULT TRUE,
creado_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
CREATE TABLE reserva (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
refugio_id INTEGER NOT NULL REFERENCES refugio(id),
fecha DATE NOT NULL,
plazas INTEGER NOT NULL CHECK (plazas BETWEEN 1 AND 12),
-- Contact details: FICTIONAL. See the retention policy further down.
nombre_titular TEXT NOT NULL,
email_titular TEXT NOT NULL,
estado TEXT NOT NULL DEFAULT 'confirmada'
CHECK (estado IN ('confirmada','cancelada')),
creada_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Index that supports the most frequent query: availability by refuge and date
CREATE INDEX idx_reserva_refugio_fecha ON reserva (refugio_id, fecha)
WHERE estado = 'confirmada';
CREATE TABLE opinion (
id SERIAL PRIMARY KEY,
refugio_id INTEGER NOT NULL REFERENCES refugio(id),
texto TEXT NOT NULL,
puntuacion INTEGER CHECK (puntuacion BETWEEN 1 AND 5),
-- Filled in by the Natural Language API
sentimiento NUMERIC(4,3),
magnitud NUMERIC(4,3),
creada_en TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Three details that deserve attention and that score in the rubric:
- The
CHECKonplazasis a business rule expressed in the database. It is free and it prevents impossible data even if the application has a bug. - The partial index (
WHERE estado = 'confirmada') is smaller and faster than a full one, because cancelled bookings do not take part in the availability calculation. - The capacity check is not in the schema. It cannot be: it depends on the sum of bookings for a given date. It goes in a transaction with a lock in the application, and it is precisely what the mandatory integration test will test.
The analytical schema
In BigQuery the criteria are different: you denormalise, you partition by date and you cluster by whatever you filter on most. The reason is economic: BigQuery charges by bytes read, and an unpartitioned table is read in full every time.
-- Analytical store. Partitioned by day and clustered by refuge.
CREATE TABLE `refugio-datos.refugio_analitica.reservas_eventos` (
evento_id STRING NOT NULL,
ocurrido_en TIMESTAMP NOT NULL,
tipo_evento STRING NOT NULL, -- created | cancelled
reserva_id STRING NOT NULL,
refugio_id INT64 NOT NULL,
refugio_nombre STRING, -- denormalised on purpose
fecha_estancia DATE NOT NULL,
plazas INT64 NOT NULL,
-- NO contact details: they are not replicated to the analytical store
)
PARTITION BY DATE(ocurrido_en)
CLUSTER BY refugio_id
OPTIONS (
partition_expiration_days = 1095, -- 3 years, then deleted automatically
require_partition_filter = TRUE -- forces filtering by date
);require_partition_filter = TRUE is the option that saves your bill. With it, a query without a WHERE DATE(ocurrido_en) ... fails instead of scanning the whole table. It is the equivalent of a seatbelt and in a project with a cost limit it is not optional.
Notice too what is not there: contact details do not travel to the analytical store. Nobody needs the booker's email to calculate occupancy, and every piece of personal data you do not replicate is a GDPR problem you do not have.
The data lifecycle
Every piece of data is born, lives and must die. The last part is the one that never gets designed.
| Data | Born | Lives | Dies | Mechanism |
|---|---|---|---|---|
| Booking | On booking | In Cloud SQL | 24 months after the stay | Nightly job that anonymises contact details |
| Contact details | With the booking | In Cloud SQL | 6 months after the stay | UPDATE that replaces them with anonimizado |
| Original photo | On upload | Standard bucket | Never (they belong to the refuge) | — |
| Thumbnail | Generated | Bucket + CDN | Regenerated if lost | Derived, not critical |
| Analytical event | On booking | BigQuery | 3 years | partition_expiration_days |
| Application log | Every request | Cloud Logging | 30 days | Log bucket retention |
| Cloud SQL backup | Daily | 7 days | Automatic | Backup retention |
| Container image | Every build | Artifact Registry | 10 versions | Cleanup policy |
⚠️ GDPR warning, and this is not a formality. As soon as your system stores a name, an email, a phone number or a position associated with an identifiable person, you are processing personal data, even if it is made up and even if it is a learning project.
For this project: use fictional data exclusively (generated names,
@example.comemails, phone numbers from the range reserved for fiction). With that, formally there is no processing of personal data and no obligation.But design as if there were, because the habit is what is being assessed and what transfers to a real job: minimisation (do not store what you do not use), purpose limitation (do not replicate contact details to the analytical store), a defined retention period with real, automatic deletion, encryption at rest, restricted access and traceability of who accesses what.
And if this design were ever going to process real people's data, a full GDPR and compliance review would be needed — legal basis, record of processing activities, impact assessment where applicable, data processing agreement with Google — which is entirely outside the scope of this course.
- Up-front cost estimate
It is done now, with the design in hand and before building. If it does not fit within the limit from 08-01, what changes is the design, not the limit.
A five-step method
- List the services from your translation table.
- For each one, estimate the driver that determines the price: hours running, GB stored, GB processed, number of requests, GB of egress to the internet.
- Check whether the free tier covers it. Surprisingly often, it does.
- Use the official calculator for whatever it does not cover.
- Add it up and add 25 % of headroom for what you have not foreseen.
The drivers that matter
| Service | What you pay for | How you estimate it | Common trap |
|---|---|---|---|
| Cloud Run | vCPU-s + GiB-s + requests | Requests/month × average duration | min-instances > 0 bills 24 h a day |
| Cloud SQL | Instance running, per hour | 730 h × price/h | You pay even when you do not use it |
| Cloud Storage | GB/month + operations + egress | Estimated GB | Egress to the internet is the expensive part, not the storage |
| BigQuery | Bytes read + GB stored | Queries/day × bytes per query | A dashboard that auto-refreshes scans non-stop |
| Pub/Sub | GiB published | Messages × size | Negligible volume in this project |
| Cloud Functions | Invocations + GB-s | Invocations/month | Same |
| HTTP(S) load balancer | Forwarding rule per hour + traffic | 730 h × price | It costs money even with no traffic |
| Cloud Logging | GB ingested above the free allowance | Log volume | Debug logs in production |
| Artifact Registry | GB stored | Images × size | Nobody deletes old images |
| AI APIs | Per unit processed | Documents/images per month | Reprocessing on every page load |
The four money sinks of a portfolio project, in order of frequency:
- Cloud SQL running 24/7. It is almost always the biggest line item.
- The global load balancer. The forwarding rule costs per hour whether or not there is traffic. It can be €15-20/month. If your limit is very tight, consider serving Cloud Run with its own custom domain (which gives free TLS) and documenting in an ADR that you give up CDN and Cloud Armor on cost grounds.
- Standard GKE. A three-node cluster is around €100/month. Do not use it.
- Streaming Dataflow. Workers running permanently.
The estimate table
| Service | Configuration | Estimated driver | Free tier | Cost/month |
| --- | --- | --- | --- | --- |
| Cloud Run web | 1 vCPU / 512 MiB / min=0 | 20,000 req., 200 ms | Yes, covers almost all of it | ~€0.20 |
| Cloud SQL | db-f1-micro, 10 GB HDD | 730 h | No | ~€8.00 |
| ... | | | | |
| **Subtotal** | | | | **€X** |
| **+25 % headroom** | | | | **€Y** |
| **Limit set (08-01)** | | | | **€12** |
| **Does it fit?** | | | | ✅ / ❌ |What to do if it does not fit
In this order, from the cheapest change to the most painful:
| # | Measure | Typical saving | Cost to the project |
|---|---|---|---|
| 1 | Switch off the development environment when not in use (destroy/apply) |
40-50 % of the total | None, if your IaC works |
| 2 | Drop Cloud SQL to the smallest instance and no HA | 50-70 % of that line item | None in a portfolio |
| 3 | Schedule an overnight shutdown of Cloud SQL (9:00-23:00) | ~40 % more | The demo does not work overnight |
| 4 | Reduce log retention to 7 days | Small | Less history for diagnosis |
| 5 | Drop CDN and load balancer; use Cloud Run's custom domain | €15-20/month | You lose Cloud Armor and caching. Requires an ADR |
| 6 | Swap Cloud SQL for Firestore | The entire line item | Redesign of the data layer. Requires an ADR |
| 7 | Reduce functional scope | Variable | Last resort |
And one decision that has to be taken consciously: if your limit is €0, the correct architecture is Firestore + Cloud Run + BigQuery + Cloud Functions + a Cloud Run custom domain, with no load balancer and no Cloud SQL. It is a perfectly defensible design, as long as why is written down. A project that says "I chose Firestore because my cost constraint was €0 and Cloud SQL costs €8/month running" demonstrates more judgement than one that spent €60 without thinking about it.
- Defining the project's SLOs
From 07-06, in its minimum viable version. One or two SLOs are enough. The mistake is defining six and measuring none.
The process in four steps
Step 1 — What is the critical user journey? Not "the site works". Something concrete: "a hiker checks availability and completes a booking".
Step 2 — Which SLI measures it? An SLI is a ratio of good events to valid events:
| SLI type | Formula | When to use it |
|---|---|---|
| Availability | requests with code < 500 ÷ total requests | Always. It is the basic one |
| Latency | requests < threshold ÷ total requests | If perception matters |
| Correctness | correct operations ÷ attempted operations | For flows involving money or state |
| Freshness | data with lag < X ÷ total | For data pipelines |
Step 3 — Set the target. Be realistic: the target must be achievable with your architecture and higher than what a user notices. 99.9 % monthly is a good starting point; 99.99 % in a portfolio project with a single region is a lie, because the region itself does not guarantee it.
Step 4 — Calculate the error budget. It is what makes the SLO useful:
| SLO | Allowed error/month | In time (30 days) |
|---|---|---|
| 99 % | 1 % | 7 h 12 min |
| 99.5 % | 0.5 % | 3 h 36 min |
| 99.9 % | 0.1 % | 43 min 12 s |
| 99.95 % | 0.05 % | 21 min 36 s |
| 99.99 % | 0.01 % | 4 min 19 s |
The project's SLO table
| # | Critical journey | SLI | Target | Window | Error budget | Measurement source |
|---|---|---|---|---|---|---|
| SLO-1 | Check availability | % of requests to /api/disponibilidad with code < 500 |
99.5 % | 30 days | 3 h 36 min | run.googleapis.com/request_count metric |
| SLO-2 | Complete a booking | % of POST /api/reservas with latency < 1,500 ms |
95 % | 30 days | 36 h of slow requests | request_latencies distribution |
And the error budget policy, written now, which is what turns the SLO into a tool rather than a decorative number:
## Error budget policy (RefugioReserva)
- **Budget > 50 % remaining:** normal development. Deploy whenever you like.
- **Budget < 50 %:** every production deployment requires the end-to-end
test to have passed in dev.
- **Budget < 10 %:** new functionality is frozen. Reliability fixes only
until it recovers.
- **Budget exhausted:** a short post-mortem is written in `docs/diario.md`
before deploying anything again.In a one-person project this may sound excessive. It is not: it is one paragraph, and it is exactly the kind of artefact that in an interview shows you have understood what an SLO is for.
- Architecture decision records (ADRs)
An ADR (Architecture Decision Record) is one page that records one important decision. It is the most valuable document you will write in this project, and here is why, without embellishment:
- Three months from now you will not remember why you chose Firestore. The code says what you did; the ADR says why.
- In an interview they are going to ask you exactly that. An ADR is a prepared answer.
- Writing the decision down forces you to actually have one. Half the time, when you reach the "alternatives" section you discover you had not considered any.
- It lets you revisit the decision honestly. If the context changes, you write a new ADR that supersedes the previous one. You do not rewrite the old one: you leave it as a historical record. That is how the evolution of your judgement becomes visible, which is what is being assessed.
The template
# ADR-00X: [Title phrased as a decision, not as a question]
- **Date:** 2026-09-12
- **Status:** Proposed | **Accepted** | Superseded by ADR-00Y | Obsolete
## Context
What situation forces a decision? What constraints are there (cost, time,
knowledge, requirements)? Facts, not opinions. 5-10 lines.
## Options considered
### Option A — <name>
- Advantages:
- Drawbacks:
- Estimated cost:
### Option B — <name>
- Advantages:
- Drawbacks:
- Estimated cost:
## Decision
We choose **<option>**.
Because <the criterion that decided it>. The deciding factor was <just one>.
## Consequences
### Positive
-
### Negative (what we accept losing)
-
### What would change this decision
If <X> happens, it would need revisiting.The two sections people leave weak and that are the ones that count:
- "Negative": every architecture decision loses something. If your ADR has no drawbacks, you have not decided: you have justified what you already wanted to do. Writing "I accept that without a load balancer I lose Cloud Armor and edge caching" demonstrates judgement, not weakness.
- "What would change this decision": it turns the decision into something revisable and shows you know it is contextual. It is exactly what AlpinaShop did with DA-004 on Anthos.
The ADRs you need as a minimum
| ADR | Decision | Why it matters |
|---|---|---|
| ADR-001 | Compute service | It is the application's structural decision |
| ADR-002 | Database | It is the most expensive to reverse |
| ADR-003 | Data strategy (streaming vs. batch) | It determines complexity and cost |
| ADR-004 | Approach for the AI component | Pre-trained API vs. your own model |
| ADR-005 | Project structure and region | Irreversible |
| ADR-006 (common) | Cost cuts | The most honest one and the one that impresses most |
Three is the rubric's minimum; five or six is normal. One per design session and you have them.
- Reviewing the design before implementing
This is milestone M1 from 08-01. You either pass it or you do not; there is no "almost". Go through it yourself, in the cold light of day, ideally the day after finishing the design.
Design checklist
Architecture
- [ ] All six functional requirements have a service assigned
- [ ] All eight non-functional requirements have a mechanism assigned
- [ ] Every service has its justification and its rejected alternative written down
- [ ] The three diagrams exist and are consistent with each other
- [ ] No diagram has more than 15 boxes
Organization and names
- [ ] It is decided how many projects there are and exactly what they are called
- [ ] The
projectIds comply with the rules (6-30, lowercase, notest/final/2) - [ ] The region is decided and the reason is written down
- [ ] There is a naming convention for every resource type
- [ ] There is a labelling convention, including
gestionado-por
Network
- [ ] There is an addressing plan with no overlaps
- [ ] The serverless connector has its
/28 - [ ] Private Cloud SQL has its private services access range
- [ ] The flow matrix is written, with deny by default
- [ ] The public surface fits in five lines
Identity
- [ ] There is one service account per workload
- [ ] None of them has
owneroreditor - [ ] Every role is at the narrowest possible scope
- [ ] WIF is planned for CI/CD, with no JSON keys
- [ ]
serviceAccountUseris accounted for the deployer
Data
- [ ] It is clear which data lives where and why
- [ ] The operational schema is written as DDL
- [ ] The analytical table is partitioned, with
require_partition_filter - [ ] The lifecycle of every piece of data is defined, including its deletion
- [ ] No contact data is replicated to the analytical store
- [ ] It is noted that all the data is fictional
Cost
- [ ] There is an estimate table by service
- [ ] The total with 25 % headroom fits within the limit from 08-01
- [ ] If it did not fit, there is an ADR documenting the cut
- [ ] The budget with alerts is already created
Operations
- [ ] There is at least one SLI and one SLO with its window
- [ ] The error budget is calculated
- [ ] There is a written error budget policy
- [ ] It is planned what is going to be monitored
Documentation
- [ ] There are ≥3 ADRs with context, options, decision and consequences
- [ ]
docs/arquitectura.mdis written and contains the diagrams - [ ] The
diario.mdhas entries for every design session
The five control questions
If you have ticked every box and still cannot answer these five without looking, the design is not finished:
- How much will it cost per month and what is the biggest line item?
- If the database goes down, what stops working and what keeps working?
- Who — which identity exactly — can read the users' data?
- What did you reject in the most important decision, and why?
- How will you find out something is wrong before somebody tells you?
- The complete design of RefugioReserva
As a consolidated reference, this is how the example project's design ends up. Read it as a model of format and level of detail, not as something to copy.
Translation table
| Logical component | Service | Why | Rejected | Why it is rejected |
|---|---|---|---|---|
| Entry point | Global HTTPS load balancer + CDN + Cloud Armor | Managed certificate, photo caching, basic WAF | Cloud Run custom domain | Cheaper, but no CDN and no WAF. It fits the budget, so the load balancer stays |
| Application | Cloud Run refugio-web |
Scales to zero (almost no traffic), standard container | GKE Autopilot | Cost from minute 1, I need nothing from Kubernetes |
| App Engine standard | Less portable, and I want the container for RF-1 | |||
| Scheduled job | Cloud Run Job + Scheduler | Reuses the same image | Cloud Composer | Disproportionate: a DAG for one task |
| Objects | Cloud Storage refugio-fotos |
Cheap, integrated with the CDN | Storing them in the DB | Bloats the DB and makes backups more expensive |
| Operational DB | Cloud SQL PostgreSQL db-f1-micro |
Capacity integrity, transaction with a lock | Firestore | No convenient multi-document transactions for capacity control. Cost: +€8/month, accepted |
| Events | Pub/Sub reservas-eventos |
Decouples the web from analytics | Writing to BigQuery from the web | Couples user latency to BigQuery |
| Processing | Cloud Function procesar-evento |
Minimal volume, zero cost | Streaming Dataflow | ~€70/month for 60 messages a day |
| Analytical store | BigQuery refugio_analitica |
Free tier is enough, SQL | Querying the Cloud SQL replica | Mixes analytical and operational load |
| Visualisation | Looker Studio | Free, native connector | Grafana | It would have to be hosted and paid for |
| AI | Natural Language API (sentiment) | Works on day one, no training | Text AutoML | Days of work and training cost for the same requirement |
| Identity | 4 service accounts + WIF | Least privilege, no keys | One SA for everything | Blast radius, and it fails block D |
| Secrets | Secret Manager | Versioned and audited | Environment variables | They stay visible in the console and in describe |
| Observability | Cloud Monitoring + Logging + one SLO | Native, no cost at this volume | Self-managed Prometheus | Cost and time |
| Delivery | Cloud Build from GitHub | Native WIF with GCP | GitHub Actions | Equivalent; Cloud Build is chosen for its Artifact Registry integration |
| IaC | Terraform, GCS backend | Industry standard, portable | Deployment Manager | Being retired since 2025 (see 06-05) |
Structure and names
| Element | Value |
|---|---|
| Projects | refugio-dev, refugio-prod, refugio-datos |
| Region | europe-west1 (Belgium). Reason: full service catalogue and reference pricing; the extra 20 ms of latency is irrelevant for this use case |
| VPC | refugio-vpc (custom, one per project) |
| IP space | dev 10.10.0.0/16 · prod 10.20.0.0/16 |
| Labels | proyecto=refugioreserva, entorno, componente, gestionado-por |
| Domain | refugioreserva.example (prod), dev.refugioreserva.example (dev) |
Estimated cost
| Service | Configuration | Cost/month |
|---|---|---|
| Cloud Run (prod + dev) | 1 vCPU, 512 MiB, min=0 | ~€0.30 |
| Cloud SQL prod | db-f1-micro, 10 GB HDD, no HA |
~€8.00 |
| Cloud SQL dev | Created and destroyed per session (~20 h/month) | ~€0.25 |
| Cloud Storage | 2 GB Standard + lifecycle | ~€0.10 |
| BigQuery | <1 GB, <5 GB queried | €0.00 |
| Pub/Sub + Functions | ~2,000 events/month | €0.00 |
| HTTPS load balancer | 1 forwarding rule | ~€18.00 ❌ |
| Artifact Registry | 2 GB with cleanup at 10 versions | ~€0.20 |
| NL API | 400 documents, one time only | ~€0.40 |
| Subtotal | €27.25 | |
| +25 % headroom | €34.06 | |
| Limit (08-01) | €12.00 | |
| Does it fit? | ❌ No |
It does not fit. The load balancer eats more than everything else put together. This is not a design failure: it is exactly what estimating before building is for. And the answer goes into an ADR:
# ADR-006: Give up the global load balancer and use Cloud Run's custom domain
- **Date:** 2026-09-13
- **Status:** Accepted
## Context
The initial design uses a global HTTPS load balancer with Cloud CDN and Cloud Armor.
The cost estimate comes to €34/month against a self-imposed limit of €12.
The load balancer alone is ~€18/month, and it charges for existing, not for traffic.
The project's expected real traffic is tens of requests a day.
## Options considered
### A. Keep the load balancer and raise the limit to €40/month
- Advantages: complete architecture, CDN and WAF, practice with 03-02/03-03/03-05.
- Drawbacks: 3.3 times the limit. It nullifies the constraint exercise.
### B. Cloud Run custom domain (managed TLS included)
- Advantages: €0. Valid TLS. Meets RNF-5.
- Drawbacks: no Cloud CDN, no Cloud Armor, no traffic distribution
across heterogeneous backends.
### C. Load balancer for one week only, to demonstrate it, then destroy it
- Advantages: cost ~€4. I demonstrate that I know how to set it up, with
screenshots and with the Terraform written.
- Drawbacks: the final architecture does not have it; it has to be explained.
## Decision
We choose **C**. The `infra/modules/balanceador/` module stays written and tested;
it is deployed during the load-testing week (08-04), documented with
screenshots and cache measurements, and destroyed afterwards. The project's
permanent state uses Cloud Run's custom domain.
The deciding factor: the cost constraint is a project requirement,
not an obstacle, and skipping it would mean failing the exercise itself.
## Consequences
### Positive
- Stable cost of ~€9/month, within the limit, with headroom.
- The load balancer code exists and is tested: the knowledge stays.
- It gives material for the presentation: a real, measured cost decision.
### Negative
- The permanent architecture has no WAF. It is partly mitigated with
concurrency limits in Cloud Run and input validation in the app.
- Without a CDN, photos are served from the bucket with signed URLs; worse
latency, irrelevant at this volume.
## What would change this decision
If the project were to receive real traffic (>10,000 visits/month) or if the limit
rose above €30/month, the load balancer would be redeployed: the Terraform
module is already written and it would be one `apply`.Revised cost: €9.55/month, within the limit. And, above all: a story to tell in the final presentation that is worth more than the load balancer.
SLOs
| # | SLI | Target | Window | Error budget |
|---|---|---|---|---|
| SLO-1 | % of requests to /api/* with code < 500 |
99.5 % | 30 days | 3 h 36 min |
| SLO-2 | % of POST /api/reservas with latency < 1,500 ms |
95 % | 30 days | — |
Common Mistakes and Tips
Designing the architecture as a list of products you want to use. The symptom is a diagram with twelve GCP logos and no justification. The cure is the logical-components step: first what is needed, then what implements it.
Skipping the "rejected alternative" column. It is half of the 20 points in block A. And it is the literal question in an interview.
Creating the projects with provisional names. There are no provisional names in GCP: the projectId is forever and cannot be reused, not even after deleting it.
Using the default VPC. It brings subnets in every region and permissive rules you did not choose. Creating a custom VPC costs five lines of Terraform.
Forgetting the private services access range. Cloud SQL with a private IP needs a reserved block and a peering. If it is not in the design, it shows up as an incomprehensible error the day you create the database.
Not partitioning your BigQuery tables. It is the mistake you pay for in the bill, not in performance. And require_partition_filter = TRUE is your safety net.
Defining six SLOs. One well-measured SLO is worth more than six in a document. Start with availability.
Designing the lifecycle without the deletion. "It is kept forever" is not a retention policy: it is the absence of one. And as soon as there is real personal data, it is a breach.
Tip: write the ADR the day you take the decision, not at the end. Fifteen minutes then, or an invented reconstruction three weeks later.
Tip: draw the diagrams in mermaid inside the repository, not in an external tool. They get versioned with the code, they are updated in the same commit as the change and they render in GitHub. A diagram in a PNG exported from a web tool is out of date from day two.
Tip: do the cost estimate even if you are sure it fits. The RefugioReserva case — where the load balancer was eating three times the whole budget — is the norm, not the exception. That discovery is worth more than the design itself.
Tip: if you hesitate between two services for more than twenty minutes, choose the simpler one and write the ADR. A reversible decision taken quickly beats a perfect decision taken late. And the ADR already leaves the door open.
Exercises
Exercise 1 — Translate your requirements into architecture and draw it
Produce, for your project: the list of logical components with what each one does in your domain; the complete translation table with the four mandatory columns (service, why, rejected alternative, why it is rejected); and the three mermaid diagrams — context, components and deployment — respecting the maximum of 15 boxes and the five readability rules.
Save it all in docs/arquitectura.md.
Exercise 2 — Design network, identity and data on paper
Without creating anything in GCP, produce: the addressing plan with its blocks (including the connector's /28 and the private services access range); the allowed flow matrix with the deny-by-default row; the public surface list; the identity table with one service account per workload and its roles at the narrowest scope; the DDL of the operational schema; the schema of the partitioned analytical table; and the lifecycle table for every piece of data with its deletion mechanism.
Add the explicit note that all the data is fictional and what you would do differently if it were not.
Exercise 3 — Estimate the cost, define the SLOs and write the ADRs
Fill in the cost estimate table using the official calculator and compare it against your limit. If it does not fit, apply the measures in order and write the ADR for the cut. Define one or two SLOs with their SLI, target, window and error budget, plus the error budget policy. Write at least three ADRs using the complete template, including the negative consequences section and the one on what would change the decision.
Finish with the complete checklist from section 13 and answer the five control questions in writing.
Solutions
Solution 1 — RefugioReserva's architecture
Logical components filled in:
| Logical component | What it does in RefugioReserva |
|---|---|
| Entry point | Serves refugioreserva.example over HTTPS with a valid certificate |
| Web application | Searches availability, creates bookings, shows the warden's dashboard |
| Asynchronous jobs | Nightly job that aggregates daily occupancy into BigQuery and anonymises expired contact details |
| Objects | Refuge photos: original and thumbnail |
| Operational DB | Refuges, bookings, reviews, users |
| Event transport | Every booking created or cancelled emits an event |
| Analytical store | Event history and occupancy aggregates |
| Visualisation | Federation dashboard: occupancy, cancellations, sentiment |
| AI | Sentiment score for the reviews |
| Identity and secrets | 4 SAs, DB password and session key in Secret Manager |
| Observability | Dashboard of the 4 signals, 2 alerts, availability SLO |
| Continuous delivery | Cloud Build from GitHub with WIF |
| IaC | Terraform with modules, state in GCS |
The translation table and the three diagrams are the ones in section 14 and in section 4. One detail of the deployment diagram is worth flagging: after ADR-006, the load balancer disappears from the permanent state and Cloud Run is exposed with its custom domain, so the definitive deployment diagram has one box fewer and the Client → Cloud Run arrow goes directly. The diagram is updated when the decision changes; a diagram that contradicts the ADR is worse than having no diagram.
Solution 2 — RefugioReserva's network, identity and data
Addressing (prod):
| Block | CIDR | Use |
|---|---|---|
| Prod space | 10.20.0.0/16 |
Reserved in full |
| Application subnet | 10.20.0.0/24 |
Reserved; no VMs today |
| Serverless connector | 10.20.8.0/28 |
Cloud Run egress towards the VPC |
| Private services access | 10.20.16.0/20 |
Peering for Cloud SQL |
| Free | 10.20.32.0/19 |
Future |
Dev uses 10.10.0.0/16 with the same structure. They do not overlap, on purpose: if one day I wanted to peer the two VPCs, I could; if they overlapped, I could not.
Flow matrix: the one in section 7, with two clarifications after ADR-006. Since there is no longer a load balancer, Cloud Run is configured with --ingress=all (required for the custom domain) and the protection moves to: authentication on the private routes, strict input validation, and --max-instances=5 as a cap on spend and on abuse. This loss is acknowledged in ADR-006 and will also appear in the 08-05 self-assessment as conscious technical debt.
Identities:
| Identity | Role | Scope |
|---|---|---|
sa-refugio-web |
roles/cloudsql.client |
project |
roles/secretmanager.secretAccessor |
secrets refugio-db-password, refugio-session-key |
|
roles/storage.objectAdmin |
bucket refugio-fotos-8f2a |
|
roles/pubsub.publisher |
topic reservas-eventos |
|
roles/logging.logWriter, roles/cloudtrace.agent |
project | |
sa-procesar-evento |
roles/bigquery.dataEditor |
dataset refugio_analitica |
roles/pubsub.subscriber |
subscription reservas-eventos-sub |
|
sa-job-nocturno |
roles/cloudsql.client |
project |
roles/bigquery.jobUser |
project | |
roles/bigquery.dataEditor |
dataset refugio_analitica |
|
sa-deploy |
roles/run.developer |
project |
roles/artifactregistry.writer |
repository refugio-imagenes |
|
roles/iam.serviceAccountUser |
only on sa-refugio-web and sa-job-nocturno |
Four accounts, no primitive roles, no keys. sa-deploy is federated from GitHub with WIF restricted to the repository and to the main branch.
Data: the schemas from section 9. The lifecycle table includes one row worth highlighting:
| Data | Dies | Mechanism | Verification |
|---|---|---|---|
| Booker's contact details | 6 months after the stay | UPDATE reserva SET nombre_titular='anonimizado', email_titular='[email protected]' WHERE fecha < NOW() - INTERVAL '6 months' in the nightly job |
A query counting rows with contact details and an old date: it must return 0 |
That verification column is what turns the retention policy into something checkable. Without it, it is an intention.
Note on fictional data: the 2,000 records are generated with data/seed/generar.py by combining lists of invented names and @example.com emails (a domain reserved by RFC 2606 for precisely this). If the system processed real data, it would need: a legal basis for the processing, information for the data subject, a record of processing activities, a data processing agreement with Google Cloud, an impact assessment for processing the refuges' location data, and probably customer-managed encryption keys (CMEK) on Cloud SQL and on the buckets. None of that applies here because there is no real personal data, and that is exactly the reason there is none.
Solution 3 — RefugioReserva's cost, SLOs and ADRs
The estimate, the load balancer discovery and the complete ADR-006 are in section 14. The final table after the cut:
| Service | Cost/month |
|---|---|
Cloud SQL prod (db-f1-micro) |
€8.00 |
| Cloud SQL dev (ephemeral, ~20 h/month) | €0.25 |
| Cloud Run (prod + dev) | €0.30 |
| Cloud Storage | €0.10 |
| Artifact Registry | €0.20 |
| NL API (one time) | €0.40 |
| BigQuery, Pub/Sub, Functions, Logging | €0.00 |
| Domain (pro rata) | €1.00 |
| Subtotal | €10.25 |
| With 25 % headroom | €12.81 |
| Limit | €12.00 |
Still just over. Additional measure applied, from the cuts table: overnight shutdown of production Cloud SQL between 01:00 and 08:00 via a scheduled job, which reduces that line item by 29 % (~€2.30). Final estimated cost: €10.51 with headroom, within the limit. The consequence — the application does not work between 1 and 8 in the morning — is documented in the README and declared explicitly in the SLO: the measurement window excludes that period, because an SLO breached by a planned and known shutdown measures nothing useful.
RefugioReserva's five ADRs:
| ADR | Decision | Deciding factor |
|---|---|---|
| ADR-001 | Cloud Run for the application | Scales to zero with almost no traffic |
| ADR-002 | Cloud SQL PostgreSQL over Firestore | Capacity control with a transaction and a row lock |
| ADR-003 | Pub/Sub + Cloud Function over Dataflow | €70/month versus €0 for 60 messages a day |
| ADR-004 | Natural Language API over AutoML | Meets RF-6 in two hours; AutoML is days and euros |
| ADR-005 | Three projects, europe-west1, refugio-* names |
Irreversible; decided before creating anything |
| ADR-006 | No permanent load balancer | The cost constraint is a requirement, not an obstacle |
The five control questions, answered:
- How much will it cost? €10.51 a month. The biggest line item is Cloud SQL (€5.70 after the overnight shutdown), followed by the domain.
- What if the database goes down? Availability search and booking creation stop working — the entire critical journey. Still working: the static refuges page (cached), the bucket photos and the Looker Studio dashboard, which reads from BigQuery. The application must degrade with a clear message, not with a 500 error. This is a concrete reliability test for 08-04.
- Who can read the users' data? My personal account (project
owner),sa-refugio-web(throughcloudsql.client+ the credentials from the secret) andsa-job-nocturno. Nobody else.sa-procesar-eventoandsa-deploydo not have access to Cloud SQL, and the analytical store contains no contact details. - What did I reject in the most important decision? In ADR-002, Firestore. It would have saved €8/month and all the private networking work, but capacity control under concurrency — the heart of the problem — would have been noticeably more fragile without row-locking transactions. I accepted the cost and offset it with the overnight shutdown.
- How do I find out something is wrong? An email alert when the 5xx error rate exceeds 5 % for 5 minutes, an alert on SLO error budget consumption at 50 %, an uptime check every 5 minutes against
/salud, and a billing budget alert at 50/90/100 % of €12.
Conclusion
You have the blueprint. And, more importantly, you have the decisions taken and written down before it cost money to undo them.
You know why you design before typing: not out of academic rigour, but because there are decisions — projectId, region, CIDR, database engine, partition key — that cost ten seconds now and a complete rebuild later. And you know what "enough design" is: nine artefacts, five to eight hours, none of them more than a page, and the proof that it is finished is the five control questions.
You know how to go from requirements to logical components before naming a single Google product, and from there to services using the course's decision trees — 02-07 for compute, 02-06 for data, 04-01 versus 04-04 for analytics — always documenting the four columns: what you choose, why, what you reject and why. That fourth column is half of block A of the rubric and the literal question in an interview.
You know how to draw the three diagrams that are worth having — context, components, deployment — each with one question and one level of detail, with the 15-box rule and the five readability rules, and in mermaid inside the repository so they age with the code and not in a forgotten PNG.
You have decided the organizational structure: how many projects and why two is the right balance, the region with its reason written down, the naming convention with GCP's hard rules (the unrepeatable projectId, the global bucket name) and the labels, with gestionado-por=terraform as a free drift detector.
You have the network designed on paper: the addressing plan with the connector's /28 and the private services access range everybody forgets, the flow matrix with deny by default, and the public surface in five lines.
You have the identity in a table written before creating anything: one service account per workload, no primitive roles, every permission at the narrowest scope, WIF without keys and serviceAccountUser accounted for so that the first automated deployment does not give you an inexplicable 403.
You have the data model with what lives where and why, the operational DDL with its constraints and its partial index, the partitioned analytical table with require_partition_filter as a seatbelt for the bill, and — what almost nobody designs — the complete lifecycle, including deletion, with its mechanism and its verification query, and with the GDPR warning that turns discipline into habit even when your data is invented.
You have the cost estimate done before spending a euro, with the four money sinks identified and the cuts table in order. And you have the RefugioReserva example, where the estimate revealed that the load balancer cost three times the entire budget: exactly the discovery that estimating beforehand is for.
You have your SLOs defined with their SLI, their realistic target, their window and their calculated error budget, plus the policy that turns that budget into decisions.
And you have your ADRs, with the complete template and with the two sections most people leave weak: the negative consequences — because every decision loses something, and acknowledging it is judgement — and what would change the decision, which is what keeps it alive.
In the next lesson, 08-03, you start building. And you will do it in a specific, justified order — organization, network, identity, Terraform state, data, application, delivery, analytics, AI, observability — with a phase 0 of preparation, eight phases each with its verifiable "done" criterion, the cross-cutting practices that stop the project deforming along the way, the five-step method for when you get stuck, and the most honest warning in the module: the first time, everything takes twice as long.
Save the design in the repository and commit. From here on, everything you create must correspond to something that is already in this document.
Google Cloud Platform (GCP) Course
Module 1: Introduction to Google Cloud Platform
- What is Google Cloud Platform?
- Setting Up Your GCP Account
- A Tour of the GCP Console
- Projects, Resource Hierarchy and Billing
- Regions, Zones and the Shared Responsibility Model
- Cloud Shell and the gcloud CLI
Module 2: Core GCP Services
- Compute Engine: Virtual Machines on Google Cloud
- Cloud Storage: Object Storage
- Cloud SQL: Managed Relational Databases
- App Engine: Platform as a Service
- Google Kubernetes Engine (GKE)
- NoSQL Databases: Firestore, Bigtable and Spanner
- How to Choose the Right Compute Service
Module 3: Networking and Security
- VPC Networks
- Cloud Load Balancing
- Cloud CDN
- Identity and Access Management (IAM)
- Cloud Armor
- Secrets and Encryption: Secret Manager and Cloud KMS
- Cloud DNS, TLS Certificates and Publishing Services Securely
Module 4: Data and Analytics
- BigQuery: The Analytical Data Warehouse
- Cloud Dataflow: Batch and Streaming Data Processing
- Cloud Dataproc: Managed Spark and Hadoop
- Cloud Pub/Sub: Asynchronous Messaging
- Cloud Data Fusion: Code-Free Data Integration
- Orchestrating Pipelines with Cloud Composer and Workflows
- Data Governance and Dashboards with Dataplex and Looker Studio
Module 5: Machine Learning and AI
- Vertex AI: The Machine Learning Platform on GCP
- AutoML: Custom Models Without Writing Code
- TensorFlow on GCP: Training and Serving Models
- Natural Language API
- Vision API
- Generative AI on Vertex AI: Gemini Models and Embeddings
- MLOps: From Model to Product with Vertex AI Pipelines
Module 6: DevOps and Monitoring
- Cloud Build: Continuous Integration on GCP
- Cloud Source Repositories and Source Code Management
- Cloud Functions: Serverless Functions
- Cloud Monitoring (formerly Stackdriver): Metrics, Dashboards and Alerts
- Cloud Deployment Manager and Native Infrastructure as Code
- Cloud Logging and Cloud Trace: Logs, Traces and Diagnostics
- Terraform on GCP: Infrastructure as Code in Practice
Module 7: Advanced GCP Topics
- Hybrid and Multicloud with Anthos
- Serverless Computing with Cloud Run
- Advanced Networking: Shared VPC, Peering and Hybrid Connectivity
- Security Best Practices
- Cost Management and Optimization
- Reliability: SLOs, High Availability and Disaster Recovery
- Governance at Scale: Organization, Policies and Auditing
