ESC
Type to search guides, tutorials, and reference documentation.

GCP Solutions Architecture

How Google Cloud is structured: the project-centred resource hierarchy, IAM inheritance, the global VPC model, and the data and serverless services that define the platform.

GCP Solutions Architecture is the practice of designing, building, and operating cloud-native systems on Google Cloud Platform, using its core services in coordinated ways to meet specific business and technical objectives. It matters when applications must scale reliably under variable load, integrate diverse data sources, reduce operational overhead, or achieve high availability and disaster recovery across regions. This page details the patterns, tools, and pitfalls of deploying and managing solutions on GCP, focusing on real-world decisions and workflows that engineers execute daily.

Designing for Scalability and Resilience

Solutions architecture on GCP begins with defining how components scale and recover from failure. The foundation is the use of Managed Instance Groups (MIGs) with Autoscaling Policies based on CPU utilization, request rate, or custom metrics. A typical MIG is configured via gcloud:

gcloud compute instance-groups managed create web-servers \
  --base-instance-name web \
  --size 3 \
  --template web-server-template \
  --zone us-central1-a \
  --autorestart \
  --health-check http-health-check

The health check is a critical component, defined separately:

gcloud compute http-health-checks create http-health-check \
  --port 80 \
  --check-interval 10 \
  --timeout 5 \
  --unhealthy-threshold 3 \
  --healthy-threshold 2 \
  --request-path /health

A misconfiguration common in production is using a http health check but not setting the request-path, leading to 503s during deployment. The health check fails silently when the target service returns a 200 on /, but the instance is actually down on /health.

Autoscaling policies are defined in a yaml file and applied with gcloud compute instance-groups managed set-autoscaling:


scaleBasedOn: [
  {
    metric: "CPU Utilization",
    target: 70,
    minNumReplicas: 2,
    maxNumReplicas: 20,
    coolDownPeriodSec: 300
  },
  {
    metric: "Custom Metric: Request Rate",
    target: 100,
    minNumReplicas: 5,
    maxNumReplicas: 50,
    coolDownPeriodSec: 600
  }
]

The policy is applied via:

gcloud compute instance-groups managed set-autoscaling web-servers \
  --zone us-central1-a \
  --policy-file autoscaling-policy.yaml

A subtle failure occurs when the custom metric is not being reported correctly. The metric field in the policy must exactly match the metric name as published via Stackdriver (now Cloud Monitoring). If the metric is named request_count in the code but requests in the policy, the autoscaler ignores it entirely—no error, no logging, just underused instances.

Building Event-Driven Workflows with Pub/Sub and Cloud Functions

Solutions that process data in real time rely on Cloud Pub/Sub and Cloud Functions to decouple producers and consumers. A typical workflow involves a data ingestion service publishing messages to a topic:

gcloud pubsub topics create user-signups

Each message is a JSON object:

{
  "userId": "u1024",
  "email": "[email protected]",
  "timestamp": "2023-04-15T12:00:00Z"
}

A Cloud Function is triggered on every message. The function is written in Node.js and deployed with gcloud:

gcloud functions deploy send-welcome-email \
  --runtime nodejs18 \
  --trigger-topic user-signups \
  --entry-point handleUserSignup \
  --source ./functions \
  --memory 256MB \
  --timeout 60s

The function code:

exports.handleUserSignup = (event, context) => {
  const data = Buffer.from(event.data, 'base64').toString('utf8');
  const payload = JSON.parse(data);

  console.log(`Processing user: ${payload.userId}`);

  // Simulate sending email
  const result = sendEmail(payload.email, 'Welcome!', 'Thank you for signing up.');
  if (!result.success) {
    console.error('Failed to send email:', result.error);
  }

  return { processed: true, userId: payload.userId };
};

A common failure is the event delivery delay due to insufficient maxInstances and minInstances. Cloud Functions scale from 0 to 1,000 instances per function, but default maxInstances is 100. When a burst of 5,000 signups arrives, the first 100 functions process messages, but the remaining 4,900 are queued in Pub/Sub, with no new instances spun up. The solution is to increase maxInstances explicitly:

gcloud functions update send-welcome-email \
  --max-instances 500

