GCP TERRAFORM → STACKIT TERRAFORM

GCP Terraform to STACKIT Terraform

StackToIT reads your Google Cloud Terraform the way your engineers wrote it, modules and variables included, and writes STACKIT provider code your platform team can review, version and apply through its own pipeline. The run on this page assessed Google Cloud's own public Jump Start Solution "Dynamic Python Web App" (37 resources in 18 files) unchanged, at one pinned commit: 11 migration units plus 5 decided at plan time, 11 of the 16 drafted as 10 STACKIT resource blocks that OpenTofu validation accepts. The complete run is published at stacktoit.com/examples/gcp-python-webapp/.

One real conversion: Cloud SQL to PostgreSQL Flex

The database of Google Cloud's Dynamic Python Web App, assessed unchanged with a one-line variable file (project_id = "example-project"). On the left the Cloud SQL instance exactly as Google publishes it, on the right the block exactly as StackToIT wrote it in the run. The comments the draft keeps above each block are left out here.

Google Cloud sourceinfra/database.tf
resource "google_sql_database_instance" "postgres" {
  name                = var.random_suffix ? "${var.instance_name}-${random_id.suffix.hex}" : var.instance_name
  database_version    = "POSTGRES_14"
  project             = var.project_id
  region              = var.region
  deletion_protection = false

  settings {
    tier        = "db-custom-2-4096" # 2 CPU, 4GB Memory
    user_labels = var.labels
  }
  depends_on = [module.project_services]

}
STACKIT Terraform by StackToITterraform/data.tf
resource "stackit_postgresflex_instance" "postgres" {
  project_id = var.project_id
  name       = "postgres"
  version    = "14"
  flavor_id  = "2.4"
  storage = {
    class = var.postgresflex_storage_class
    size  = var.postgres_storage_size
  }
  backup_schedule = "0 2 * * *"
  network = {
    acl = var.postgres_allowed_cidrs
  }
  retention_days = 32
}
  • Engine, version and size carried over

    POSTGRES_14 becomes a PostgreSQL Flex instance on version 14. The tier db-custom-2-4096 (2 CPU, 4 GB memory) becomes flavor 2.4, which is 2 vCPU and 4 GiB on a single node in the eu01 catalogue of 18 October 2026.

  • Backups and retention

    The source states no backup start time, so the draft uses a default of 02:00 daily (0 2 * * *) and says so. Its backup retention is not visible statically; retention_days is 32, the STACKIT minimum (32 to 90).

  • Values to decide become variables

    The source states no disk size, and a Cloud SQL disk grows by itself while a PostgreSQL Flex storage has a fixed size: postgres_storage_size is a variable (5 to 4000 GB) to set after reading the current disk. The storage class and the allowed CIDRs, which limit the instance endpoint by ACL, are variables too.

  • Every property accounted for

    Of the 12 meaningful properties on this Cloud SQL unit (instance, database and user), 6 are carried or explained and 6 are named as not carried: the labels, the user password, both names and both ABANDON deletion policies. None is left unaccounted. STACKIT generates the user password, and you create the user and the database with stackit_postgresflex_user and stackit_postgresflex_database.

Second conversion: Cloud Run service to STACKIT Kubernetes Engine

STACKIT has no Cloud Run, so the Django API service becomes a workload on STACKIT Kubernetes Engine. On the left the service exactly as Google publishes it. On the right the Kubernetes manifests StackToIT wrote for it; they run on the cluster stackit_ske_cluster.containers in terraform/kubernetes.tf, which also hosts the migration job.

