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.
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]
}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.
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,
]
}
}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: TCPImage, 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
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.
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.
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.
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.
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.html37 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 source | STACKIT draft | Class |
|---|---|---|
| google_sql_database_instance.postgres | stackit_postgresflex_instance, version 14, flavor 2.4 | Replatform |
| google_pubsub_topic.faux | stackit_rabbitmq_instance, shared: the topic becomes an exchange, created in RabbitMQ by hand | Replatform |
| google_cloud_run_v2_service.server | Kubernetes Deployment, ConfigMap and Service on stackit_ske_cluster.containers | Containerize |
| google_cloud_run_v2_job.migrate | Kubernetes Job on the same cluster | Containerize |
| google_secret_manager_secret.django_settings | stackit_secretsmanager_instance.migrated_secrets, shared | Reconfigure |
| google_secret_manager_secret.django_admin_password | The same stackit_secretsmanager_instance; the values are written by an API step, never copied | Reconfigure |
| google_storage_bucket.media | stackit_objectstorage_bucket.media; objects are copied with rclone | Reconfigure |
| google_service_account.server | stackit_service_account.server | Reconfigure |
| google_service_account.client | stackit_service_account.client | Reconfigure |
| google_service_account.automation | stackit_service_account.automation | Reconfigure |
| google_firebase_project.default | No STACKIT target: STACKIT has no mobile or web app backend platform, so your architect decides app by app | Architect decision |
| google_service_account.init (decided at plan time) | stackit_service_account.init | Reconfigure |
| google_cloudbuild_trigger.activategcb (decided at plan time) | Proposed, switched off: a STACKIT Git Pipelines workflow or your own CI/CD | Redesign |
| google_cloudbuild_trigger.init (decided at plan time) | Proposed, switched off: a STACKIT Git Pipelines workflow or your own CI/CD | Redesign |
| google_cloudbuild_trigger.placeholder (decided at plan time) | Proposed, switched off: a STACKIT Git Pipelines workflow or your own CI/CD | Redesign |
| google_firebase_hosting_site.client (decided at plan time) | Proposed, switched off: Object Storage as origin of a CDN distribution with a custom domain | Redesign |
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
- StackToIT example: Google Cloud Dynamic Python Web App, the complete run
- GitHub: GoogleCloudPlatform/terraform-dynamic-python-webapp (Apache-2.0, pinned commit)
- STACKIT Docs: Terraform provider
- STACKIT Docs: PostgreSQL Flex
- STACKIT Docs: Kubernetes Engine
- STACKIT Docs: RabbitMQ
- Google Cloud documentation: Cloud SQL introduction
- Google Cloud documentation: What is Cloud Run
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
Thank you.
Your request has reached us. Our specialists will get in touch.