Another subtle issue arises when messages are processed out of order. Pub/Sub guarantees delivery, but not ordering. To enforce ordering, use message ordering keys:

{
  "userId": "u1024",
  "email": "[email protected]",
  "timestamp": "2023-04-15T12:00:00Z",
  "orderingKey": "u1024"
}

The orderingKey ensures that all messages for the same user are processed in sequence. However, the function must be written to handle the orderingKey field, and the push endpoint must be configured to expect it. If the function assumes all messages have userId but the orderingKey is not used, ordering is lost.

Storing and Querying Data at Scale with BigQuery and Cloud Spanner

For analytical workloads, BigQuery is the primary data warehouse. Data is ingested via bq CLI:

bq load --source_format=NEWLINE_DELIMITED_JSON \
  mydataset.user_events \
  gs://data-lake/events-20230415.json

A table is defined in a schema.json file:

[
  {
    "name": "event_id",
    "type": "STRING",
    "mode": "REQUIRED"
  },
  {
    "name": "user_id",
    "type": "STRING",
    "mode": "REQUIRED"
  },
  {
    "name": "timestamp",
    "type": "TIMESTAMP",
    "mode": "REQUIRED"
  },
  {
    "name": "event_type",
    "type": "STRING",
    "mode": "NULLABLE"
  }
]

The bq load command uses this schema to infer the table structure. A common mistake is forgetting to set timePartitioning on large tables:

bq mk \
  --time_partitioning_field timestamp \
  --time_partitioning_type DAY \
  mydataset.user_events

Without time partitioning, queries over large date ranges scan the entire table, leading to high cost and latency. The default is no partitioning, so it’s easy to miss.

For transactional workloads requiring strong consistency, Cloud Spanner is used. A database is created:

gcloud spanner databases create sales-db \
  --instance=sales-instance \
  --description="Sales transaction database"

A table is defined and loaded via SQL:

CREATE TABLE Users (
  UserId STRING(36) NOT NULL,
  Email STRING(255) NOT NULL,
  CreatedAt TIMESTAMP NOT NULL,
  LastLogin TIMESTAMP
) PRIMARY KEY (UserId);

INSERT INTO Users (UserId, Email, CreatedAt, LastLogin)
VALUES
  ('u1024', '[email protected]', '2023-04-15T12:00:00Z', '2023-04-15T12:00:00Z'),
  ('u1025', '[email protected]', '2023-04-15T12:00:00Z', '2023-04-15T12:00:00Z');

A sharp edge is transactional consistency across multiple databases. Spanner supports global transactions across multiple instances, but the application must explicitly begin a transaction and commit it:

BEGIN;
UPDATE Users SET LastLogin = NOW() WHERE UserId = 'u1024';
UPDATE Orders SET Status = 'Confirmed' WHERE UserId = 'u1024';
COMMIT;

If the application opens a connection but does not wrap writes in a BEGIN/COMMIT, the changes are not atomic across databases. The failure mode is partial updates: a user’s order is confirmed, but their last login is not updated, or vice versa.

Securing and Managing Access with IAM and Identity-Aware Proxy

Access to GCP resources is governed by IAM policies. A policy is applied to a project, a bucket, or a function:

gcloud projects add-iam-policy-binding my-project \
  --member="serviceAccount:[email protected]" \
  --role="roles/cloudrun.invoker" \
  --condition="expression: request.time >= '2023-04-15T00:00:00Z'"

The condition is a standard expression language, and the expression field is evaluated at runtime. A common failure is using request.time but not setting request.time in the condition, leading to policies that apply only to a specific time window.

For web applications, Identity-Aware Proxy (IAP) secures access to services. IAP is configured via gcloud:

gcloud iap web-backend-services create web-app-backend \
  --url-suffix=app.example.com \
  [email protected] \
  --client-id=1234567890.apps.googleusercontent.com

IAP protects a Cloud Run service, which is accessible via https://app.example.com. The proxy adds HTTP headers:

  • X-Goog-Authenticated-User-Email
  • X-Goog-Authenticated-User-Id
  • X-Goog-Authenticated-User-Name