Google Cloud sourceinfra/service.tf
resource "google_cloud_run_v2_service" "server" {
  name     = var.random_suffix ? "${var.service_name}-${random_id.suffix.hex}" : var.service_name
  location = var.region

  deletion_protection = false

  template {
    service_account = google_service_account.server.email
    containers {
      image = local.server_image
      env {
        name = "DJANGO_ENV"
        value_source {
          secret_key_ref {
            secret  = google_secret_manager_secret.django_settings.secret_id
            version = "latest"
          }
        }
      }
      env {
        name  = "PYTHONPATH"
        value = ""
      }
      env {
        name  = "DJANGO_SETTINGS_MODULE"
        value = "avocano_api.settings"
      }
      # These variables are a standard part of OpenTelemetry SDK configuration.
      # Details are available at
      # https://opentelemetry.io/docs/reference/specification/sdk-environment-variables/#exporter-selection
      env {
        name  = "OTEL_METRICS_EXPORTER"
        value = "none"
      }
      env {
        name  = "OTEL_TRACES_EXPORTER"
        value = "gcp_trace"
      }
      env {
        name  = "OTEL_PYTHON_EXCLUDED_URLS"
        value = "healthy"
      }
      # Supply the custom suffix, if used, to support automated CSRF code in application settings
      env {
        name  = "DEPLOYMENT_SUFFIX"
        value = var.random_suffix ? random_id.suffix.hex : ""
      }
      volume_mounts {
        name       = "cloudsql"
        mount_path = "/cloudsql"
      }
      liveness_probe {
        http_get {
          path = "/healthy"
        }
      }
    }
    labels = var.labels
    volumes {
      name = "cloudsql"
      cloud_sql_instance {
        instances = [google_sql_database_instance.postgres.connection_name]
      }
    }
  }
  traffic {
    percent = 100
    type    = "TRAFFIC_TARGET_ALLOCATION_TYPE_LATEST"
  }

  depends_on = [
    google_secret_manager_secret_version.django_settings
  ]

  lifecycle {
    ignore_changes = [
      scaling,
    ]
  }
}
Kubernetes manifests by StackToITterraform/kubernetes/server/manifests.yaml
apiVersion: v1
kind: ConfigMap
metadata:
  name: server-env
  labels:
    app.kubernetes.io/name: server
data:
  DEPLOYMENT_SUFFIX: "" # fill in: the value is not copied from the source
  DJANGO_SETTINGS_MODULE: "" # fill in: the value is not copied from the source
  OTEL_METRICS_EXPORTER: "" # fill in: the value is not copied from the source
  OTEL_PYTHON_EXCLUDED_URLS: "" # fill in: the value is not copied from the source
  OTEL_TRACES_EXPORTER: "" # fill in: the value is not copied from the source
  PYTHONPATH: "" # fill in: the value is not copied from the source
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: server
  labels:
    app.kubernetes.io/name: server
spec:
  replicas: REQUIRED_SERVER_REPLICAS
  selector:
    matchLabels:
      app.kubernetes.io/name: server
  template:
    metadata:
      labels:
        app.kubernetes.io/name: server
    spec:
      containers:
      - name: container-1
        image: "us-docker.pkg.dev/hsa-public/containers/terraform-python-dynamic-webapp/server:v1.13.4"
        ports:
        - containerPort: 8080
          protocol: TCP
        env:
        - name: PORT
          value: "8080"
        - name: DJANGO_ENV
          valueFrom:
            secretKeyRef:
              name: server-secrets
              key: DJANGO_ENV
        - name: PYTHONPATH
          valueFrom:
            configMapKeyRef:
              name: server-env
              key: PYTHONPATH
        - name: DJANGO_SETTINGS_MODULE
          valueFrom:
            configMapKeyRef:
              name: server-env
              key: DJANGO_SETTINGS_MODULE
        - name: OTEL_METRICS_EXPORTER
          valueFrom:
            configMapKeyRef:
              name: server-env
              key: OTEL_METRICS_EXPORTER
        - name: OTEL_TRACES_EXPORTER
          valueFrom:
            configMapKeyRef:
              name: server-env
              key: OTEL_TRACES_EXPORTER
        - name: OTEL_PYTHON_EXCLUDED_URLS
          valueFrom:
            configMapKeyRef:
              name: server-env
              key: OTEL_PYTHON_EXCLUDED_URLS
        - name: DEPLOYMENT_SUFFIX
          valueFrom:
            configMapKeyRef:
              name: server-env
              key: DEPLOYMENT_SUFFIX
        livenessProbe:
          httpGet:
            path: "/healthy"
            port: 8080
        readinessProbe:
          tcpSocket:
            port: 8080
