Google Cloud Platform

Google Cloud Platform

Self-hosting on Google Cloud Platform

To self-host Membrane on Google Cloud Platform, you need to set up Google Cloud Storage.

Everything else is the same as in the main guide.

Setting up Google Cloud Storage

This guide uses terraform to manage the infrastructure.

Create a service account:

# Create a Google Service Account for the API service
resource "google_service_account" "api_service" {
  account_id   = "${var.project}-${var.environment}-api"
  display_name = "API Service Account for ${var.environment}"
  project      = var.gcp_project_id
}

# Grant necessary permissions to the service account
resource "google_project_iam_member" "api_storage_admin" {
  project = var.gcp_project_id
  role    = "roles/storage.admin"
  member  = "serviceAccount:${google_service_account.api_service.email}"
}

# Grant token creator permission for storage signed URLs
resource "google_project_iam_member" "api_token_creator" {
  project = var.gcp_project_id
  role    = "roles/iam.serviceAccountTokenCreator"
  member  = "serviceAccount:${google_service_account.api_service.email}"
}

# Allow the Kubernetes service account to impersonate the Google service account
resource "google_service_account_iam_binding" "api_workload_identity" {
  service_account_id = google_service_account.api_service.name
  role               = "roles/iam.workloadIdentityUser"

  members = [
    "serviceAccount:${var.gcp_project_id}.svc.id.goog[${var.kubernetes_namespace}/integration-app]"
  ]
}
  1. Create your buckets
# Temporary files bucket
resource "google_storage_bucket" "tmp" {
  name     = "${var.project}-${var.environment}-tmp"
  location = var.gcp_region
  project  = var.gcp_project_id

  # Force destroy even if bucket contains objects
  force_destroy = true

  # Uniform bucket-level access
  uniform_bucket_level_access = true

  # Lifecycle rule to delete objects after 7 days
  lifecycle_rule {
    condition {
      age = 7
    }
    action {
      type = "Delete"
    }
  }

  # Versioning
  versioning {
    enabled = false
  }

  # Labels
  labels = local.common_labels
}

# Connectors bucket
resource "google_storage_bucket" "connectors" {
  name     = "${var.project}-${var.environment}-connectors"
  location = var.gcp_region
  project  = var.gcp_project_id

  # Prevent accidental deletion
  force_destroy = false

  # Uniform bucket-level access
  uniform_bucket_level_access = true

  # Versioning for code safety
  versioning {
    enabled = true
  }

  # Lifecycle rule to delete old versions after 30 days
  lifecycle_rule {
    condition {
      num_newer_versions = 3
      with_state         = "ARCHIVED"
    }
    action {
      type = "Delete"
    }
  }

  # Labels
  labels = local.common_labels
}

# Static content bucket
resource "google_storage_bucket" "static" {
  name     = "${var.project}-${var.environment}-static"
  location = var.gcp_region
  project  = var.gcp_project_id

  # Force destroy for development environments
  force_destroy = var.environment != "prod"

  # Uniform bucket-level access
  uniform_bucket_level_access = true

  # Website configuration
  website {
    main_page_suffix = "index.html"
    not_found_page   = "404.html"
  }

  # CORS configuration
  cors {
    origin          = ["*"]
    method          = ["GET", "HEAD", "OPTIONS"]
    response_header = ["*"]
    max_age_seconds = 3600
  }

  # Versioning
  versioning {
    enabled = false
  }

  # Labels
  labels = local.common_labels
}

# Make static bucket publicly accessible for CDN (Optional)
resource "google_storage_bucket_iam_member" "static_public_access" {
  bucket = google_storage_bucket.static.name
  role   = "roles/storage.objectViewer"
  member = "allUsers"
}
  1. Configure CDN for serving static files (Optional)
 # Reserve a global static IP for the load balancer
resource "google_compute_global_address" "cdn_ip" {
  count = var.enable_cdn ? 1 : 0

  name    = "${var.project}-${var.environment}-cdn-ip"
  project = var.gcp_project_id
}

# Backend bucket for serving static content
resource "google_compute_backend_bucket" "static_backend" {
  count = var.enable_cdn ? 1 : 0

  name        = "${var.project}-${var.environment}-static-backend"
  description = "Backend bucket for static content"
  bucket_name = google_storage_bucket.static.name
  project     = var.gcp_project_id

  # Enable CDN
  enable_cdn = true

  # CDN policy
  cdn_policy {
    cache_mode = "CACHE_ALL_STATIC"

    # Cache TTLs
    client_ttl  = 3600
    default_ttl = 3600
    max_ttl     = 86400

    # Negative caching
    negative_caching = true
    negative_caching_policy {
      code = 404
      ttl  = 120
    }

    # Cache key policy
    cache_key_policy {
      query_string_whitelist = []
    }
  }

  # Compression
  compression_mode = "AUTOMATIC"
}

# URL map for routing
resource "google_compute_url_map" "cdn_url_map" {
  count = var.enable_cdn ? 1 : 0

  name            = "${var.project}-${var.environment}-cdn-url-map"
  default_service = google_compute_backend_bucket.static_backend[0].id
  project         = var.gcp_project_id

  # Host rules for custom domains
  host_rule {
    hosts        = ["static.${var.domain_name}"]
    path_matcher = "static-paths"
  }

  # Path matchers
  path_matcher {
    name            = "static-paths"
    default_service = google_compute_backend_bucket.static_backend[0].id

    path_rule {
      paths   = ["/api/*"]
      service = google_compute_backend_bucket.static_backend[0].id
      route_action {
        url_rewrite {
          path_prefix_rewrite = "/"
        }
      }
    }
  }
}

