GOOGLE CLOUD → STACKIT · CASE STUDY

Cloud Run and Cloud SQL: a complete assessment walkthrough

One realistic estate, start to finish: a public Cloud Run API, a regional Cloud SQL database on a private network, a secret, a bucket and a registry. StackToIT read 13 resources, formed 10 migration units, drafted STACKIT Terraform for six of them and left 22 pieces of manual work, with 29 critical settings waiting for a decision. This page shows what that looks like.

The run in figures

Assessed by StackToIT on 4 November 2026, offline, on an estate written for this page. All numbers are from the run’s console output and assessment.json.

FigureValueMeaning
Resources and units13 resources, 10 unitsThree resources (the database, the secret binding and the invoker binding) are not units of their own; 12 dependencies link the units.
Classes1 CONTAINERIZE, 8 RECONFIGURE, 1 REPLATFORMThe service is containerized, the database changes platform, the rest is reconfigured.
Draft6 Terraform-only, 1 manual, 3 need no resourceSix units have a block in the STACKIT draft; the registry is set up by hand; the VPC network, the reserved private range and the private services connection need no block.
Waves4Foundations in wave 1, database last in wave 4. Units in a wave can move in parallel.
Open work22 manual pieces, 29 critical settings in 9 unitsEach is a person’s decision or step. None runs by itself.
Readiness0 of 7 automated steps can run as writtenThe package is a proposal; the example has no lock file for the Google provider and the run was not asked to validate with OpenTofu.

The run finished with 12 of 12 global invariants passing, three of which checked nothing for this estate.

The database: Cloud SQL to PostgreSQL Flex

Left: the regional PostgreSQL 16 instance of the input, with private IP and point-in-time recovery. Right: the blocks of the STACKIT Terraform the run wrote; the review comments above each block in the file are left out here.

Example inputmain.tf
resource "google_sql_database_instance" "orders" {
  name             = "orders-pg"
  region           = "europe-west3"
  database_version = "POSTGRES_16"
  depends_on       = [google_service_networking_connection.sql]

  settings {
    tier              = "db-custom-2-8192"
    availability_type = "REGIONAL"
    disk_size         = 100
    disk_type         = "PD_SSD"

    ip_configuration {
      ipv4_enabled    = false
      private_network = google_compute_network.shop.id
    }

    backup_configuration {
      enabled                        = true
      point_in_time_recovery_enabled = true
    }
  }
}

resource "google_sql_database" "orders" {
  name     = "orders"
  instance = google_sql_database_instance.orders.name
}
STACKIT Terraform by StackToITterraform/data.tf
resource "stackit_postgresflex_instance" "orders" {
  project_id = var.project_id
  name       = "orders-pg"
  version    = "16"
  flavor_id  = "2.16-replica"
  storage = {
    class = var.postgresflex_storage_class
    size  = 100
  }
  backup_schedule = "0 2 * * *"
  network = {
    acl = var.orders_allowed_cidrs
  }
  retention_days = 32
}

resource "stackit_postgresflex_database" "orders_orders" {
  project_id  = var.project_id
  instance_id = stackit_postgresflex_instance.orders.instance_id
  name        = "orders"
  owner       = var.orders_orders_owner
}
  • Version, storage and database carry over

    PostgreSQL 16, 100 GB and the database name are in the draft. The regional tier db-custom-2-8192 becomes flavor 2.16-replica, which the run's comment describes as 2 vCPU and 16 GiB: the smallest replica flavor in eu01 that covers 2 vCPU and 8 GB.

  • Private IP has no counterpart

    The instance is reached at the endpoint STACKIT assigns, limited by an ACL. The list of allowed addresses is a variable you fill.

  • Point-in-time recovery is not a setting

    STACKIT’s backup-and-clone how-to confirms it: restoring a backup, including to a point in time, creates a new instance in the same project, and there is no in-place restore. The input sets no retention, so Cloud SQL keeps its default of seven automated backups; the run records that as 7 days, and the draft sets retention_days = 32. The 02:00 daily backup schedule is a default the run says you should confirm.

The service: Cloud Run to a workload on SKE

Left: the Cloud Run service with its secret reference, bucket variable and VPC access. Right: the cluster the run drafted to host it as a Deployment.