---
apiVersion: v1
kind: Service
metadata:
  name: server
  labels:
    app.kubernetes.io/name: server
spec:
  type: ClusterIP
  selector:
    app.kubernetes.io/name: server
  ports:
  - name: p8080-tcp
    port: 8080
    targetPort: 8080
    protocol: TCP
  • Image, port and health check carried over

    The container image and its liveness path /healthy come straight from the service. The source states no port, so the draft uses 8080, the Cloud Run default, and adds a readiness check on that port.

  • Secret and environment split

    DJANGO_ENV comes from a Secret Manager secret and becomes a Kubernetes Secret reference, server-secrets, which you create. The five plain variables become the keys of the ConfigMap server-env; the draft does not copy their values, you fill them in.

  • One cluster for service and job

    The cluster stackit_ske_cluster.containers has a node pool named workloads. Machine type, minimum and maximum nodes are variables without a default, and the zones default to eu01-1, eu01-2 and eu01-3. The migration job becomes a Kubernetes Job on the same cluster.

  • Replica count held, exposure to decide

    The number of pods is a required input, so the apply holds until you set it: 0 for the apply step, then the number the source runs once the data is loaded. The Service is a ClusterIP and exposes nothing yet: an Ingress or a LoadBalancer Service, its certificate and its DNS record are your decision.

  • Named as not carried

    The service account of the revision, the labels, the traffic split between revisions, the deletion protection and the Cloud SQL connection volume. The pods run as the default ServiceAccount until you decide the identity, and they reach the database by its network address.

How the conversion works

  1. Resolve the estate

    18 files and the registry module project_services 18.0.0 become one dependency graph: 37 resources, 11 migration units and 29 dependencies, read offline. Five more units depend on variables and are decided when OpenTofu plans.

  2. Decide per resource

    A versioned rule base gives each resource a STACKIT target and a migration class, based on its actual configuration and with a confidence value.

  3. Write STACKIT code

    10 resource blocks in files per domain: data, Kubernetes, messaging, storage, identity and security, plus 13 variables, Kubernetes manifests and the STACKIT provider pinned to 0.121.0. Four units are proposed as modules that stay switched off.

  4. Check and trace

    StackToIT ran its 20 draft checks, and none found a violation. OpenTofu 1.13.1 ran tofu init and tofu validate on the draft and accepted it. Each block carries a trace ID that leads back to its source resource.

What the run reported

Selected lines of the console summary of this run, verbatim.

Console summary (selected lines)console.txt
Source files:           18
Resources scanned:      37  (34 in the root module · 3 inside 1 module call)
Migration units:        11 known-present (11 instances) · 5 conditional
Unit dependencies:      29
Modules:                1 module call (1 external, 0 local): 1 loaded, 0 not downloaded, 0 invalid
Source checks:          Built-in source checks passed — OpenTofu validation of the source did not run (LOCK_FILE)

DIRECT              0  
RECONFIGURE         6  ████████████████ 54%
REPLATFORM          2  █████ 18%
CONTAINERIZE        2  █████ 18%
REDESIGN            0  
MANUAL_REVIEW       1  ██ 9%
BLOCKED             0    (the mapping class BLOCKED, not the execution blockers, which Migration readiness counts)
Confidence  HIGH 0 · MEDIUM 10 · LOW 1

69 source properties checked across 11 generated units: 29 carried or explained, 40 NOT CARRIED, 0 unaccounted — see engineering/architect-report.md.

STACKIT draft (this folder/terraform): 11 units generated · 4 proposed, switched off · 0 need no resource · 1 left for the architect · 13 variables
  StackToIT draft checks: PASSED (20 checks, none found a violation)
  OpenTofu validation: PASSED (tofu init -backend=false and tofu validate accepted the draft; tofu 1.13.1)
tofu validate: STATIC_TERRAFORM_VALIDATION_PASSED — deployment readiness and live target behaviour not verified