# Managed SSL certificate
resource "google_compute_managed_ssl_certificate" "cdn_cert" {
  count = var.enable_cdn ? 1 : 0

  name    = "${var.project}-${var.environment}-cdn-cert"
  project = var.gcp_project_id

  managed {
    domains = ["static.${var.domain_name}"]
  }
}

# HTTPS proxy
resource "google_compute_target_https_proxy" "cdn_https_proxy" {
  count = var.enable_cdn ? 1 : 0

  name             = "${var.project}-${var.environment}-cdn-https-proxy"
  url_map          = google_compute_url_map.cdn_url_map[0].id
  ssl_certificates = [google_compute_managed_ssl_certificate.cdn_cert[0].id]
  project          = var.gcp_project_id
}

# HTTP proxy (for redirect to HTTPS)
resource "google_compute_target_http_proxy" "cdn_http_proxy" {
  count = var.enable_cdn ? 1 : 0

  name    = "${var.project}-${var.environment}-cdn-http-proxy"
  url_map = google_compute_url_map.cdn_http_redirect[0].id
  project = var.gcp_project_id
}

# URL map for HTTP to HTTPS redirect
resource "google_compute_url_map" "cdn_http_redirect" {
  count = var.enable_cdn ? 1 : 0

  name    = "${var.project}-${var.environment}-cdn-http-redirect"
  project = var.gcp_project_id

  default_url_redirect {
    https_redirect         = true
    redirect_response_code = "MOVED_PERMANENTLY_DEFAULT"
    strip_query            = false
  }
}

# Global forwarding rule for HTTPS
resource "google_compute_global_forwarding_rule" "cdn_https_forwarding" {
  count = var.enable_cdn ? 1 : 0

  name                  = "${var.project}-${var.environment}-cdn-https-forwarding"
  ip_protocol           = "TCP"
  load_balancing_scheme = "EXTERNAL_MANAGED"
  port_range            = "443"
  target                = google_compute_target_https_proxy.cdn_https_proxy[0].id
  ip_address            = google_compute_global_address.cdn_ip[0].id
  project               = var.gcp_project_id
}

# Global forwarding rule for HTTP
resource "google_compute_global_forwarding_rule" "cdn_http_forwarding" {
  count = var.enable_cdn ? 1 : 0

  name                  = "${var.project}-${var.environment}-cdn-http-forwarding"
  ip_protocol           = "TCP"
  load_balancing_scheme = "EXTERNAL_MANAGED"
  port_range            = "80"
  target                = google_compute_target_http_proxy.cdn_http_proxy[0].id
  ip_address            = google_compute_global_address.cdn_ip[0].id
  project               = var.gcp_project_id
}

# Cloud Armor security policy (optional, for DDoS protection)
resource "google_compute_security_policy" "cdn_security_policy" {
  count = var.enable_cdn && var.environment == "prod" ? 1 : 0

  name    = "${var.project}-${var.environment}-cdn-security-policy"
  project = var.gcp_project_id

  # Default rule
  rule {
    action   = "allow"
    priority = "2147483647"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["*"]
      }
    }
    description = "Default rule, allow all traffic"
  }

  # Rate limiting rule
  rule {
    action   = "rate_based_ban"
    priority = "1000"
    match {
      versioned_expr = "SRC_IPS_V1"
      config {
        src_ip_ranges = ["*"]
      }
    }
    rate_limit_options {
      conform_action = "allow"
      exceed_action  = "deny(429)"
      enforce_on_key = "IP"
      rate_limit_threshold {
        count        = 1000
        interval_sec = 60
      }
    }
    description = "Rate limit rule"
  }
}
  1. Configure custom domain DNS managed zone for BASE_STATIC_URI (Optional)
# Create DNS managed zone
resource "google_dns_managed_zone" "main" {
   name        = var.dns_zone_name
   dns_name    = "${var.domain_name}."
   description = "${var.project} ${var.environment} DNS zone"
   project     = var.gcp_project_id

   labels = local.common_labels

   dnssec_config {
      state = "on"
   }
}

# DNS record for static content CDN
resource "google_dns_record_set" "static_cdn" {
   count = var.enable_cdn ? 1 : 0

   name         = "static.${google_dns_managed_zone.main.dns_name}"
   managed_zone = google_dns_managed_zone.main.name
   type         = "A"
   ttl          = 300
   project      = var.gcp_project_id

   rrdatas = [google_compute_global_address.cdn_ip[0].address]
}

# TXT record for domain verification
resource "google_dns_record_set" "domain_verification" {
   name         = google_dns_managed_zone.main.dns_name
   managed_zone = google_dns_managed_zone.main.name
   type         = "TXT"
   ttl          = 300
   project      = var.gcp_project_id

   rrdatas = [
      "\"v=spf1 -all\"",
      "\"google-site-verification=${var.google_site_verification_code}\""
   ]
}

# A record for the root domain (optional)
resource "google_dns_record_set" "root" {
   count = var.enable_cdn ? 1 : 0

   name         = google_dns_managed_zone.main.dns_name
   managed_zone = google_dns_managed_zone.main.name
   type         = "A"
   ttl          = 300
   project      = var.gcp_project_id

   rrdatas = [google_compute_global_address.cdn_ip[0].address]
}

# CAA record for SSL certificate issuance
resource "google_dns_record_set" "caa" {
   name         = google_dns_managed_zone.main.dns_name
   managed_zone = google_dns_managed_zone.main.name
   type         = "CAA"
   ttl          = 300
   project      = var.gcp_project_id

   rrdatas = [
      "0 issue \"pki.goog\"",
      "0 issue \"letsencrypt.org\""
   ]
}