Google Cloud Platform Explained: Core Services, Real-World Architecture & When to Use GCP
Every production application you've ever used — from a startup's API to a Fortune 500's data pipeline — runs on someone's computers. The question is whose, and at what cost. Running your own servers means upfront capital, a team to maintain them, and a very bad Monday when one fails at 2 AM. Cloud platforms exist to flip that model: you get world-class infrastructure on demand, billed like a utility, with Google's Site Reliability Engineers quietly keeping the lights on behind the scenes. Google Cloud Platform is Google's answer to that problem, and it's built on the same infrastructure that runs Search, Gmail, and YouTube — systems engineered to handle billions of requests a day.
The real problem GCP solves isn't just 'running code remotely.' It's the operational complexity that kills engineering teams: patching OS vulnerabilities, provisioning storage that scales automatically, routing traffic across continents, and debugging distributed systems. Before managed cloud services, teams burned enormous engineering hours on infrastructure that added zero value to their product. GCP packages that complexity into opinionated, composable services so your team can stay focused on the thing that actually matters — the software itself.
By the end of this article you'll be able to confidently map a real-world application's requirements to specific GCP services, understand the difference between GCP's compute tiers and when each is appropriate, deploy a containerized workload to Google Kubernetes Engine, and avoid the billing and security mistakes that catch new GCP users off guard. This isn't a tour of the UI — it's a mental model you'll actually use.
GCP's Mental Model: Projects, Regions, and the Resource Hierarchy
Before touching any GCP service, you need to understand how GCP organises everything. Get this wrong and you'll end up with sprawling costs, broken IAM permissions, and services that can't talk to each other.
GCP groups resources into a three-tier hierarchy: Organisation → Folders → Projects. A Project is the atomic unit — every resource (a VM, a bucket, a database) lives inside exactly one project. Billing, IAM permissions, and API enablement are all scoped to the project. This is intentional: it means a dev team can have a payments-service-dev project completely isolated from payments-service-prod, with different budgets, different access controls, and separate audit logs.
Regions and zones handle physical location. A Region is a geographic area (e.g., us-central1 in Iowa). Each region contains multiple Zones (us-central1-a, us-central1-b, etc.) — these are independent data centres within that region. The rule of thumb: deploy across at least two zones for high availability, across multiple regions only if latency to global users or data sovereignty requires it. Cross-region data transfer costs money, so don't do it by default.
Understanding this hierarchy is what separates developers who get surprised by a $4,000 bill from those who plan budgets accurately from day one.
#!/bin/bash # ----------------------------------------------------------- # GCP PROJECT SETUP SCRIPT # Run this once to initialise a new GCP project correctly. # Requires: gcloud CLI authenticated via `gcloud auth login` # ----------------------------------------------------------- # Define project configuration as variables — never hardcode these inline PROJECT_ID="payments-service-prod" # Must be globally unique across all GCP BILLING_ACCOUNT_ID="012345-ABCDEF-789GHI" # Found in GCP Console > Billing PRIMARY_REGION="us-central1" # Closest region to your main user base PRIMARY_ZONE="us-central1-a" # Default zone within that region # Step 1: Create the project # --set-as-default means subsequent gcloud commands target this project automatically gcloud projects create "${PROJECT_ID}" \ --name="Payments Service Production" \ --set-as-default echo "Project '${PROJECT_ID}' created." # Step 2: Link a billing account — without this, most services won't activate gcloud billing projects link "${PROJECT_ID}" \ --billing-account="${BILLING_ACCOUNT_ID}" echo "Billing account linked." # Step 3: Set the default region and zone so you don't have to repeat --region/--zone # on every command. This saves you from accidentally deploying to the wrong region. gcloud config set compute/region "${PRIMARY_REGION}" gcloud config set compute/zone "${PRIMARY_ZONE}" echo "Default region set to ${PRIMARY_REGION}, zone to ${PRIMARY_ZONE}." # Step 4: Enable only the APIs your project actually needs. # GCP disables most APIs by default — this is a security feature, not a bug. # Enabling unused APIs increases your attack surface for nothing. gcloud services enable \ compute.googleapis.com \ container.googleapis.com \ cloudsql.googleapis.com \ storage.googleapis.com echo "Core APIs enabled." echo "Project initialisation complete. Run 'gcloud config list' to verify."
Billing account linked.
Default region set to us-central1, zone to us-central1-a.
Operation "operations/acf.p2-1234567890-abcdef" finished successfully.
Core APIs enabled.
Project initialisation complete. Run 'gcloud config list' to verify.
GCP Compute Options: Choosing the Right Engine for Your Workload
GCP gives you five distinct ways to run code, and picking the wrong one is one of the most common — and expensive — mistakes teams make. They're not interchangeable; each is optimised for a specific shape of workload.
Compute Engine (GCE) is raw virtual machines. You control the OS, you manage patching, you configure networking. Use this when you're lifting-and-shifting an existing application that has specific OS dependencies, or when you need GPU access for ML training jobs. It's the most flexible and the most operational overhead.
Google Kubernetes Engine (GKE) is managed Kubernetes. GCP handles the control plane (the bit that schedules your containers) and you manage your node pools and workloads. This is the workhorse for microservices architectures — use it when you have multiple services that need independent scaling, resource isolation, and rolling deployments.
Cloud Run is serverless containers. You push a container image, GCP handles everything else — scaling from zero to thousands of instances, load balancing, HTTPS. No cluster to manage. Use this for stateless APIs and event-driven services where you want zero infrastructure management. It's phenomenally cost-efficient for variable traffic.
App Engine is the oldest PaaS on GCP — opinionated, language-specific runtimes. Mostly superseded by Cloud Run for new projects.
Cloud Functions is function-level serverless for event triggers. Use it for glue code: responding to a file upload, processing a Pub/Sub message, or running a webhook handler. Not suited for long-running or compute-heavy work.
# ----------------------------------------------------------- # CLOUD RUN SERVICE DEFINITION # Deploys a containerised payments API to Cloud Run. # Cloud Run auto-scales to zero when idle — you pay nothing # when your service isn't handling requests. # Deploy with: gcloud run services replace cloud_run_service.yaml # ----------------------------------------------------------- apiVersion: serving.knative.dev/v1 kind: Service metadata: name: payments-api namespace: "123456789" # Your GCP Project Number (not Project ID) annotations: # Force all traffic through HTTPS — never allow plain HTTP in production run.googleapis.com/ingress: all spec: template: metadata: annotations: # Scale down to zero instances when there are no requests # This is what makes Cloud Run cost-effective for variable traffic autoscaling.knative.dev/minScale: "0" # Cap at 10 instances to prevent runaway costs during a traffic spike autoscaling.knative.dev/maxScale: "10" # Each instance handles max 80 concurrent requests before a new one spins up run.googleapis.com/execution-environment: gen2 spec: # How long Cloud Run waits for a response before treating it as a timeout timeoutSeconds: 30 # CPU and memory are per-instance limits containers: - image: gcr.io/payments-service-prod/payments-api:v2.1.0 ports: - containerPort: 8080 # Cloud Run always routes traffic to port 8080 resources: limits: cpu: "1" # 1 vCPU per instance memory: "512Mi" # 512MB RAM — right-size this based on profiling env: # Never hardcode secrets. Reference Secret Manager instead. - name: DB_PASSWORD valueFrom: secretKeyRef: key: latest name: payments-db-password # Name of secret in Secret Manager traffic: # 100% of traffic goes to the latest revision # You can split traffic here for canary deployments (e.g., 90/10) - latestRevision: true percent: 100
Setting IAM policy
Done.
Service [payments-api] revision [payments-api-00002-xyz] has been deployed and is serving 100 percent of traffic.
Service URL: https://payments-api-abcdef-uc.a.run.app
Storage on GCP: Matching the Data Shape to the Right Service
Nothing reveals a GCP beginner faster than seeing them store relational data in Cloud Storage or put time-series metrics into Cloud SQL. GCP has six distinct storage services and each one is engineered for a specific data access pattern. Using the wrong one doesn't just waste money — it actively degrades performance.
Cloud Storage (GCS) is object storage — think S3. Binary blobs, static assets, backups, data lake files. Infinitely scalable, globally accessible, extremely cheap. Access pattern: write once, read many, no updates to individual fields.
Cloud SQL is managed relational databases — PostgreSQL, MySQL, or SQL Server. Handles backups, failover, and patching. Use it when you have structured data with relationships and your team already thinks in SQL. Scales vertically (bigger machine) with read replicas for horizontal read scaling.
Cloud Spanner is the exotic one — globally distributed, horizontally scalable relational database with ACID transactions. It's what powers Google's own financial systems. Use it when Cloud SQL's 96TB limit isn't enough or when you need active-active multi-region writes. The price point reflects its power — about 20x Cloud SQL.
Firestore is a serverless NoSQL document database, optimised for mobile and web clients with real-time sync built in. Excellent for user profiles, session data, and content that's hierarchical and document-shaped.
Bigtable is a managed wide-column NoSQL store, designed for petabyte-scale time-series, IoT, and financial data with millisecond latency at massive scale. Not a general-purpose database.
Memorystore is managed Redis or Memcached — in-memory caching layer for your hot data.
# ----------------------------------------------------------- # GCS OBJECT UPLOAD + SIGNED URL GENERATION # Real-world pattern: a user uploads a profile photo. # We store it privately in GCS, then generate a short-lived # signed URL so the frontend can display it without making # the bucket publicly readable (a major security mistake). # # Install dependencies: pip install google-cloud-storage # Auth: set GOOGLE_APPLICATION_CREDENTIALS env var to your # service account key JSON path, or use Workload Identity. # ----------------------------------------------------------- import datetime from pathlib import Path from google.cloud import storage GCP_PROJECT_ID = "payments-service-prod" PRIVATE_BUCKET_NAME = "user-profile-photos-prod" # This bucket is NOT public SIGNED_URL_EXPIRY_MINUTES = 15 # Short expiry — limits blast radius if URL leaks def upload_user_profile_photo( user_id: str, local_file_path: Path, content_type: str = "image/jpeg", ) -> str: """ Uploads a profile photo to GCS and returns a signed URL the frontend can use to display it for the next 15 minutes. Returns the signed URL string. """ storage_client = storage.Client(project=GCP_PROJECT_ID) bucket = storage_client.bucket(PRIVATE_BUCKET_NAME) # Build a deterministic object path — makes it easy to find later # and naturally organises objects by user without needing folders object_name = f"users/{user_id}/profile/avatar.jpg" blob = bucket.blob(object_name) # Set content type so browsers render it correctly, not download it blob.content_type = content_type # Upload the file — this overwrites any existing photo for this user blob.upload_from_filename(str(local_file_path)) print(f"Uploaded '{local_file_path}' to gs://{PRIVATE_BUCKET_NAME}/{object_name}") # Generate a V4 signed URL — time-limited, cryptographically signed # by our service account. The bucket stays private; only holders of # this URL can access the object, and only until it expires. signed_url = blob.generate_signed_url( version="v4", expiration=datetime.timedelta(minutes=SIGNED_URL_EXPIRY_MINUTES), method="GET", # Read-only access ) print(f"Signed URL (valid {SIGNED_URL_EXPIRY_MINUTES} mins): {signed_url[:80]}...") return signed_url if __name__ == "__main__": # Simulate uploading a photo for user ID 'usr_8821' sample_photo_path = Path("/tmp/avatar_upload.jpg") # In production this file comes from a multipart form upload sample_photo_path.write_bytes(b"<fake-jpeg-bytes-for-demo>") url = upload_user_profile_photo( user_id="usr_8821", local_file_path=sample_photo_path, ) print(f"\nFrontend should use this URL to render the avatar: {url[:60]}...")
Signed URL (valid 15 mins): https://storage.googleapis.com/user-profile-photos-prod/users/usr_88...
Frontend should use this URL to render the avatar: https://storage.googleapis.com/user-profile-photos...
GCP IAM and Networking: The Security Layer You Can't Skip
Here's the uncomfortable truth: most cloud security incidents aren't caused by sophisticated attacks. They're caused by over-permissioned service accounts, open firewall rules, and credentials hardcoded into source code. GCP's IAM and VPC model exist specifically to prevent this — but only if you use them intentionally.
IAM (Identity and Access Management) in GCP follows the principle of least privilege. Every service account, user, and group gets only the permissions it needs — nothing more. Roles are either predefined (like roles/storage.objectViewer) or custom. The most dangerous role is roles/editor on a project — it's temptingly broad and you'll see it everywhere in tutorials. Never use it in production.
Workload Identity is the right way for GKE workloads to authenticate to GCP APIs. Instead of downloading a service account key JSON file (a long-lived credential that can be stolen), Workload Identity binds a Kubernetes service account to a GCP service account. The credential is ephemeral and automatically rotated. If you're using key files in a Kubernetes cluster, stop — switch to Workload Identity.
VPC (Virtual Private Cloud) is your private network inside GCP. By default, GCP creates a 'default' VPC with permissive firewall rules. For anything production, create a custom VPC with explicit subnets per region, and firewall rules that deny all ingress by default and allow only what you specify. Use Private Google Access on subnets so VMs can reach GCP APIs without needing a public IP.
#!/bin/bash # ----------------------------------------------------------- # GCP IAM LEAST-PRIVILEGE SETUP # Creates a service account for a Cloud Run payments service # with ONLY the permissions it actually needs: # - Read secrets from Secret Manager # - Write to a specific Cloud Storage bucket # - Publish to a specific Pub/Sub topic # Nothing else. This limits blast radius if the service is compromised. # ----------------------------------------------------------- PROJECT_ID="payments-service-prod" SERVICE_NAME="payments-api" # Step 1: Create a dedicated service account for this service # One service account per service — never share service accounts SERVICE_ACCOUNT_EMAIL="${SERVICE_NAME}@${PROJECT_ID}.iam.gserviceaccount.com" gcloud iam service-accounts create "${SERVICE_NAME}" \ --project="${PROJECT_ID}" \ --display-name="Payments API Service Account" \ --description="Identity for the payments-api Cloud Run service. Least-privilege access only." echo "Service account created: ${SERVICE_ACCOUNT_EMAIL}" # Step 2: Grant permission to read secrets from Secret Manager # This is scoped to the PROJECT level — ideally scope it to individual secrets # using resource-level IAM for even finer control gcloud projects add-iam-policy-binding "${PROJECT_ID}" \ --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \ --role="roles/secretmanager.secretAccessor" echo "Secret Manager access granted." # Step 3: Grant permission to write objects to a specific bucket ONLY # Note: roles/storage.objectCreator is narrower than roles/storage.objectAdmin # objectCreator can write new objects but cannot delete or overwrite existing ones TARGET_BUCKET="gs://payments-receipts-prod" gcloud storage buckets add-iam-policy-binding "${TARGET_BUCKET}" \ --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \ --role="roles/storage.objectCreator" echo "Storage write access granted to ${TARGET_BUCKET} only." # Step 4: Grant Pub/Sub publish permission on one specific topic TARGET_TOPIC="projects/${PROJECT_ID}/topics/payment-completed-events" gcloud pubsub topics add-iam-policy-binding "payment-completed-events" \ --project="${PROJECT_ID}" \ --member="serviceAccount:${SERVICE_ACCOUNT_EMAIL}" \ --role="roles/pubsub.publisher" echo "Pub/Sub publish access granted to payment-completed-events topic." # Step 5: Attach this service account to the Cloud Run service # The service now authenticates as this SA automatically — no key files needed gcloud run services update "${SERVICE_NAME}" \ --project="${PROJECT_ID}" \ --region="us-central1" \ --service-account="${SERVICE_ACCOUNT_EMAIL}" echo "Service account attached to Cloud Run service." echo "IAM setup complete. This service account has NO other GCP permissions."
Secret Manager access granted.
Storage write access granted to gs://payments-receipts-prod only.
Pub/Sub publish access granted to payment-completed-events topic.
Service account attached to Cloud Run service.
IAM setup complete. This service account has NO other GCP permissions.
| Dimension | Compute Engine (GCE) | Google Kubernetes Engine (GKE) | Cloud Run |
|---|---|---|---|
| Abstraction Level | Raw VMs (IaaS) | Managed Kubernetes (CaaS) | Serverless containers (PaaS) |
| Ops Overhead | High — you manage OS, patching, scaling | Medium — GCP manages control plane, you manage node pools | Low — GCP manages everything except your container |
| Scaling Behaviour | Manual or MIG autoscaling (minutes) | Pod autoscaling via HPA (seconds) | Instant scale-to-zero and scale-out (sub-second) |
| Billing Unit | Per-second VM uptime | Per-second node uptime | Per-request CPU and memory (free at idle) |
| Best For | Legacy apps, GPU workloads, custom OS configs | Microservices, multi-container apps, stateful workloads | Stateless APIs, event-driven functions, variable traffic |
| Cold Start | None (always running) | None (always running) | Yes — 300ms to 3s depending on runtime |
| Max Request Timeout | N/A — not request-oriented | N/A — not request-oriented | 3600 seconds (1 hour) |
| Minimum Cost | ~$5-10/month for f1-micro | ~$70/month for smallest cluster | $0/month at zero traffic |
🎯 Key Takeaways
- GCP's Project is the atomic unit of isolation — billing, IAM, and APIs are all scoped per project. Use separate projects for dev/staging/prod, not separate folders within one project.
- The compute decision (GCE vs GKE vs Cloud Run) is really a decision about how much operational ownership you want — more control always means more operational overhead. Cloud Run is the default choice for new stateless services unless you have a specific reason not to use it.
- The right storage service is determined entirely by your data access pattern — Cloud Storage for blobs, Cloud SQL for relational data under 96TB, Spanner for globally distributed relational, Firestore for document-shaped hierarchical data. Mixing these up costs money and performance.
- IAM is not an afterthought — set up least-privilege service accounts before you deploy your first service. The cost of retrofitting permissions on a live system is far higher than getting it right during initial setup.
⚠ Common Mistakes to Avoid
- ✕Mistake 1: Enabling allUsers IAM on a GCS bucket containing user data — Symptom: all objects in the bucket are publicly readable on the internet, often discovered via a security scanner or, worse, a data breach report — Fix: Remove the allUsers binding immediately with
gcloud storage buckets remove-iam-policy-binding gs://BUCKET_NAME --member=allUsers --role=roles/storage.objectViewer, then audit which objects were accessed in Cloud Audit Logs, and switch to signed URLs for any access that requires temporary public readability. - ✕Mistake 2: Using a single service account with roles/editor for every service in a project — Symptom: if one service is compromised or a key file is leaked, an attacker gains near-full write access to your entire GCP project including secrets, databases, and compute — Fix: Create one service account per service, grant only the specific predefined roles required (e.g., roles/pubsub.publisher not roles/pubsub.admin), and use Workload Identity for GKE instead of key files entirely.
- ✕Mistake 3: Deploying all resources to a single zone without high-availability consideration — Symptom: a GCP zonal outage (they do happen — see the 2021 us-central1-b incident) takes down your entire application, violating your SLA — Fix: For Compute Engine, use Managed Instance Groups (MIGs) spread across multiple zones in the same region. For Cloud SQL, enable the High Availability option which provisions a standby instance in a different zone. For GKE, create node pools with nodes spread across zones using the
--num-nodes-per-zoneflag.
Interview Questions on This Topic
- QYou're building a payments microservice that needs to read from Cloud SQL and publish to Pub/Sub. Walk me through how you'd set up IAM for it in production — and specifically, what would you NOT do that junior engineers typically get wrong?
- QA product manager tells you traffic to your API is unpredictable — quiet for hours, then spikes to 10,000 requests per minute around lunchtime. Which GCP compute option would you choose and why? What are the trade-offs of your choice?
- QYour team is moving from a monolith to microservices on GCP. How do you decide between GKE and Cloud Run for each service? What signals in a service's requirements push you toward one versus the other?
Frequently Asked Questions
Is Google Cloud Platform better than AWS for beginners?
Neither is objectively better — they solve the same problems with different UX and pricing models. GCP tends to have a cleaner CLI (gcloud) and more opinionated managed services like Cloud Run and BigQuery that reduce setup time. AWS has a larger ecosystem and more third-party integrations. For net-new projects without existing cloud investments, GCP's Cloud Run and managed Kubernetes are genuinely excellent starting points, and GCP's free tier is generous enough to learn without a credit card charge.
What is the difference between a GCP Region and a Zone?
A Region is a geographic area containing multiple independent data centres, called Zones. For example, us-central1 is a region in Iowa containing zones us-central1-a, -b, -c, and -f. Zones are physically separate — different power, cooling, and network infrastructure — so a failure in one zone doesn't affect the others. Deploy across multiple zones for high availability within a region; deploy across multiple regions only when you need geographic redundancy or data sovereignty.
What is Workload Identity in GCP and why is it better than a service account key file?
Workload Identity lets a Kubernetes pod authenticate to GCP APIs by binding its Kubernetes service account to a GCP service account — no credential file involved. A service account key JSON file is a long-lived credential that can be accidentally committed to git, included in a Docker image, or exfiltrated from a compromised pod. Workload Identity credentials are short-lived, automatically rotated by GCP, and never written to disk, making them dramatically safer for production workloads.
Written and reviewed by senior developers with real-world experience across enterprise, startup and open-source projects. Every article on TheCodeForge is written to be clear, accurate and genuinely useful — not just SEO filler.