Next action   Allow the calls of STACKIT APIs that are not generally available — Owner: Platform Architect · details: index.html
  • 37 resources, 11 units, 5 at plan time

    34 resources sit in the root module and 3 inside the registry module. Child resources join the unit they serve, and 8 support resources are read by the units that use them. Five further units exist only if a variable holds when OpenTofu plans; they are assessed too and marked as such.

  • 11 of 16 units drafted

    11 units have generated STACKIT resources, written as 10 blocks. 4 are proposed and stay switched off until you decide, and the Firebase project stays with your architect. The draft has 13 variables.

  • 69 properties, 0 unaccounted

    29 properties are carried or explained and 40 are named as not carried over, in the file engineering/not-carried.csv. The check covers the 11 generated units.

  • A class for every unit

    The run decided 6 RECONFIGURE, 2 REPLATFORM, 2 CONTAINERIZE and 1 MANUAL_REVIEW, and assessment.json gives the reason for each unit.

  • The next action has an owner

    The plan has 78 steps. 10 of its 12 automated steps are free of decisions and wait only for values such as project_id. 6 decisions are open; the first, owned by the Platform Architect, is to allow STACKIT APIs that are not generally available. 27 input requests are open.

How the 11 units and the 5 decided at plan time were decided

Google Cloud sourceSTACKIT draftClass
google_sql_database_instance.postgresstackit_postgresflex_instance, version 14, flavor 2.4Replatform
google_pubsub_topic.fauxstackit_rabbitmq_instance, shared: the topic becomes an exchange, created in RabbitMQ by handReplatform
google_cloud_run_v2_service.serverKubernetes Deployment, ConfigMap and Service on stackit_ske_cluster.containersContainerize
google_cloud_run_v2_job.migrateKubernetes Job on the same clusterContainerize
google_secret_manager_secret.django_settingsstackit_secretsmanager_instance.migrated_secrets, sharedReconfigure
google_secret_manager_secret.django_admin_passwordThe same stackit_secretsmanager_instance; the values are written by an API step, never copiedReconfigure
google_storage_bucket.mediastackit_objectstorage_bucket.media; objects are copied with rcloneReconfigure
google_service_account.serverstackit_service_account.serverReconfigure
google_service_account.clientstackit_service_account.clientReconfigure
google_service_account.automationstackit_service_account.automationReconfigure
google_firebase_project.defaultNo STACKIT target: STACKIT has no mobile or web app backend platform, so your architect decides app by appArchitect decision
google_service_account.init (decided at plan time)stackit_service_account.initReconfigure
google_cloudbuild_trigger.activategcb (decided at plan time)Proposed, switched off: a STACKIT Git Pipelines workflow or your own CI/CDRedesign
google_cloudbuild_trigger.init (decided at plan time)Proposed, switched off: a STACKIT Git Pipelines workflow or your own CI/CDRedesign
google_cloudbuild_trigger.placeholder (decided at plan time)Proposed, switched off: a STACKIT Git Pipelines workflow or your own CI/CDRedesign
google_firebase_hosting_site.client (decided at plan time)Proposed, switched off: Object Storage as origin of a CDN distribution with a custom domainRedesign

Classes as the run decided them. The first 11 rows are the units found in the source; the last 5 exist only if a variable holds when OpenTofu plans, and hold on the defaults of this run.

What lands in terraform/

The STACKIT Terraform of this run, organized the way a platform team maintains it.

data.tf
PostgreSQL Flex instance for the Cloud SQL instance
kubernetes.tf
One SKE cluster with a node pool for the Cloud Run service and job
kubernetes/
Manifests for server (ConfigMap, Deployment, Service) and migrate (Job)
messaging.tf
RabbitMQ instance for the Pub/Sub topic
rabbitmq/
Topology file and script for the exchanges and queues, which the provider does not create
storage.tf
Object Storage bucket for the media bucket
identity.tf
The 4 service accounts
security.tf
Secrets Manager instance and the migration user that writes the secrets
proposals.tf, proposals/
8 modules, each behind a switch that is off until you decide
handover/
Preparation notes for the database move, the media copy and cluster access
variables.tf
The 13 values your team sets or confirms, each with a description
terraform.tf
Pinned STACKIT provider
README.md
What the draft holds, the services to enable first, the target security checklist and what is not carried

What comes with the Terraform

