Until now, every piece of Kilometre Zero has run on specific machines: the three Cassandra nodes that Ansible configured in 07-05, the Kafka brokers that are restarted one at a time while respecting the ISR, the Patroni cluster with its etcd, the MinIO with its disks, the Kubernetes cluster on which the six services are deployed. Somebody has bought those machines, installed them in a data centre, fitted them with disks and network, and replaces them when they fail. And when 07-03 settled on a pilot light disaster recovery plan with a "secondary region", it left open the question of where that region is and who operates it. This lesson answers that by changing the premise: the infrastructure becomes an API that a provider offers on demand, with regions and availability zones, virtual networks, load balancers, autoscaling and, above all, managed services that do for us what we have assembled by hand throughout the course. We will look at the service and deployment models, the concepts that change the design (regions and zones, VPCs, elasticity), the mapping between each Kilometre Zero component and its managed equivalent on AWS and GCP, the advantages and challenges (including the one that requires a legal review), infrastructure as code with Terraform, the pillars of a well-architected design, and an introduction to FinOps with the platform's approximate monthly cost. The hands-on part deploys Kilometre Zero on AWS with Terraform and the k8s/ manifests from 07-05. Serverless, edge and CDN are left for 08-04.
Contents
- Service models: IaaS, PaaS, SaaS and shared responsibility
- Deployment models: public, private, hybrid and multi-cloud
- Regions, availability zones and disaster recovery
- Cloud networking: VPCs, subnets, load balancers and autoscaling
- Managed services: what replaces each piece of Kilometre Zero
- Advantages and challenges: elasticity, pay-as-you-go, lock-in, egress and compliance
- Infrastructure as code with Terraform
- Well-architected design: the pillars
- FinOps: tags, reservations, spot, budgets and the cost of Kilometre Zero
- Hands-on: Kilometre Zero on AWS with Terraform and EKS
- Common Mistakes and Tips
- Exercises
- Conclusion
- Service models: IaaS, PaaS, SaaS and shared responsibility
The cloud is defined by what the provider takes on and what remains the customer's responsibility. That line moves depending on the service model, and knowing where it lies in each case is what prevents both paying for what is not used and assuming that "the cloud takes care of it" when it does not.
| Layer | Own data centre (what Kilometre Zero has today) | IaaS (virtual machines: EC2, Compute Engine) | PaaS (platform: RDS, EKS, App Engine, Cloud Run) | SaaS (application: managed Keycloak, Confluent Cloud, Datadog) | FaaS (functions: Lambda; covered in 08-04) |
|---|---|---|---|---|---|
| Building, power, physical network | Customer | Provider | Provider | Provider | Provider |
| Hardware, hypervisor | Customer | Provider | Provider | Provider | Provider |
| Operating system, patches | Customer | Customer | Provider | Provider | Provider |
| Runtime, middleware (PostgreSQL, Kafka, Kubernetes) | Customer | Customer | Provider (installation, patches, failover) | Provider | Provider |
| Service configuration (parameters, replicas, schemas) | Customer | Customer | Customer | Partial | Customer |
| The application and its code | Customer | Customer | Customer | Provider | Customer (only the function) |
| Data: classification, encryption, access, logical backups | Customer | Customer | Customer | Customer | Customer |
| Identities and permissions (IAM) | Customer | Customer | Customer | Customer | Customer |
Two rows never change column: data and identities always belong to the customer. That is the shared responsibility model every provider publishes: the provider is accountable for the security of the cloud (nobody gets physical access to the disk, the hypervisor isolates), the customer for security in the cloud (the bucket is not public, the IAM role does not have *, personal data is encrypted and in the right region). A km0-invoices bucket reachable without authentication is the customer's responsibility under any model.
For Kilometre Zero, the natural split is: PaaS for everything that has a managed equivalent (databases, Kafka, Kubernetes, cache, objects), IaaS only where there is none or where the control pays off (the Kubernetes worker nodes are IaaS underneath, but managed as a group), and SaaS for whatever makes no difference (identity, observability if the decision is taken to outsource it).
- Deployment models: public, private, hybrid and multi-cloud
| Model | What it is | When it makes sense | Hidden cost |
|---|---|---|---|
| Public cloud | A provider's resources (AWS, GCP, Azure...) shared between customers, logically isolated | Variable workloads, small operations teams, a need for managed services, uncertain growth | Lock-in, egress, pay-as-you-go costs that grow with success |
| Private cloud | The same abstraction (API, self-service, elasticity) over your own hardware: OpenStack, VMware, on-premise Kubernetes | Strict regulation, latency to in-house systems, a stable volume that amortises the hardware | You have to operate the cloud as well as the application; no real elasticity beyond the hardware purchased |
| Hybrid | Part private, part public, connected (VPN or dedicated link) | Gradual migration (the strangler approach of 08-01 applied to infrastructure); data that must stay inside with elastic compute outside | Latency and cost of the link; two security and observability models |
| Multi-cloud | Workloads spread across several public providers | Avoiding dependence on one, exploiting each one's unique services, customer demands | The most expensive to operate: two IAMs, two networks, two sets of managed services; egress between clouds; portability forces you to give up what is managed |
Kilometre Zero, which today has its own hardware, will move to the public cloud with a hybrid period during the migration (the VPN link between the data centre and the VPC lets Kafka replicate with MirrorMaker and lets Patroni have a replica in the cloud before the cutover). Multi-cloud is explicitly ruled out: with four teams, operating two providers would cost more than the risk it mitigates, and portability is preserved another way, with Kubernetes as the deployment layer and containers for whatever is not managed (section 6).
- Regions, availability zones and disaster recovery
A provider organises its infrastructure into regions (a geographical area: Ireland, Frankfurt, Paris...) and, within each region, into several availability zones (AZs): physically separate data centres (kilometres apart, with independent power and network) connected with 1-2 ms latency. This structure maps directly onto the failure domains of 07-03:
- An AZ is a data-centre failure domain: fire, power cut, building network failure. Spreading replicas across AZs (the three Cassandra nodes in three AZs; the Patroni primary and its synchronous replica in different AZs; the Kafka brokers with
broker.rack= AZ so that each partition's replicas are spread out) means that losing an AZ is not a disaster, just a failover. It is whattopologySpreadConstraintsdid in 07-05 with the zone label. - A region is a geographical (and regulatory) failure domain: a natural disaster, a failure of a provider's regional service, or a legal change. The secondary region of 07-03 is another region of the same provider, hundreds or thousands of kilometres away, with 10-80 ms latency, which rules out synchronous replication between them.
With that, the three DR strategies of 07-03 take concrete form in the cloud:
| Strategy | What exists in the secondary region | RPO | RTO | Relative cost | How it is done on AWS |
|---|---|---|---|---|---|
| Pilot light | Replicated data (RDS cross-region read replica, S3 replication, MSK MirrorMaker); a minimal or non-existent EKS cluster; Terraform ready to create the rest | Minutes (asynchronous replication) | 30-60 min (create nodes, promote the replica, switch DNS) | ~15-20% of the primary | RDS cross-region read replica; S3 CRR; Route 53 with manual failover |
| Warm standby | Everything deployed at reduced scale (EKS with few nodes, services with 1 replica) receiving data | Minutes | Minutes (scale up and switch DNS) | ~40-50% | As above + an active EKS with the HPA ready; Route 53 with health checks |
| Active-active multi-region | Everything at full scale in both, serving traffic simultaneously | Seconds or zero (depending on the data) | Seconds (DNS or the global load balancer stops sending) | ~200% + data complexity | Aurora Global Database or multi-DC Cassandra; Route 53 latency routing; the multi-leader design of 03-04 |
Kilometre Zero keeps the 07-03 decision (pilot light) on cost grounds, with one important difference from the in-house data centre: in the cloud, the secondary region costs nothing while it is empty. Terraform can create the secondary region's infrastructure in 20 minutes from the same code; the only thing that has to exist beforehand is the replicated data. Active-active is left as a next step in 08-05, because it involves resolving write conflicts between regions (03-04) and would change the consistency model for stock.
flowchart TB
subgraph R1[Region eu-west-1, primary]
direction TB
ALB[Application load balancer<br/>ALB, public]
subgraph AZa[AZ a]
EKSa[EKS nodes<br/>orders, catalog...]
RDSa[(RDS primary<br/>km0_inventory)]
MSKa[MSK broker 1]
CASa[Cassandra 1]
end
subgraph AZb[AZ b]
EKSb[EKS nodes]
RDSb[(RDS standby<br/>Multi-AZ, synchronous)]
MSKb[MSK broker 2]
CASb[Cassandra 2]
end
subgraph AZc[AZ c]
EKSc[EKS nodes]
MSKc[MSK broker 3]
CASc[Cassandra 3]
REDc[(ElastiCache<br/>replica)]
end
S3[(S3 km0-photos<br/>regional, 3+ AZ)]
ALB --> EKSa & EKSb & EKSc
end
subgraph R2[Region eu-central-1, pilot light]
RDSr[(Read replica<br/>asynchronous)]
S3r[(Replicated S3)]
MSKr[MirrorMaker → minimal MSK]
EKSr[EKS: only defined<br/>in Terraform]
end
RDSa -. "asynchronous replication" .-> RDSr
S3 -. "CRR" .-> S3r
MSKa -. "MirrorMaker" .-> MSKr
DNS[Route 53<br/>km0.example] --> ALB
DNS -. "manual failover" .-> R2
- Cloud networking: VPCs, subnets, load balancers and autoscaling
A VPC (Virtual Private Cloud) is an isolated private network inside the region, with its own address range (10.0.0.0/16), divided into subnets, each of which belongs to one AZ. The convention Kilometre Zero adopts, and the usual one, is three tiers per AZ:
| Subnet | What it contains | Access from the internet | Outbound to the internet |
|---|---|---|---|
| Public | Load balancers, NAT gateway, bastion (if there is one) | Yes (they have a public IP and a route to the Internet Gateway) | Direct |
| Private, application | EKS nodes with the six services, Kong | No | Through the NAT gateway (to call the payment gateway, to pull images) |
| Private, data | RDS, MSK, ElastiCache, Cassandra nodes | No | None (neither inbound nor outbound to the internet) |
Security groups are stateful firewalls per resource, and they express the same rules as the Kubernetes network policies of 07-05 but at the VPC layer: RDS only accepts 5432 from the EKS nodes' security group; MSK only 9094 (TLS) from EKS; nothing from outside. Managed services such as S3 are reached through VPC endpoints, which keep traffic to km0-photos from going out to the internet (and avoid the NAT cost).
Managed load balancers replace the Ingress + Kong of 07-05 at its outer layer: an application load balancer (ALB on AWS, HTTP(S) Load Balancer on GCP) terminates TLS with managed, automatically renewed certificates, spreads traffic across AZs, performs health checks and exposes an IP in several AZs. Kong still exists behind it, as the gateway with its plugins (06-05); the load balancer is only the door into the region. The WebSocket of 08-02 requires the load balancer to support Upgrade and to have a long idle timeout (the ALB allows it; a layer-4 network load balancer passes it through without looking).
Autoscaling operates at two levels that must be kept apart: the HPA of 07-05 adds Pods based on CPU or Kafka lag, but Pods need nodes; the cluster autoscaler (Cluster Autoscaler or Karpenter on EKS, node pool autoscaling on GKE) adds and removes virtual machines when there are pending Pods or empty nodes. Underneath, an Auto Scaling Group of virtual machines replaces them if they fail a health check and spreads them across AZs. Grape Harvest Week with k6 (07-06) thus becomes: the HPA scales orders from 3 to 12 Pods → 6 Pods pending for lack of CPU → Karpenter starts 2 nodes in 90 s → the Pods are scheduled. Those 90 s are the reason for keeping headroom (nodes with spare capacity) ahead of an announced campaign, or for scaling on a schedule.
- Managed services: what replaces each piece of Kilometre Zero
This table is the mapping between what was built in the course and what the provider offers as a managed service. In every row, what is gained is what the provider takes on (installation, patches, failover, backups, scaling) and what is lost is control and portability.
| Kilometre Zero piece | Lesson | AWS | GCP | What the provider manages | What remains ours |
|---|---|---|---|---|---|
PostgreSQL + Patroni (km0_inventory, km0_analytics) |
03-04, 07-03 | RDS PostgreSQL Multi-AZ (or Aurora) | Cloud SQL with high availability | Failover between AZs (~60 s), automatic backups, PITR, patches, read replicas | Schema, indexes, parameters, connection pooling, restore tests |
Kafka (orders.events, delivery.*) |
02-04 | MSK (or Confluent Cloud) | Confluent Cloud or Pub/Sub (a different model) | Brokers, ZooKeeper/KRaft, patches, replicas across AZs, storage scaling | Topics, partitions, retention, schemas, consumers |
Cassandra (km0_orders) |
04-04 | Keyspaces (compatible API, serverless) or Cassandra on EC2 | Cassandra on GCE or Astra (DataStax) | Nodes, compactions, repairs, scaling | Data model, consistency levels (Keyspaces does not support all of CQL: lightweight transactions and some types need review) |
| Redis (cache, rate limiter, pub/sub) | 04-05 | ElastiCache for Redis (replicas across AZs) | Memorystore | Failover, patches, replicas | Caching strategy, TTLs, keys |
MinIO (km0-photos, km0-invoices, km0-backups, km0-audit) |
04-03 | S3 | GCS | Durability (11 nines), versioning, lifecycle, cross-region replication | Access policies, encryption with our own keys, key structure |
| HDFS + Spark + Airflow | 04-02, 05-03, 05-05 | EMR (Spark on S3, no persistent HDFS) + MWAA (Airflow) | Dataproc + Cloud Composer | Ephemeral clusters, Spark versions, managed Airflow | The jobs (daily_sales.py), the DAGs (km0_daily_sales), the partitioning of the lake on S3 |
Flink (stock_alerts.py) |
05-04 | Managed Service for Apache Flink | Dataflow (Beam model) or Flink on GKE | Checkpoints, scaling, versions | The job code and its state |
| Kubernetes | 07-05 | EKS | GKE | Control plane (API server, etcd) with an SLA, upgrades | Nodes (managed groups), manifests, operators, mesh |
| Vault | 06-04 | Secrets Manager + KMS | Secret Manager + Cloud KMS | Storage, scheduled rotation, auditing | Which secrets, who reads them (IAM), integration into the Pods |
Keycloak (realm km0) |
06-03 | Cognito | Identity Platform | High availability, scaling, federation | Realm, clients, roles, flows (with less flexibility than Keycloak) |
| Prometheus + Grafana + Loki + Tempo | 07-01, 07-02 | CloudWatch (metrics, logs) + X-Ray, or Amazon Managed Prometheus/Grafana | Cloud Monitoring + Cloud Logging + Cloud Trace | Storage, retention, alerts | Instrumentation (services/common/), dashboards, SLOs |
| Kong | 06-05 | Kong on EKS (or managed API Gateway, with fewer plugins) | Kong on GKE (or Apigee) | If managed: scaling and availability | Routes, plugins |
| Mosquitto (08-02) | 08-02 | IoT Core (retired at some providers) or EMQX on EKS | EMQX/HiveMQ on GKE | — | Everything, if self-managed |
km0-ca certificates, mTLS |
06-04 | ACM (public) + mesh for mTLS | Certificate Manager + mesh | Issuing and renewing public certificates | Internal mTLS remains the mesh's job |
Three notes on this table. First: GCP Pub/Sub is not Kafka: it has no Kafka-style partitions or offsets (although it offers ordering by key and replay), and consumer code with groups and commits changes; if you want Kafka on GCP, it is Confluent Cloud. Second: Keyspaces is CQL-compatible but it is not Cassandra: consistency levels and lightweight transactions have differences that must be verified against the 04-04 repository before migrating. Third: managed does not eliminate operations, it changes them: PostgreSQL is no longer patched by us, but the instance still has to be sized, the maintenance window chosen (RDS restarts to apply major patches), restores tested and connections watched.
- Advantages and challenges: elasticity, pay-as-you-go, lock-in, egress and compliance
| Advantage | What it brings to Kilometre Zero | Condition for it to be real |
|---|---|---|
| Elasticity | Grape Harvest Week (×15) is absorbed with nodes that exist only that week; the rest of the year you pay for 1/15 | The application must scale horizontally (Modules 4 and 7 made that possible) and the autoscaling must be tested (07-06) |
| Pay-as-you-go | No upfront investment; an empty secondary region costs nothing | The discipline to switch off what is not in use (section 9) |
| Managed services | Patroni, Cassandra repairs, Kafka patches stop being on-call duties for the platform team | Accepting less control and some functional differences |
| Global reach | Regions close to new markets; DR in another region with the same API | Latency-aware design between regions |
| Baseline security | A certified data centre, encryption at rest by default, granular IAM, integrated auditing (CloudTrail) | Configuring it: the shared responsibility of section 1 |
| Challenge | What it consists of | How Kilometre Zero handles it |
|---|---|---|
| Lock-in | The more managed, the more provider-specific: Keyspaces instead of Cassandra, Cognito instead of Keycloak, Step Functions (08-04) instead of order_saga.py. Switching providers becomes a months-long project |
Selective portability: Kubernetes and containers for the services; standard APIs (PostgreSQL, Kafka, S3, Redis, CQL) for the data, so that the managed service can be swapped for a self-managed one; accept lock-in where the benefit is large (RDS, S3) and avoid it where it is small (Cognito versus Keycloak on EKS) |
| Egress cost | Data that leaves the region is charged per GB (inbound is usually free): the photos served to Anna, the replica to the secondary region, traffic between AZs (also charged, though less) | A CDN in front of km0-photos (04-03, 08-04); Kafka consumers in the same AZ as their leader broker whenever possible (client.rack); lake and compute in the same region |
| Cost that grows with success | At a stable, high volume, the cloud can cost more than amortised in-house hardware | Reservations and spot (section 9); review every year with numbers |
| Compliance and data residency | Personal data (Anna's name, address and position; payment data) is subject to regulation (in the EU, the GDPR) that constrains where it may be stored and processed, who may access it (including the provider) and which contracts are needed. Choosing a region outside the EU, or a service that replicates metadata to another region, may breach it | Warning: this decision requires a legal review, not just a technical one. Kilometre Zero chooses EU regions (eu-west-1, eu-central-1), encrypts personal data with its own keys in KMS (06-02) and documents the sub-processors; but which data may leave the EU, under which contractual clauses and with which impact assessment is decided by the data protection officer, not the architect |
| IAM complexity | Hundreds of roles and policies; an s3:* on * in a service role is the most common data leak |
Least privilege per service (IAM Roles for Service Accounts in the hands-on part); automated review (Access Analyzer) |
| Dependence on a third party | Regions fail (it happens several times a year at every provider) | Multi-AZ for the everyday, cross-region DR for the exceptional (section 3) |
- Infrastructure as code with Terraform
In 07-05 a distinction was drawn between configuration tools (Ansible: bringing an existing machine to a given state) and provisioning tools (Terraform: creating the infrastructure itself), and the latter was deferred to this lesson. When the infrastructure is an API, describing it as code is what makes it possible to review it in a pull request, reproduce it in another region and know what exists. Terraform (and its open-source fork OpenTofu) is declarative: the desired state is described in .tf files (the HCL language) and the tool works out and executes the API calls needed to get there, in the order the dependencies dictate.
Four concepts are enough to read and write the hands-on part in section 10:
- Providers: plugins that translate resources into API calls (
aws,google,kubernetes,helm,kafka...). They are declared with a pinned version. - Resources and data sources:
resource "aws_db_instance" "inventory" { ... }creates;data "aws_availability_zones" "available" {}queries. They reference each other (aws_vpc.km0.id), and those references are the dependency graph. - Remote state: Terraform keeps in a state file the correspondence between what is declared and what exists (real ids). That file is critical and shared: it is stored in a remote backend (S3 with versioning, and a DynamoDB table as a lock so that two people do not apply at the same time; it is the same mutual exclusion problem as in 03-03, solved with a managed service), it is encrypted and it is never committed to Git. It contains secrets (initial RDS passwords), which reinforces the point.
- Modules: reusable folders of
.tffiles with input variables and outputs (module "vpc" { source = "terraform-aws-modules/vpc/aws" ... }). Kilometre Zero uses community modules for the VPC and EKS (hundreds of resources not worth writing by hand) and its own modules for each environment.
The cycle is terraform init (downloads providers and modules, connects to the state), terraform plan (computes the diff between what is declared and the state and shows it: + create, ~ modify, - destroy, -/+ replace), and terraform apply (executes it after confirmation). The plan is the most valuable piece: it runs in the pipeline (07-05) and is pasted into the pull request; a -/+ on aws_db_instance.inventory (replacing the database) is what a review must stop. In GitOps, apply is only ever run by the pipeline after the merge, never by a person from their laptop.
- Well-architected design: the pillars
Providers publish "well-architected" frameworks with pillars that serve as a checklist. The six AWS pillars (GCP has an equivalent list) match course modules almost one to one:
| Pillar | The question it asks | Where Kilometre Zero has worked on it |
|---|---|---|
| Operational excellence | Is it operated with code, observed, and are incidents learned from? | IaC (07-05, this lesson), observability (07-01/07-02), postmortems and runbooks (07-03), GitOps |
| Security | Identity, least privilege, encryption, auditing, defence in depth? | The whole of Module 6; IAM and security groups in this lesson |
| Reliability | Does it survive component, AZ and region failures? Has that been tested? | Redundancy and failover (07-03), resilience (07-04), chaos (07-06), Multi-AZ and DR (section 3) |
| Performance efficiency | Are the right resources used for each workload, and are they measured? | Storage per workload (Module 4), cache (04-05), autoscaling, p99 SLO (07-01) |
| Cost optimisation | Do you pay for what you use, measure it and review it? | FinOps (section 9) |
| Sustainability | Is consumption minimised: switching off idle resources, regions with clean energy, code efficiency? | Scaling down, ephemeral Spark clusters, data retention (04-02) |
Their practical use is the periodic review: every six months, walk through the framework's questions against the platform and note what is missing. It is an exercise in the final project (08-05).
- FinOps: tags, reservations, spot, budgets and the cost of Kilometre Zero
In the cloud, cost is one more metric of the system, and like any metric it has to be measured, attributed and alerted on. The discipline is called FinOps and, at its basic level, it boils down to five practices:
- Tagging: every resource carries
team,service,environmentandcost_centretags. Without them, the bill is a number; with them, "the delivery team spent €1,840 in September, 60% of it on MSK". Terraform applies them to everything by default (default_tags). - Reservations and savings plans: committing to usage for 1 or 3 years in exchange for a 30-60% discount. Applied to the baseline that is always on (the minimum EKS nodes, RDS, MSK, ElastiCache), never to the peak.
- Spot instances: the provider's spare capacity at a 60-90% discount, which can be reclaimed with 2 minutes' notice. Ideal for whatever tolerates interruptions and restarts on its own: Spark executors (05-03 already tolerated losing executors by recomputing partitions), EKS nodes for stateless workloads with several replicas. Never for RDS, Cassandra, Kafka or the control plane.
- Budgets and alerts: a budget per team and environment, with alerts at 80% and 100% of forecast, and anomaly detection (a NAT gateway that suddenly moves 2 TB because someone served the photos without a CDN). They are alerts like those of 07-01, sent to the same channel.
- Switching off idle resources:
stagingenvironments switched off at night and at weekends (with Terraform or with schedules), ephemeral Spark clusters (EMR starts up for thekm0_daily_salesDAG and is destroyed when it finishes), orphaned volumes and snapshots.
Approximate monthly cost of Kilometre Zero on AWS
The prices below are fictional and indicative, of the right order of magnitude for a European region, and serve the reasoning, not as a budget. The useful exercise is to see where the cost is, not the exact figure.
| Component | Sizing | Base monthly cost (€) | With reservations / spot (€) | Note |
|---|---|---|---|---|
| EKS control plane | 1 cluster | 70 | 70 | Fixed |
| EKS nodes (services, Kong, mesh, observability) | 6 × 4 vCPU/16 GB permanent + up to 12 during a campaign | 900 + 300 (average of peaks) | 550 + 120 (reserved baseline, spot for peaks) | Largest compute item |
RDS PostgreSQL Multi-AZ (km0_inventory) |
2 vCPU/8 GB × 2 (standby), 200 GB | 380 | 250 | Multi-AZ doubles it |
RDS PostgreSQL (km0_analytics) |
2 vCPU/8 GB, 500 GB | 210 | 140 | No Multi-AZ: tolerates an RTO of hours |
| MSK | 3 brokers × 2 vCPU/8 GB, 1 TB total | 650 | 450 | Cost per broker-hour + storage |
| Cassandra on EC2 (3 nodes) | 3 × 4 vCPU/16 GB + 3 × 500 GB SSD | 480 | 320 | Keyspaces would be per read/write; with 1.2 M orders/month it comes out similar |
| ElastiCache Redis | 2 nodes, 2 GB (primary + replica) | 90 | 60 | |
S3 (km0-photos 30 GB, km0-invoices, km0-backups 800 GB, km0-audit) |
~1 TB + requests | 35 | 35 | Storage is cheap |
| Egress to the internet (photos, API) | 3 TB/month without a CDN | 240 | 240 → ~40 with a CDN (08-04) | The item the CDN reduces |
| Traffic between AZs | ~2 TB/month (replicas, consumers) | 40 | 40 | Always forgotten |
| NAT gateway | 2 (one per active AZ) + 500 GB | 110 | 110 | VPC endpoints for S3 reduce it |
| ALB | 1 + traffic | 45 | 45 | |
| Ephemeral EMR (Spark) | 2 h/day × 6 spot nodes | 180 | 60 | Spot: −70% |
| MWAA (Airflow) | Small environment | 320 | 320 | Expensive for what it does: a candidate for Airflow on EKS (~40) |
| Managed Flink | 4 KPU | 420 | 420 | Alternative: Flink on EKS |
| Secrets Manager + KMS | 60 secrets, 3 keys | 45 | 45 | |
| CloudWatch (logs 200 GB/month, metrics) | 180 | 180 | Or Loki/Tempo/Prometheus on EKS (cost in nodes) | |
| Secondary region (pilot light) | RDS replica, S3 CRR, minimal MSK | 350 | 350 | ~10% of the primary |
| Provider support | Business plan | 250 | 250 | |
| Approximate total | ≈ 5,300 | ≈ 3,900 |
What the table teaches: compute (EKS + databases + Kafka) is two thirds of the total; reservations save 25-30% on the baseline; egress is the item most sensitive to design (the CDN of 08-04 divides it by six); and two managed services (MWAA, managed Flink) cost more than their self-managed equivalents on the cluster, which illustrates that "managed" does not always win: the decision depends on how much the time of the team that would operate it is worth.
- Hands-on: Kilometre Zero on AWS with Terraform and EKS
The structure of the infrastructure code, added to the km0/ project:
km0/infra/ ├── aws/ │ ├── main.tf # providers, state backend, VPC, EKS, RDS, MSK, ElastiCache, S3, IAM │ ├── variables.tf # region, environment, sizes │ ├── outputs.tf # endpoints the k8s/ manifests need │ └── environments/ │ ├── prod.tfvars │ └── staging.tfvars └── gcp/ # the GCP equivalent (table only in this lesson)
What follows is main.tf explained block by block. It is simplified (some security and networking parameters that a real module includes are missing), but every block is real and corresponds to a decision from the previous sections.
Block 1: providers and remote state
# km0/infra/aws/main.tf
terraform {
required_version = ">= 1.6"
required_providers {
aws = { source = "hashicorp/aws", version = "~> 5.40" }
kubernetes = { source = "hashicorp/kubernetes", version = "~> 2.27" }
helm = { source = "hashicorp/helm", version = "~> 2.12" }
}
# Remote state: S3 with versioning (to recover a previous state) and
# DynamoDB as a lock (two simultaneous 'apply' runs exclude each other).
backend "s3" {
bucket = "km0-terraform-state"
key = "prod/infra.tfstate"
region = "eu-west-1"
dynamodb_table = "km0-terraform-lock"
encrypt = true
}
}
provider "aws" {
region = var.region # "eu-west-1": an EU region (compliance, section 6)
default_tags { # FinOps: every piece carries these tags
tags = { project = "km0", environment = var.environment, managed_by = "terraform" }
}
}
data "aws_availability_zones" "available" { state = "available" }
locals {
azs = slice(data.aws_availability_zones.available.names, 0, 3) # three AZs: a, b, c
}Block 2: the VPC with three tiers per AZ
module "vpc" {
source = "terraform-aws-modules/vpc/aws"
version = "~> 5.5"
name = "km0-${var.environment}"
cidr = "10.0.0.0/16"
azs = local.azs
public_subnets = ["10.0.0.0/24", "10.0.1.0/24", "10.0.2.0/24"] # ALB, NAT
private_subnets = ["10.0.10.0/23", "10.0.12.0/23", "10.0.14.0/23"] # EKS nodes (512 IPs per AZ: Pods consume IPs)
database_subnets = ["10.0.20.0/24", "10.0.21.0/24", "10.0.22.0/24"] # RDS, MSK, ElastiCache, Cassandra
enable_nat_gateway = true
one_nat_gateway_per_az = true # without this, a NAT in a single AZ is a single point of failure for outbound traffic
create_database_subnet_group = true
# Tags EKS needs to discover which subnets to create load balancers in
public_subnet_tags = { "kubernetes.io/role/elb" = 1 }
private_subnet_tags = { "kubernetes.io/role/internal-elb" = 1 }
}
# VPC endpoint for S3: traffic to km0-photos neither goes out to the internet nor through the NAT (cost and security)
resource "aws_vpc_endpoint" "s3" {
vpc_id = module.vpc.vpc_id
service_name = "com.amazonaws.${var.region}.s3"
route_table_ids = module.vpc.private_route_table_ids
}Block 3: EKS with permanent and spot node groups
module "eks" {
source = "terraform-aws-modules/eks/aws"
version = "~> 20.8"
cluster_name = "km0-${var.environment}"
cluster_version = "1.29"
vpc_id = module.vpc.vpc_id
subnet_ids = module.vpc.private_subnets # nodes never in public subnets
cluster_endpoint_public_access = true # kubectl from the pipeline (restrict by CIDR in prod)
enable_irsa = true # IAM Roles for Service Accounts (block 6)
eks_managed_node_groups = {
base = { # permanent capacity: this is where the reservations go
instance_types = ["m6i.xlarge"] # 4 vCPU / 16 GB
min_size = 6, desired_size = 6, max_size = 9
labels = { "km0/pool" = "base" }
}
campaign = { # elastic capacity on spot: stateless Pods only
instance_types = ["m6i.xlarge", "m5.xlarge", "m6a.xlarge"] # several types: better odds of getting spot
capacity_type = "SPOT"
min_size = 0, desired_size = 0, max_size = 12
labels = { "km0/pool" = "spot" }
taints = [{ key = "km0/spot", value = "true", effect = "NO_SCHEDULE" }] # only what explicitly tolerates it
}
}
}The orders and catalog Pods carry in k8s/orders-deployment.yaml a toleration for km0/spot and an affinity that prefers base, so that during a campaign they overflow onto spot; Cassandra, Kafka (if it were self-managed) and the databases do not tolerate the taint and never land on spot. The cluster autoscaler (Karpenter, installed with the helm provider) is what takes the campaign group from 0 to 12 according to the pending Pods.
Block 4: RDS PostgreSQL Multi-AZ for km0_inventory
resource "aws_db_subnet_group" "data" {
name = "km0-data"
subnet_ids = module.vpc.database_subnets
}
resource "aws_security_group" "rds" {
name = "km0-rds"
vpc_id = module.vpc.vpc_id
ingress { # only 5432 and only from the EKS nodes
from_port = 5432, to_port = 5432, protocol = "tcp"
security_groups = [module.eks.node_security_group_id]
}
}
resource "aws_db_instance" "inventory" {
identifier = "km0-inventory-${var.environment}"
engine = "postgres"
engine_version = "16"
instance_class = var.environment == "prod" ? "db.m6g.large" : "db.t4g.medium"
allocated_storage = 200
storage_encrypted = true
kms_key_id = aws_kms_key.data.arn # our own key (06-02): we control rotation and access
db_name = "km0_inventory"
username = "km0_admin"
manage_master_user_password = true # the password is generated and stored by Secrets Manager
multi_az = var.environment == "prod" # synchronous standby in another AZ: replaces Patroni (07-03)
backup_retention_period = 14 # daily backups + continuous WAL: 14-day PITR
backup_window = "02:00-03:00"
maintenance_window = "sun:03:30-sun:04:30" # RDS restarts for major patches: a known window
deletion_protection = var.environment == "prod" # an accidental 'terraform destroy' does not delete production
performance_insights_enabled = true
db_subnet_group_name = aws_db_subnet_group.data.name
vpc_security_group_ids = [aws_security_group.rds.id]
}
# Read replica in the secondary region: the basis of the pilot light (07-03, section 3)
resource "aws_db_instance" "inventory_dr" {
provider = aws.secondary # a second provider with region = "eu-central-1"
identifier = "km0-inventory-dr"
replicate_source_db = aws_db_instance.inventory.arn
instance_class = "db.t4g.medium" # small: resized on promotion
kms_key_id = aws_kms_key.data_dr.arn
skip_final_snapshot = true
}What replaces the hand-built pieces here: multi_az = true is Patroni + etcd + synchronous replica + automatic failover (RDS switches the endpoint's DNS to the standby in ~60 s; the application only needs to reconnect, which is what the pool with retries from 07-04 already does); backup_retention_period is the pgBackRest and WAL archiving of 07-03. What it does not replace: the weekly restore test, which remains an Airflow job that restores the latest snapshot onto a temporary instance and runs checks.
Block 5: MSK, ElastiCache and S3
resource "aws_msk_cluster" "events" {
cluster_name = "km0-events-${var.environment}"
kafka_version = "3.6.0"
number_of_broker_nodes = 3 # one per AZ: partition replicas spread out
broker_node_group_info {
instance_type = "kafka.m5.large"
client_subnets = module.vpc.database_subnets
security_groups = [aws_security_group.msk.id]
storage_info { ebs_storage_info { volume_size = 350 } } # 3 × 350 GB ≈ 1 TB: the retention from 02-04
}
encryption_info {
encryption_in_transit { client_broker = "TLS", in_cluster = true } # mandatory TLS (06-02)
}
configuration_info {
arn = aws_msk_configuration.events.arn
revision = aws_msk_configuration.events.latest_revision
}
}
resource "aws_msk_configuration" "events" {
name = "km0-events"
server_properties = <<-EOT
default.replication.factor=3
min.insync.replicas=2
auto.create.topics.enable=false
EOT # the same values as in 02-04 and 07-03
}
resource "aws_elasticache_replication_group" "cache" {
replication_group_id = "km0-cache-${var.environment}"
description = "Catalog cache, rate limiter, delivery pub/sub"
engine = "redis"
node_type = "cache.t4g.small"
num_cache_clusters = 2 # primary + replica in another AZ
automatic_failover_enabled = true
multi_az_enabled = true
at_rest_encryption_enabled = true
transit_encryption_enabled = true
subnet_group_name = aws_elasticache_subnet_group.data.name
security_group_ids = [aws_security_group.redis.id]
}
resource "aws_s3_bucket" "photos" {
bucket = "km0-photos-${var.environment}"
}
resource "aws_s3_bucket_versioning" "photos" { # versioning, as in MinIO (04-03)
bucket = aws_s3_bucket.photos.id
versioning_configuration { status = "Enabled" }
}
resource "aws_s3_bucket_public_access_block" "photos" { # never public: served via presigned URL or CDN
bucket = aws_s3_bucket.photos.id
block_public_acls = true, block_public_policy = true, ignore_public_acls = true, restrict_public_buckets = true
}
resource "aws_s3_bucket_lifecycle_configuration" "photos" {
bucket = aws_s3_bucket.photos.id
rule {
id = "old-versions"
status = "Enabled"
noncurrent_version_expiration { noncurrent_days = 30 } # the lifecycle rule from 04-03
}
}
resource "aws_s3_bucket_replication_configuration" "photos_dr" { # CRR to the secondary region (pilot light)
bucket = aws_s3_bucket.photos.id
role = aws_iam_role.s3_replication.arn
rule {
id = "dr"
status = "Enabled"
destination { bucket = aws_s3_bucket.photos_dr.arn, storage_class = "STANDARD_IA" }
}
}Block 6: IAM roles for service accounts (IRSA)
The catalog Pods need to write to km0-photos; the orders Pods need to read their secrets. Instead of long-lived credentials in a Kubernetes Secret (which 06-04 avoided with Vault), EKS allows an IAM role to be bound to a Kubernetes service account: the Pod obtains temporary credentials automatically, with least privilege, and CloudTrail records who did what. It is the workload identity of 06-04 (SPIFFE) with the provider's IAM.
data "aws_iam_policy_document" "catalog_photos" {
statement {
actions = ["s3:PutObject", "s3:GetObject", "s3:DeleteObject"] # no s3:* and no ListAllMyBuckets
resources = ["${aws_s3_bucket.photos.arn}/photos/*"] # only the photos prefix
}
}
module "irsa_catalog" {
source = "terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks"
version = "~> 5.39"
role_name = "km0-catalog-${var.environment}"
role_policy_arns = { photos = aws_iam_policy.catalog_photos.arn }
oidc_providers = {
eks = {
provider_arn = module.eks.oidc_provider_arn
namespace_service_accounts = ["km0-prod:catalog"] # ONLY this service account can assume the role
}
}
}In the k8s/catalog-deployment.yaml manifest all that is needed is serviceAccountName: catalog and the eks.amazonaws.com/role-arn: <role arn> annotation on the ServiceAccount; the AWS SDK in the Pod (boto3, the same photos.py from 04-03 pointed at S3 instead of MinIO) finds the credentials on its own.
Block 7: outputs for the manifests
# km0/infra/aws/outputs.tf
output "rds_inventory_endpoint" { value = aws_db_instance.inventory.address }
output "msk_bootstrap_tls" { value = aws_msk_cluster.events.bootstrap_brokers_tls }
output "redis_endpoint" { value = aws_elasticache_replication_group.cache.primary_endpoint_address }
output "bucket_photos" { value = aws_s3_bucket.photos.bucket }
output "eks_cluster_name" { value = module.eks.cluster_name }Deploying the k8s/ manifests from 07-05 on EKS
The manifests do not change in shape: what changes are the configuration values pointing at the dependencies. In 07-05, orders-config had KAFKA_BOOTSTRAP=kafka-0.kafka:9092; now the pipeline generates it from the Terraform outputs:
# km0/infra/aws/deploy.sh — run by the pipeline after 'terraform apply'
set -euo pipefail
cd km0/infra/aws
terraform init -input=false
terraform plan -var-file=environments/prod.tfvars -out=plan.bin # the plan is attached to the PR
terraform apply -input=false plan.bin
aws eks update-kubeconfig --name "$(terraform output -raw eks_cluster_name)" --region eu-west-1
# ConfigMaps with the real endpoints (the 07-05 manifests reference them by name)
kubectl -n km0-prod create configmap platform-endpoints \
--from-literal=KAFKA_BOOTSTRAP="$(terraform output -raw msk_bootstrap_tls)" \
--from-literal=PG_INVENTORY_HOST="$(terraform output -raw rds_inventory_endpoint)" \
--from-literal=REDIS_URL="rediss://$(terraform output -raw redis_endpoint):6379" \
--from-literal=S3_BUCKET_PHOTOS="$(terraform output -raw bucket_photos)" \
--dry-run=client -o yaml | kubectl apply -f -
# The same manifests as in 07-05: Deployments, Services, HPA, PDB, NetworkPolicies, canary
kubectl apply -k km0/k8s/overlays/aws-prod # kustomize: the 07-05 base + patches (serviceAccountName, spot tolerations)
kubectl -n km0-prod rollout status deployment/orders --timeout=300sWhat disappears from k8s/ when moving to managed services: the StatefulSets for PostgreSQL with Patroni, for Kafka and for Redis, and their operators. What stays: the six services, Kong, the mesh, Cassandra (if Keyspaces is not adopted), Prometheus/Loki/Tempo (if CloudWatch is not adopted) and Mosquitto. The HPA of 07-05 works the same; what is new is Karpenter underneath. And the tests of 07-06 are repeated against the staging environment deployed with staging.tfvars (small instances, no Multi-AZ, no DR), which is switched off at night.
The GCP equivalent
Resource on AWS (main.tf) |
GCP equivalent (google provider) |
Relevant difference |
|---|---|---|
| VPC with subnets per AZ | google_compute_network + google_compute_subnetwork (subnets are regional, spanning every zone) |
Fewer subnets; zone distribution is done by GKE |
| EKS + node groups | GKE (regional google_container_cluster + google_container_node_pool, with Autopilot as a nodeless option) |
Regional GKE spreads the control plane and the nodes across 3 zones by default |
| RDS Multi-AZ | Cloud SQL with availability_type = "REGIONAL" |
Similar failover (~60 s); PITR with point_in_time_recovery_enabled |
| MSK | Confluent Cloud (Kafka) or Pub/Sub (a different model: no Kafka-style partitions or offsets) | Pub/Sub requires rewriting consumers; Confluent keeps the code |
| ElastiCache | Memorystore for Redis (tier = "STANDARD_HA") |
No cluster mode in the basic tiers |
| S3 + CRR | GCS with a dual-region or multi-region bucket | Cross-region replication is a property of the bucket, not a rule |
| IRSA | Workload Identity (Kubernetes service account ↔ Google service account) | Same concept |
| Secrets Manager + KMS | Secret Manager + Cloud KMS | Equivalent |
| EMR / MWAA / managed Flink | Dataproc / Cloud Composer / Dataflow | Dataflow uses the Beam model, not the Flink API |
| CloudWatch | Cloud Monitoring + Cloud Logging + Cloud Trace | Equivalent |
| Remote state on S3 + DynamoDB | gcs backend (the bucket itself provides the lock) |
Simpler |
Common Mistakes and Tips
- Assuming that "managed" means "no operations". RDS still restarts in the maintenance window, MSK still needs partitions sized, and nobody tests the restores for us. The work changes; it does not disappear.
- A single AZ "to save money". One NAT, one subnet, RDS without Multi-AZ: the provider's first AZ incident is a total outage. Multi-AZ for everything on the path of an order.
- Forgetting egress and inter-AZ traffic in the budget. They are the two items that surprise on the first bill. A CDN for public content, VPC endpoints for S3,
client.rackon Kafka consumers. - IAM roles with
*. A service role withs3:*over every bucket turns a compromised Pod into a total leak. One policy per service account, with specific actions and resources (block 6). - Terraform state in Git or on a laptop. It contains secrets and is the only map of what exists. An encrypted remote backend with versioning and a lock, always.
- Applying without reading the plan. A
-/+on a database is a destroy-and-recreate. The plan goes in the pull request anddeletion_protectionon production. - Choosing the region only on price or latency. Personal data has residency restrictions; the region is a decision with a legal review, and a documented one.
- Buying reservations for the peak. Reservations cover the baseline that is always on; the peak goes to spot or on-demand. Over-buying is paying for capacity that is not used.
- Tip: deploy
stagingfirst with the same Terraform configuration and small sizes; 90% of the networking, IAM and security group mistakes show up there at minimal cost. - Tip: put cost in Grafana next to the other metrics (providers export daily spend per tag). A chart of euros per order is the business metric that generates the most useful conversations.
Exercises
Exercise 1: failure domains in the VPC
Review the main.tf from section 10 and answer: (a) if AZ b disappears entirely, which Kilometre Zero components are affected, which mechanism recovers each one and in roughly how long; (b) what would have happened with one_nat_gateway_per_az = false; (c) which component of main.tf would not survive the loss of the region, and what would have to be run to recover it.
Exercise 2: the bill for changing a decision
Using the cost table from section 9 (fictional prices), calculate the approximate monthly saving or extra cost of these three decisions and reason about whether they are worth it: (a) replacing MWAA with Airflow on EKS (estimate: €40 in nodes); (b) moving km0_analytics on RDS to Multi-AZ; (c) moving the permanent EKS nodes from on-demand to a 3-year reservation (estimated discount 55% instead of 40%), knowing that the team plans to reduce to 4 permanent nodes within a year.
Exercise 3: selective lock-in
For each of these components, decide whether Kilometre Zero should use the provider's managed service or deploy it on EKS, applying the "selective lock-in" criterion from section 6 (benefit of the managed service versus cost of the dependency): PostgreSQL, Kafka, Cassandra, Keycloak, Vault, Prometheus/Grafana. Justify each one in two sentences.
Solutions
Exercise 1.
(a) With AZ b down: RDS km0_inventory: the standby was in b (or the primary; RDS decides); if it was the standby, nothing visible happens and RDS creates a new one in another AZ; if it was the primary, automatic failover to the standby in ~60 s, during which stock reservations fail and the saga retries (07-04). MSK: broker 2 goes down; the partitions whose leader was there elect a new leader from among the ISR (03-03) within seconds; with min.insync.replicas=2 and RF=3, writes continue. Cassandra: one node out of three; with QUORUM (2 of 3), reads and writes continue. ElastiCache: if the primary was in b, automatic failover to the replica (~30 s) and the cache refills; if it was the replica, nothing. EKS: the nodes in b disappear; Kubernetes reschedules their Pods onto a and c (07-05: topologySpreadConstraints guaranteed that no service had all its replicas in b) in 1-3 minutes, and Karpenter adds nodes if capacity is short. ALB: it stops sending to b once the health checks fail (seconds). NAT: the one in b goes down, but so do the nodes in b, so nobody needs it.
(b) A single NAT in, say, AZ a: if a goes down, the nodes in b and c stay alive but lose their outbound internet access: payments cannot reach the gateway, the pipeline cannot pull images. A healthy component is rendered useless by someone else's failure domain: exactly what 07-03 called non-independent redundancy.
(c) Everything in the primary region. What exists in the secondary: the RDS replica (inventory_dr), the replicated bucket and the minimal MSK with MirrorMaker. To recover: terraform apply of the same code with region = "eu-central-1" (some 20-30 min for VPC + EKS + ElastiCache), promote the RDS replica (aws rds promote-read-replica, ~5 min), deploy the manifests pointing at the new endpoints, restore Cassandra from the snapshots replicated to S3 (the longest step and the one that sets the RPO for orders), and switch the Route 53 record. It is the pilot light runbook from 07-03, and the real RTO is whatever comes out of the game day, not the one in the table.
Exercise 2.
(a) MWAA €320 → Airflow on EKS €40: saving ≈ €280/month (€3,360/year). It is worth it if the platform team takes on operating Airflow (upgrades, metadata database, on-call); with the Kubernetes operator and a small RDS instance for the metadata (~€30 more), it is still worth it, and it also avoids a provider-specific dependency. It is the example of managed not always winning.
(b) €210 → roughly double, ≈ €420: extra cost ≈ €210/month. For km0_analytics, whose temporary loss does not prevent selling (an RTO of hours in 07-03; it is rebuilt from the lake), it is not worth it; the 07-03 decision still stands.
(c) Node baseline: €900 on-demand; with a 1-year reservation at 40%: €540; with a 3-year reservation at 55%: €405. Difference in favour of 3 years: €135/month. But the 3-year reservation covers 6 nodes and within a year only 4 will be used: 2 reserved nodes would be paid for unused for 2 years (≈ 2 × €67 × 24 months ≈ €3,200), against a saving of 135 × 12 = €1,620 in the first year. Not worth it: a 1-year reservation, renegotiable when the sizing changes. Reservations are bought for the stable baseline, and "stable" includes the foreseeable future.
Exercise 3.
- PostgreSQL → RDS (managed). High benefit (Multi-AZ, PITR and patches replace the entire Patroni cluster) and low lock-in: it is standard PostgreSQL, a
pg_dumpmoves it anywhere. - Kafka → MSK (managed). High benefit (the broker rolling restarts of 07-05 and disk management disappear) and low lock-in: standard Kafka protocol, MirrorMaker replicates it to any Kafka. On GCP, Confluent rather than Pub/Sub for the same reason.
- Cassandra → on EKS (or EC2), not Keyspaces. Keyspaces changes the semantics (lightweight transactions, some types, a per-request cost model) and the 04-04 repository would need review: high lock-in for a medium benefit. It stays self-managed with the operator from 07-05, and is reconsidered if the platform team cannot keep up.
- Keycloak → on EKS, not Cognito. Cognito is more limited in flows and customisation, and migrating users and clients away from an identity provider is painful: high lock-in. Keycloak with two replicas and RDS behind it is easy to operate.
- Vault → Secrets Manager (managed), with a caveat. For static secrets and their rotation, Secrets Manager with IRSA is simpler and integrates with IAM. The dynamic database credentials and the PKI for mTLS that Vault provided in 06-04 have no direct equivalent: if the mesh takes over mTLS (08-01), Vault can be retired; if not, Vault stays on EKS for that alone.
- Prometheus/Grafana → on EKS. The
services/common/instrumentation is standard (OpenTelemetry, Prometheus exposition) and the 07-01 dashboards and alerts already exist; CloudWatch would cost more and would change the query language (PromQL). Avoidable lock-in at low cost. The provider's managed Prometheus/Grafana services are an acceptable middle ground if metrics storage grows.
Conclusion
The cloud turns infrastructure into an API, and with that it changes three things in the design of Kilometre Zero. The first is responsibility: IaaS, PaaS, SaaS and FaaS move the line between what the provider does and what the customer does, but data and identities always stay on this side of it. The second is geography: regions and availability zones are failure domains with names, AZs absorb everyday failures with Multi-AZ on everything an order touches, and the secondary region of the 07-03 pilot light becomes Terraform code that costs nothing while it is not run. The third is substitution: RDS for Patroni, MSK for the Ansible-managed brokers, S3 for MinIO, EKS for the in-house cluster, with a table that says what is gained and what is lost in every row, and a selective lock-in criterion (managed where the benefit is high and the API standard, self-hosted where the dependency would cost more than the operation). Terraform describes the VPC with three tiers per AZ, EKS with base and spot nodes, Multi-AZ RDS with its replica in another region, MSK, ElastiCache, S3 with versioning and replication, and IRSA so that every Pod has only the permissions it needs; the 07-05 manifests are deployed almost unchanged, and the Terraform plan is reviewed in the pull request like any other code. The well-architected pillars summarise the course as a checklist, and FinOps adds cost as a metric: tags, reservations for the baseline, spot for Spark and the peaks, budgets with alerts, and a table showing that compute is two thirds of the bill, that two managed services cost more than their equivalents on the cluster, and that egress is the item design can reduce the most.
Precisely that item, and two more questions, open the next lesson. Does it take a service on Kubernetes, with its replicas, its probes and its on-call rota, to generate a thumbnail every time Montblanc Dairy uploads a photo, or to produce a PDF when a payment.confirmed arrives? And why do the aged-cheese photos have to travel from Ireland to Anna's phone in Valencia on every visit, when they could be twenty kilometres away from her? Functions that exist only while there is an event to process, and compute and cache at the edge of the network: Serverless and Edge Computing Architectures.
Distributed Architectures Course
Module 1: Introduction to Distributed Systems
- Basic Concepts of Distributed Systems
- Distributed System Models
- Advantages and Challenges of Distributed Systems
- The Fallacies of Distributed Computing
- Time, Clocks and Event Ordering
- From Monolith to Distributed Platform: the Kilometre Zero Case
Module 2: Communication in Distributed Systems
- Communication Protocols
- RPC and RMI
- gRPC and Data Serialization
- Messaging and Message Queues
- Asynchronous Communication Patterns
Module 3: Consistency and Replication
- Consistency Models
- The CAP Theorem and PACELC
- Consensus Algorithms
- Data Replication
- Distributed Transactions and Sagas
Module 4: Distributed Storage
- Data Partitioning and Consistent Hashing
- Distributed File Systems
- Object Storage
- Distributed Databases
- Distributed Caches
Module 5: Distributed Computing
- Distributed Computing Models
- MapReduce and Hadoop
- Spark and In-Memory Computing
- Stream Processing
- Job Scheduling and Data Pipelines
Module 6: Security in Distributed Systems
- Authentication and Authorization
- Encryption and Data Protection
- Identity Management
- Service-to-Service Security: mTLS and Secrets Management
- API Gateways, Rate Limiting and Auditing
Module 7: Monitoring and Maintenance
- Monitoring Distributed Systems
- Centralized Logs and Distributed Tracing
- Failure Management and Recovery
- Resilience Patterns: Timeouts, Retries and Circuit Breakers
- Automation and Orchestration
- Testing Distributed Systems and Chaos Engineering
