MyCertStack logoMyCertStack

    1: Designing highly scalable, secure, and reliable cloud-native applications

    Designing high-performing applications and APIs

    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.app URL 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-throttling keeps 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-traffic and forgetting to invoke update-traffic to promote. The deployment "succeeds" but production sees nothing. The cure is to either promote in the same command (--tag=canary --no-traffic followed 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.


    Continue with the interactive course

    Track your progress, jump into practice questions and use the Shark AI tutor inside the GCP - Professional Cloud Developer learning hub.