Example inputmain.tf
resource "google_cloud_run_v2_service" "orders" {
  name     = "orders-api"
  location = "europe-west3"
  ingress  = "INGRESS_TRAFFIC_ALL"

  template {
    service_account = google_service_account.api.email

    scaling {
      min_instance_count = 1
      max_instance_count = 10
    }

    vpc_access {
      egress = "PRIVATE_RANGES_ONLY"
      network_interfaces {
        network    = google_compute_network.shop.id
        subnetwork = google_compute_subnetwork.apps.id
      }
    }

    containers {
      image = "europe-west3-docker.pkg.dev/shop-prod/images/orders-api:2.4.0"

      resources {
        limits = {
          cpu    = "1"
          memory = "512Mi"
        }
      }

      env {
        name  = "RECEIPTS_BUCKET"
        value = google_storage_bucket.receipts.name
      }

      env {
        name = "DB_PASSWORD"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.db_password.secret_id
            version = "latest"
          }
        }
      }
    }
  }
}
STACKIT Terraform by StackToITterraform/kubernetes.tf
resource "stackit_ske_cluster" "containers" {
  project_id = var.project_id
  name       = "containers"
  node_pools = [{
    name               = "workloads"
    machine_type       = var.containers_machine_type
    minimum            = var.containers_nodes_min
    maximum            = var.containers_nodes_max
    availability_zones = var.availability_zones
  }]
}
  • The image moves, the manifest is yours

    The run writes the cluster. The Deployment and Service manifests are not generated; name, environment, resources, service account and network settings are listed as not carried.

  • Public access is flagged

    The allUsers invoker binding raises a warning: anyone may call the service today, and your ingress on SKE must decide who may in future.

  • Scaling bounds go into your manifest

    The source keeps 1 to 10 instances. The cluster has no workload yet, so set replicas and an autoscaler (for example a HorizontalPodAutoscaler) yourself. The run also warns that scale-to-zero needs KEDA or Knative on SKE; with a minimum of one instance this service does not rely on it.

Every unit, its wave and its target

From migration/technical-sequencing.md. A unit moves only after the units it depends on.

Unit (Google Cloud)Wave and classSTACKIT target in the run
Artifact Registry repository1 · RECONFIGURESTACKIT Container Registry, set up by hand
VPC network1 · RECONFIGUREProject networking, with a Network Area where needed; no block in the draft
Service account1 · RECONFIGURESTACKIT service account
Storage bucket1 · RECONFIGURESTACKIT Object Storage bucket
Reserved private range, subnetwork, secret2 · RECONFIGURENo resource; STACKIT network 10.20.0.0/24; key in Secrets Manager
Cloud Run service3 · CONTAINERIZESKE workload, or a Cloud Foundry app from the image
Private services connection3 · RECONFIGURENot needed: managed services have their own endpoints and ACLs
Cloud SQL instance4 · REPLATFORMSTACKIT PostgreSQL Flex

The interesting decisions

Each comes from a warning or a not-carried line the run wrote next to the block.

  • How clients reach the database

    The source database has no public address. On STACKIT it has an endpoint with an ACL. Decide the allowed ranges, and whether the new workload reaches it through the cluster’s egress addresses.

  • Recovery and retention

    Point-in-time recovery restores into a new instance on PostgreSQL Flex, and the draft keeps backups 32 days, where the source relies on Cloud SQL’s default of seven automated backups. Check both against your recovery point objective and update the recovery runbook, including where clients connect after a restore.

  • Instance size

    The run picks 2.16-replica because the source is regional (a standby in a second zone). The run itself notes that no STACKIT page states the zone of each replica node or how a failover works, so confirm the flavor and the availability you need before you apply.

  • Protection against deletion

    Terraform’s deletion protection on the Cloud SQL instance guards only the Terraform workflow, and the draft does not add a counterpart. Add lifecycle { prevent_destroy = true } as the run advises; the PostgreSQL Flex documentation also recommends instance protection for critical instances.

  • Who may call the API

    The source allows anyone. Decide whether the new ingress stays public, sits behind a gateway or requires a token.

  • Secret and its readers

    The secret is recreated as a key by hand; the value is never copied into the draft, and the accessor binding is not carried.

  • Bucket versioning

    The source bucket is versioned. The run notes that STACKIT Object Storage supports versioning through the S3 API, but the bucket resource has no versioning attribute, so the draft block sets only name and project. Turn versioning on after the bucket exists; once on, it can only be suspended.

  • Data movement

    migration/data-movement.md lists the database and the bucket as decisions with no mechanism chosen, because the source states neither data size nor tolerated downtime. The unit reasons name pg_dump with pg_restore or logical replication for the database and rclone for the bucket.

Where to read each answer

index.html
The summary and the decision pages, offline
assessment.json
Every unit with class, target, reason and findings
terraform/
The STACKIT draft, one file per area
migration/technical-sequencing.md
The waves above
migration/data-movement.md
Database and bucket with the open data decisions
execution/manual/actions.md
The 16 work packages of manual steps, each with an owner
engineering/architect-report.md
Findings, validation matrix and what is not proven

Questions architects ask about this run

Is the STACKIT Terraform ready to apply?

No. The run calls it a draft that needs architect review, and none of the seven automated steps can run as written until decisions and values are in. The example also has no provider lock file and was not validated with OpenTofu.

Why does the database come last?

It depends on the network and on the private services connection, so the run puts it in wave 4. The order is a starting point for your wave plan, not a schedule.

Why is the Cloud Run service not Terraform?

The run drafts the SKE cluster that hosts it. The Deployment and Service manifests are not generated, so you write them from the not-carried list.

Does the run move any data?

No. StackToIT writes files and changes nothing in any cloud. The data steps are decisions for your team.

Sources

Your next move: STACKIT.

Tell us about your estate. We show you what StackToIT delivers for it: target architecture, Terraform artifacts and a structured migration plan.