A high-performing cloud-native application on Google Cloud is rarely a single binary that runs faster. It is a deliberate stack of choices, the compute substrate, the way containers are built, the load balancer in front, the cache between request and backing store, the API mediation layer, and the asynchronous fabric that absorbs spikes. This section walks through each layer in the order a developer encounters them when designing a service from scratch. The goal is to give you the decision framing the exam tests: not "what is Cloud Run" but "given this request shape, why is Cloud Run the answer and what would push you off it".
The starting principle is that performance and scalability are properties of the system, not of any one service. A Cloud Run service that calls a single-region Cloud SQL instance in a far region will be slow no matter how many instances it spawns. A GKE deployment that holds session state in memory will not scale horizontally even with a Horizontal Pod Autoscaler. The design decisions in this section interlock; treat them as a single budget.
Choosing a platform based on application requirements
Three services share most container workloads on Google Cloud: Compute Engine, Google Kubernetes Engine (GKE), and Cloud Run. The decision tree is narrow.
Compute Engine is the IaaS substrate. You pick a machine type (E2, N2, N4, C3, C4 for general purpose; C2, C2D, C3 for compute-optimized; M2, M3 for memory-optimized; A2, A3, G2 for accelerator workloads), attach persistent or hyperdisk volumes, and run any operating system or runtime. Pick Compute Engine when the workload is not containerized, when the licensing model requires a long-lived host (SAP, Oracle, Windows Server with BYOL), when the application depends on kernel modules or low-level networking that GKE will not expose, or when the team owns an existing VM image they will not refactor.
GKE is the Kubernetes platform. It runs in two operational modes. GKE Standard exposes node pools, and you size, scale, and patch them yourself; the cluster SLA covers the control plane only. GKE Autopilot hides nodes entirely; Google bills per requested pod CPU and memory, applies a hardened pod security policy by default, and offers a workload-level SLA. Choose GKE Standard when you need GPU node pools, DaemonSets with broader privileges, custom kernel parameters, or precise bin-packing for cost. Choose GKE Autopilot when you want Kubernetes primitives (Deployments, Services, ConfigMaps, custom controllers) without the node-operations toil. Both modes use Workload Identity to bind a Kubernetes Service Account to a Google Cloud Service Account; on Autopilot it is on by default, on Standard you opt in via the --workload-pool=<PROJECT>.svc.id.goog flag at cluster creation.
Cloud Run is the request-driven serverless runtime. It accepts any container that listens on a port (default 8080) and scales from zero to many instances based on concurrent requests. Cloud Run has three product surfaces: Cloud Run services (HTTP request-response, can scale to zero), Cloud Run jobs (run-to-completion batch containers, no HTTP), and Cloud Run functions (single-purpose handlers, formerly Cloud Functions, running on the Cloud Run runtime). All three share the same security model, networking surface, and IAM bindings.
# Deploy a containerized HTTP API to Cloud Run
gcloud run deploy orders-api \
--image=us-central1-docker.pkg.dev/orders-prod/api/orders:v3 \
--region=us-central1 \
--service-account=orders-runtime@orders-prod.iam.gserviceaccount.com \
--min-instances=1 \
--max-instances=50 \
--concurrency=80 \
--cpu=2 \
--memory=2Gi \
--no-allow-unauthenticated \
--ingress=internal-and-cloud-load-balancing
# Create a GKE Autopilot cluster and deploy a workload
gcloud container clusters create-auto orders-cluster \
--region=us-central1 \
--release-channel=stable
gcloud container clusters get-credentials orders-cluster --region=us-central1
kubectl apply -f orders-deployment.yaml
💡 Exam Trap: Cloud Run jobs do not accept HTTP traffic. A stem describing "a nightly container that processes a CSV from Cloud Storage and exits" points to Cloud Run jobs, never Cloud Run services. The reverse trap names "a web API that handles bursty user requests" and the wrong answer suggests Cloud Run jobs.
⚠️ Anti-Pattern: Using Compute Engine for a stateless HTTP API only because the team already has Terraform for VMs. The right move is to refactor to a container and target Cloud Run or GKE; Compute Engine carries operational overhead the workload does not need.
Building, refactoring, and deploying application containers to Cloud Run and Google Kubernetes Engine
Container builds on Google Cloud start in Artifact Registry (the successor to Container Registry) and Cloud Build. Artifact Registry repositories are regional; pushing a Docker repository to us-central1-docker.pkg.dev/<project>/<repo> keeps the image co-located with the workload that pulls it.
Source-based deployments to Cloud Run are the fastest path: gcloud run deploy --source . invokes Cloud Build buildpacks to detect the language (Go, Java, Node, Python, Ruby, PHP), produce a container, push it to Artifact Registry, and roll out a Cloud Run revision. No Dockerfile is required for supported runtimes; one is honored if present.
# Source-based deploy without a Dockerfile
gcloud run deploy invoices-svc --source . --region=us-east1
For GKE, the canonical CI/CD shape uses Cloud Build to build and push, then Cloud Deploy or kubectl apply to roll out:
# cloudbuild.yaml: build, push to Artifact Registry, then update GKE
steps:
- name: gcr.io/cloud-builders/docker
args: ["build", "-t", "us-central1-docker.pkg.dev/$PROJECT_ID/web/orders:$SHORT_SHA", "."]
- name: gcr.io/cloud-builders/docker
args: ["push", "us-central1-docker.pkg.dev/$PROJECT_ID/web/orders:$SHORT_SHA"]
- name: gcr.io/cloud-builders/gke-deploy
args:
- run
- --filename=k8s/
- --image=us-central1-docker.pkg.dev/$PROJECT_ID/web/orders:$SHORT_SHA
- --cluster=orders-cluster
- --location=us-central1
options:
logging: CLOUD_LOGGING_ONLY
Refactoring a monolith for these targets follows a predictable order: extract a single HTTP entry point, externalize configuration to environment variables, replace in-process scheduling with Cloud Scheduler or Cloud Tasks, push session state into Memorystore, then containerize. Skipping the externalization steps produces a container that runs but does not scale horizontally.
💡 Exam Trap: A scenario that describes "uploading a JAR to a managed runtime" without mentioning containers points to App Engine Standard, not Cloud Run. App Engine Standard remains for the language-specific runtimes; new builds prefer Cloud Run with a container.
Geographic distribution of Google Cloud services and latency implications of compute and storage location
A Google Cloud region (for example europe-west4) is a set of zones with low-latency interconnect between them. A zone (for example europe-west4-b) is the unit of failure isolation; one zone failure should not take down a regional service. Multi-regional and dual-region resources span multiple regions.
Latency between resources falls into four bands. Same-zone calls are sub-millisecond. Cross-zone within a region are single-digit milliseconds. Cross-region within a continent are tens of milliseconds. Inter-continental are well over 100 milliseconds. A common architectural error is co-locating compute in one region and the database in another; the per-request round-trip dominates response time before any other tuning.
Co-location rules of thumb:
- Pin Cloud Run, GKE node pools, Cloud SQL, and Memorystore to the same region for synchronous request paths.
- Use multi-regional Cloud Storage buckets only when the read pattern is genuinely global; otherwise a regional bucket in the compute region is cheaper and lower-latency for reads.
- For globally-distributed APIs, front them with a global HTTPS Application Load Balancer that anycasts requests to the nearest healthy backend.
📊 Limit: Inter-region egress charges accumulate per gigabyte; an architecture that ships every request payload across regions inflates the bill independently of latency. Verify the published egress price for your traffic shape before committing to a multi-region read path.
Defining the use case and use of load balancers
Google Cloud load balancers split along three axes: scope (global vs regional), protocol (Application, Network passthrough, Network proxy), and visibility (external vs internal). The combinations the exam expects you to recognize:
- Global external Application Load Balancer: anycast HTTPS for multi-region backends, supports Cloud CDN, Cloud Armor, IAP, gRPC, and WebSocket. Use for public web and API traffic that requires geographic distribution.
- Regional external Application Load Balancer: HTTPS within a single region, regional Cloud Armor, lower complexity. Use when traffic is regional and you do not need cross-region failover.
- Internal Application Load Balancer: HTTPS for east-west traffic inside a VPC; supports both regional and cross-region variants.
- External and internal passthrough Network Load Balancer: Layer 4 TCP/UDP. Internal passthrough is the canonical front door for stateful protocols inside a VPC.
- External and internal proxy Network Load Balancer: Layer 4 with proxy capabilities (TLS termination at the edge, SSL offload).
# Provision a global external Application Load Balancer in front of a Cloud Run service
gcloud compute backend-services create orders-backend \
--global \
--load-balancing-scheme=EXTERNAL_MANAGED \
--protocol=HTTPS
gcloud compute network-endpoint-groups create orders-neg \
--region=us-central1 \
--network-endpoint-type=serverless \
--cloud-run-service=orders-api
gcloud compute backend-services add-backend orders-backend \
--global \
--network-endpoint-group=orders-neg \
--network-endpoint-group-region=us-central1
💡 Exam Trap: Cloud Run services receive a public
*.run.appURL by default. Putting an external Application Load Balancer in front lets you attach Cloud Armor policies, Identity-Aware Proxy, and a custom domain. Without the load balancer, IAP and Cloud Armor are not in the request path.
Using session affinity with load balancers based on application requirements for performant content delivery
Session affinity routes successive requests from the same client to the same backend instance. Application Load Balancers support multiple modes: NONE, CLIENT_IP, CLIENT_IP_PORT_PROTO, GENERATED_COOKIE, HEADER_FIELD, HTTP_COOKIE. Each mode has trade-offs.
- CLIENT_IP affinity works for IPv4 single-NAT clients but breaks when clients sit behind shared NAT; many clients then collapse onto one backend.
- GENERATED_COOKIE places a cookie issued by the load balancer; durable across NAT boundaries, but breaks for non-browser clients (mobile native, gRPC).
- HTTP_COOKIE lets the application set the affinity cookie name and value.
Session affinity is a performance tool when paired with in-instance caching (warm caches per user). It is an availability anti-pattern when treated as a substitute for shared session storage; if an affined backend goes down the user's session disappears. The remedy is to back affinity with Memorystore-stored session state and treat affinity as a cache-hit accelerator only.
⚠️ Anti-Pattern: Using CLIENT_IP affinity for a public mobile API. Carrier NATs collapse hundreds of thousands of devices behind a few addresses; affinity then concentrates load on a few backends and starves the rest. Choose GENERATED_COOKIE or move sessions to Memorystore entirely.
Using caching strategies (Memorystore for Redis, Valkey, Memcached)
Memorystore is Google Cloud's managed in-memory cache. It offers three engine choices: Memorystore for Redis (the original, OSS Redis compatible up through the supported version), Memorystore for Valkey (the open-source Redis fork preserved under the Linux Foundation), and Memorystore for Memcached (the simpler key-value cache with no persistence).
The decision among them follows feature need:
- Memcached for very large flat key-value caches where you do not need persistence, replication, or data structures. It scales horizontally by adding nodes; clients shard via consistent hashing.
- Redis or Valkey for caches that need lists, sorted sets, hashes, pub/sub, Lua scripting, or persistence (RDB snapshots, AOF). Pick Valkey when license sensitivity matters (open-source BSD-3); pick Redis for parity with existing Redis clients.
# Application-side caching with Memorystore for Redis (Standard Tier, read replicas)
import os
import redis
client = redis.Redis(
host=os.environ["REDIS_HOST"],
port=6379,
decode_responses=True,
ssl=True, # in-transit encryption available on Redis 6+
)
def get_order(order_id: str) -> dict:
cache_key = f"order:{order_id}"
cached = client.get(cache_key)
if cached:
return json.loads(cached)
record = fetch_from_database(order_id)
client.setex(cache_key, 300, json.dumps(record)) # 5-minute TTL
return record
Memorystore instances are regional. They cannot be reached across VPC peers without explicit Private Service Access configuration; this is the most common reason a Cloud Run service cannot connect to a Memorystore instance during testing.
📊 Limit: Memorystore for Redis Standard Tier replicates synchronously to a single replica in another zone within the region for HA. Multi-region replication is not supported; achieve cross-region resilience by deploying parallel instances behind a regional cache-aside pattern.
Creating and deploying secure APIs (HTTP REST, gRPC)
REST APIs are the default for browser clients and third-party integrations: HTTP/1.1 or HTTP/2, JSON payloads, OpenAPI as the contract surface. gRPC uses HTTP/2 and Protobuf, which compresses payloads and enables typed streaming. gRPC suits internal service-to-service traffic where both ends are owned, where typed contracts and streaming matter, and where payloads are small but high-frequency.
Cloud Run accepts both, but gRPC requires explicit HTTP/2 enabling: --use-http2 at deploy time and an Application Load Balancer configured with HTTP/2 to the backend. gRPC-Web bridges browser clients (which cannot speak raw gRPC) to gRPC backends through Envoy or Apigee.
# Deploy a gRPC service to Cloud Run with HTTP/2 end-to-end
gcloud run deploy inventory-grpc \
--image=us-central1-docker.pkg.dev/inventory-prod/api/inventory:v2 \
--region=us-central1 \
--use-http2 \
--port=50051 \
--no-allow-unauthenticated
Security on REST and gRPC APIs combines transport (TLS 1.2 or 1.3 to the load balancer; mTLS service-to-service via Cloud Service Mesh), authentication (OAuth 2.0 with Google Identity, JWT with custom issuers, API keys for low-trust scenarios), and authorization (IAM bindings for Google-identity callers, custom checks for end-user identity).
💡 Exam Trap: A scenario describing "browser client to gRPC backend" requires gRPC-Web or REST translation; native gRPC is unsupported by browsers. Apigee can do the gRPC-Web shim; Cloud API Gateway cannot terminate gRPC at all and works only for REST.
Application rate limiting, authentication, and observability for APIs using Apigee and Cloud API Gateway
Apigee is the enterprise API management platform: a developer portal, monetization, advanced policies (Spike Arrest, Quota, JavaCallout, XSL transform, JSON-to-XML), traffic shaping, fine-grained analytics, and a partner ecosystem. Apigee X runs as a managed service inside a Google-managed VPC peered to your project. Cloud API Gateway is the lightweight serverless gateway: an OpenAPI spec deployed in front of Cloud Run, Cloud Run functions, or App Engine, with API key and JWT validation, basic rate limiting, and Cloud Logging integration.
The decision: pick Cloud API Gateway when you need a thin façade with API keys and minimal policy; pick Apigee when you have multiple API products, partner developers, monetization, complex transformations, or strict SLAs requiring spike control.
# Cloud API Gateway: OpenAPI spec fronting a Cloud Run service
swagger: "2.0"
info:
title: orders-gateway
version: 1.0.0
host: orders.example.com
schemes: ["https"]
securityDefinitions:
api_key:
type: apiKey
name: x-api-key
in: header
paths:
/orders:
get:
operationId: listOrders
security: [{ api_key: [] }]
x-google-backend:
address: https://orders-api-xyz.a.run.app
responses:
"200":
description: OK
<!-- Apigee Spike Arrest policy: 100 requests per minute, smoothed -->
<SpikeArrest async="false" continueOnError="false" enabled="true" name="SA-100pm">
<Identifier ref="request.header.x-api-key"/>
<Rate>100pm</Rate>
</SpikeArrest>
💡 Exam Trap: Apigee Quota and Spike Arrest do different things. Quota enforces a long-window count (1000 requests per hour); Spike Arrest enforces a short-window rate (100 per minute, smoothed across a one-second window). The exam likes to test choosing one when the answer is the other.
Integration with asynchronous and event-driven services (Eventarc, Pub/Sub)
Pub/Sub is the global publish-subscribe service: a topic accepts messages from publishers, subscriptions deliver to consumers either via push (HTTP POST to a URL) or pull (consumer polls). Topics and subscriptions are globally available; messages persist for up to seven days by default, or 31 days with extended retention.
Eventarc is the event router. It accepts events from Google Cloud sources (Cloud Storage object create, Audit Log filters across many services, Cloud Pub/Sub, Firestore changes) in CloudEvents format and routes them to Cloud Run, GKE workloads (via Eventarc on GKE), or Workflows. Behind the scenes, Eventarc uses Pub/Sub for transport; the value Eventarc adds is the typed source filter, the CloudEvents envelope, and the IAM-bound subscription per destination.
# Pub/Sub topic and Cloud Run push subscription
gcloud pubsub topics create order-events
gcloud pubsub subscriptions create order-events-sub \
--topic=order-events \
--push-endpoint=https://orders-worker-xyz.a.run.app/ \
--push-auth-service-account=pubsub-pusher@orders-prod.iam.gserviceaccount.com \
--ack-deadline=60
# Eventarc trigger from Cloud Storage object finalize to a Cloud Run service
gcloud eventarc triggers create on-upload \
--location=us-central1 \
--destination-run-service=image-thumbnailer \
--destination-run-region=us-central1 \
--event-filters="type=google.cloud.storage.object.v1.finalized" \
--event-filters="bucket=uploads-orders-prod" \
--service-account=eventarc-trigger@orders-prod.iam.gserviceaccount.com
🎯 Scenario: A team needs to thumbnail every image uploaded to a Cloud Storage bucket. Two options surface: a Pub/Sub notification on the bucket plus a push subscription, or an Eventarc trigger directly on the object-finalized event. Eventarc is the lower-friction answer: it provides the CloudEvents envelope, the typed filter, and IAM binding on a single resource. The Pub/Sub option works but requires manually wiring bucket notifications and CloudEvents conversion in the receiver.
Defining application resource requirements
Right-sizing requires three data points: peak concurrent requests, per-request CPU and memory cost, and steady-state baseline. On Cloud Run, the formula is instances = ceil(peak_qps * latency_seconds / concurrency). A service that hits 200 QPS with 150 ms p95 latency and concurrency=80 needs ceil(200 * 0.15 / 80) = 1 instance baseline but more for headroom. On GKE, pod CPU and memory requests must match steady-state usage; over-requesting wastes node capacity, under-requesting causes OOMKills.
# GKE Deployment with right-sized requests and limits
apiVersion: apps/v1
kind: Deployment
metadata:
name: orders-api
spec:
replicas: 3
template:
spec:
containers:
- name: orders
image: us-central1-docker.pkg.dev/orders-prod/api/orders:v3
resources:
requests:
cpu: "500m"
memory: "512Mi"
limits:
cpu: "1000m"
memory: "1Gi"
Cost and resource optimization
Cost optimization on Google Cloud combines committed use discounts (CUDs) for stable baseline, Spot VMs for restart-tolerant workloads, autoscaling on the right signal, and right-sized regions. Cloud Run charges per CPU-second and memory-GB-second of allocated time; setting --cpu-throttling (the default) charges only during request handling and zero during idle, while --no-cpu-throttling keeps CPU allocated continuously (needed for background work or for low-latency cold-start mitigation paired with min-instances).
💡 Exam Trap: Cold start mitigation on Cloud Run uses
--min-instances=N, not--no-cpu-throttling.--no-cpu-throttlingkeeps CPU allocated to the running instance; it does not prevent the platform from scaling to zero when no instances exist.
Data replication options between zones and regions for failover
Replication choice depends on the database and the RPO/RTO target. Cloud SQL HA mode adds a synchronous standby in a different zone within the region (RPO=0, RTO measured in seconds). Cloud SQL read replicas, which can be cross-region, replicate asynchronously (RPO of seconds to minutes). Spanner replicates synchronously across regions in multi-regional configurations (RPO=0 globally). AlloyDB replicates synchronously within the cluster's primary region and supports cross-region replicas asynchronously. Bigtable supports multi-cluster replication with eventual consistency.
Cloud Storage offers regional, dual-region, and multi-region buckets. Multi-region keeps data available across continents but with eventual cross-region consistency for some operations; dual-region offers turbo replication SLA for tight RPO across two named regions.
Application traffic splitting (gradual rollouts, rollbacks, A/B testing on Cloud Run or GKE)
Cloud Run traffic splitting is first-class. Every gcloud run deploy creates a new immutable revision; you control which revisions receive traffic and in what proportion.
# Canary: send 90% to current production, 10% to new revision
gcloud run services update-traffic orders-api \
--to-revisions=orders-api-00007-abc=90,orders-api-00008-def=10 \
--region=us-central1
# Roll back: send 100% to the previous revision
gcloud run services update-traffic orders-api \
--to-revisions=orders-api-00007-abc=100 \
--region=us-central1
# Tag a revision for testing without sending production traffic
gcloud run services update-traffic orders-api \
--set-tags=staging=orders-api-00008-def \
--region=us-central1
On GKE, traffic splitting uses progressive deployment strategies. Native rolling updates roll new pods in over the existing pods; the surge and maxUnavailable settings control velocity. For canary, Cloud Deploy with the canary strategy targets GKE through a service mesh (Cloud Service Mesh or Gateway API HTTPRoute) that splits traffic between two backend Deployments by percentage.
⚠️ Anti-Pattern: Rolling forward with
gcloud run deploy --no-trafficand forgetting to invokeupdate-trafficto promote. The deployment "succeeds" but production sees nothing. The cure is to either promote in the same command (--tag=canary --no-trafficfollowed by a deliberate split) or set up Cloud Deploy with explicit promotion gates.
Orchestrating application services (Workflows, Eventarc, Cloud Tasks, Cloud Scheduler)
The orchestration surface has four services with distinct jobs.
- Cloud Scheduler is the cron service. It triggers HTTP endpoints, Pub/Sub topics, or App Engine handlers on a schedule. Use for periodic batch jobs, scheduled cache warmups, and nightly reconciliation.
- Cloud Tasks is the retryable single-target queue. A producer enqueues a task targeting one HTTP URL; Cloud Tasks delivers it with configurable retry policy, rate limit (max dispatches per second), and scheduling. Use for fan-in workloads where each task is one HTTP call to one endpoint and you need rate control plus exponential backoff.
- Pub/Sub is fan-out. One message to a topic delivers to many subscriptions, each with its own consumer. Use for broadcast events and decoupled consumption.
- Workflows is the declarative orchestrator. A workflow YAML defines steps, branches, parallel calls, retries, and state passed between calls. Use for multi-step business processes where the steps are heterogeneous service calls and you need observability over the whole flow.
# Workflows definition: validate, charge, ship - with retry and parallel notify
main:
params: [order]
steps:
- validateOrder:
call: http.post
args:
url: https://orders-api-xyz.a.run.app/validate
auth: { type: OIDC }
body: ${order}
result: validated
- chargeCard:
try:
call: http.post
args:
url: https://payments-xyz.a.run.app/charge
auth: { type: OIDC }
body: ${validated.body}
result: charge
retry:
predicate: ${http.default_retry_predicate}
max_retries: 3
backoff:
initial_delay: 1
max_delay: 30
multiplier: 2
- shipAndNotify:
parallel:
branches:
- shipBranch:
steps:
- createShipment:
call: http.post
args:
url: https://shipping-xyz.a.run.app/create
auth: { type: OIDC }
body: ${charge.body}
- notifyBranch:
steps:
- sendEmail:
call: http.post
args:
url: https://notify-xyz.a.run.app/email
auth: { type: OIDC }
body: ${charge.body}
Decision Anchor: orchestration choice
Choose Cloud Scheduler when the trigger is a clock. Choose Cloud Tasks when you need a retryable, rate-limited queue to one HTTP target. Choose Pub/Sub when one event must reach many consumers in parallel. Choose Workflows when steps are heterogeneous, conditional, and benefit from a stateful orchestrator. Choose Eventarc when you want typed Google Cloud source events routed to compute without writing Pub/Sub plumbing.
Component decision flow
💡 Exam Trap: Cloud Tasks and Pub/Sub both queue messages, but Cloud Tasks targets exactly one HTTP receiver per task and offers per-task scheduling and rate caps. A scenario that needs "deliver to one URL with retries and a 100 RPS cap" points at Cloud Tasks. A scenario that needs "broadcast to many consumers" points at Pub/Sub.