The same run writes the material your architects and operators work from.

assessment.json
Per unit: class, confidence, STACKIT target, reason and findings
engineering/not-carried.csv
The 40 properties the draft does not carry, one row each, with the reason
engineering/trace/
One trace file per draft file, from each block back to its source
architecture/
Diagrams of the current and target architecture, network security and data flows
migration/technical-sequencing.md
4 waves computed from the dependencies between units
execution/
The execution package: 24 work packages with 64 manual steps, 2 API steps, a deploy script and a pipeline example
decisions.json
The structured decision record, with 6 decisions to make in this run
financial/
Price lines and coverage: 2 of 8 STACKIT lines priced, no comparison stated
review/
Templates for your answers and target values, which the next run reads

Proven on a real Google Cloud estate

Google Cloud's Dynamic Python Web App, a public Terraform project from Google, assessed by StackToIT and published in full, exactly as the run wrote it.

View Case Study
37
Terraform resource declarations analyzed
11
migration units identified
11
target units generated, from 16 considered
78
plan steps written

Questions platform teams ask

Does StackToIT convert Terraform modules?

Yes. StackToIT resolves registry and local modules. This estate calls one registry module, project_services 18.0.0, which the run downloaded and read together with the root module: its 3 resources are part of the 37 and form no migration unit of their own.

Does it work with OpenTofu?

Yes. StackToIT reads Terraform and OpenTofu root modules. In this run OpenTofu 1.13.1 ran tofu init -backend=false and tofu validate on the STACKIT draft, and both passed. That is static validation against the pinned provider schema; no plan was run against a STACKIT project.

What happens to a Cloud Run service and job?

STACKIT has no Cloud Run. The run classifies the service server and the job migrate as CONTAINERIZE: both become Kubernetes workloads on one SKE cluster. Image, port, health check and secret references are carried; the machine type, the node counts and the number of pods are inputs, and who may call the service is decided by a person.

What happens to Pub/Sub?

The topic becomes an exchange on a shared STACKIT RabbitMQ instance (REPLATFORM), and its subscriptions become queues. Names, delivery, retention and push settings are not generated; a topology file and a script are provided. Two API steps, for the RabbitMQ topology and for Secrets Manager, call STACKIT APIs the run treats as pre-release (the messaging API is a pre-release version, the Secrets Manager API states no release status), so the plan makes them only after an explicit opt-in.

What about Firebase?

The Firebase project is classed MANUAL_REVIEW. None of the 115 resource types of the pinned STACKIT provider is a mobile or web app backend platform (checked 5 November 2026), so a person decides, app by app, which backend replaces the Firebase services. The hosting site is proposed as Object Storage behind a CDN distribution and stays switched off.

What happens to the database password?

It is not copied. STACKIT generates the password of the PostgreSQL Flex user, and you hand the new one to the application.

Why is confidence capped at MEDIUM?

The repository commits no .terraform.lock.hcl and constrains google to ">= 6, < 8", which excludes the 8.4.0 release the knowledge base was checked against. The OpenTofu validation of the source therefore did not run, and every unit stays at MEDIUM or below. The remedy is named: commit the lock file that tofu init writes and run again.

Is there a price comparison?

Not in this run. The price cache holds no Google Cloud price, so the report states no saving. 2 of 8 STACKIT lines are priced, 164.67 EUR a month as a partial list price, and the report prints the commands that fill the missing prices.

How do I repeat the run?

Clone the upstream repository at commit 7c9e8fb, create a variable file with the single line project_id = "example-project" and run stacktoit assess from the infra folder: stacktoit assess . ../../run-python-webapp --var-file ../../example.tfvars. Then run stacktoit verify on the output folder; on the published copy it reports 146 files matching manifest.json.

Who applies the STACKIT Terraform?

Your team, through your own pipeline, after your architects review the draft. StackToIT changes nothing in any cloud; the region variable defaults to eu01 and is the one default the report asks a person to confirm.

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.

Leave your contact details and our specialists will get in touch.

Prefer to write yourself? connect@stacktoit.com

We use your details only to answer this request and to discuss a possible pilot. Privacy policy