These are used by the application to identify users and enforce access control. A subtle failure occurs when the X-Goog-Authenticated-User-Email header is not properly propagated through a load balancer or reverse proxy. The application receives the header, but the value is null or incorrect, leading to inconsistent user sessions.

Managing Secrets and Configuration

Secrets are managed using Secret Manager. A secret is created:

gcloud secrets create db-password \
  --replication-policy automatic \
  --labels=env=prod

A version is added:

gcloud secrets versions add db-password \
  --data-file=secrets/db-password.txt

The application accesses the secret via the gcloud CLI or the REST API:

curl -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  "https://secretmanager.googleapis.com/v1/projects/my-project/secrets/db-password/versions/latest"

The response includes a payload field with data as a base64-encoded string:

{
  "name": "projects/my-project/secrets/db-password/versions/latest",
  "payload": {
    "data": "cGFzc3dvcmQxMjM=",
    "dataContentType": "text/plain"
  }
}

The application decodes the data field and uses the password. A common mistake is not setting dataContentType, leading to text/plain being assumed, but the actual content being application/json—the application fails to parse the secret.

Ensuring Observability and Monitoring

All solutions rely on Cloud Monitoring and Cloud Logging. Logs are collected from VMs, Cloud Run, and custom services using fluentd or logback.

A log entry is structured:

{
  "timestamp": "2023-04-15T12:00:00Z",
  "severity": "INFO",
  "labels": {
    "instance_id": "i-12345",
    "service": "web-server"
  },
  "textPayload": "User u1024 logged in."
}

A metric is defined via gcloud:

gcloud monitoring metrics create \
  --type=custom.googleapis.com/user/login_duration \
  --description="Time to complete user login" \
  --unit=ms \
  --value-type=DOUBLE \
  --label-keys=userId,region \
  --metric-kind=CUMULATIVE

Alerting policies monitor these metrics:

gcloud monitoring alerting policies create \
  --display-name="High Login Latency" \
  --condition="metric=custom.googleapis.com/user/login_duration,threshold=1000,filter=metric.labels.region=\"us-central1\""

A failure mode is alerting on a metric that is not being reported. The alert fires, but the metric data is missing. The cause is often a mismatch between the metric field in the condition and the actual metric name, especially when the metric is created via a script but not validated.

Deploying and Managing Infrastructure as Code

Infrastructure is managed using Terraform, Deployment Manager, or Cloud Build. A typical main.tf file:

resource "google_compute_instance" "web" {
  name         = "web-server"
  machine_type = "e2-medium"
  zone         = "us-central1-a"

  boot_disk {
    source_image = "projects/debian-cloud/global/images/debian-11-bullseye-v20230401"
    size         = 20
  }

  network_interface {
    network = "default"
    access_config {
      nat_ip = "35.234.123.45"
    }
  }

  metadata = {
    startup-script = <<-EOT
      #!/bin/bash
      apt-get update
      apt-get install -y nginx
      systemctl enable nginx
    EOT
  }
}

The deployment is triggered via terraform apply, with state stored in a Cloud Storage bucket. A common issue is state drift: the terraform apply command modifies the infrastructure, but the state file is not updated or not versioned. When a new deployment runs, it applies changes, but the state is out of sync with reality.

The pipeline is often orchestrated via Cloud Build, using a cloudbuild.yaml file:

steps:
- name: 'gcr.io/cloud-builders/docker'
  args: ['build', '-t', 'gcr.io/my-project/web-app:latest', '.']
- name: 'gcr.io/cloud-builders/gcloud'
  args: [
    'run', 'deploy', 'web-app',
    '--image', 'gcr.io/my-project/web-app:latest',
    '--region', 'us-central1'
  ]
- name: 'gcr.io/cloud-builders/gcloud'
  args: [
    'compute', 'instance-groups', 'managed', 'set-autoscaling',
    'web-servers', '--zone', 'us-central1-a',
    '--policy-file', 'autoscaling-policy.yaml'
  ]
timeout: '1500s'

A sharp edge is failure during a build step, where the cloudbuild.yaml file is not properly structured. The steps field is a list, and each step must be a map. A common mistake is using step instead of steps, or writing args as a flat list instead of a list of strings. The build runs, but the gcloud command is not executed correctly.

This page was rewritten on 16 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.