AWS, AZURE, GOOGLE CLOUD → STACKIT · TERRAFORM AND OPENTOFU

Terraform and OpenTofu migration to STACKIT, step by step

Your Terraform or OpenTofu code already states what your estate is meant to be, so it is the best starting point for a migration to STACKIT, provided the tool sees every module and every variable. StackToIT reads the configuration, maps each unit to a STACKIT target and writes a new STACKIT Terraform draft. This guide walks through that path on a small estate with two module calls, a variable file and a queue, and shows what happens when an input is missing. The demo estate is AWS; the StackToIT guides for Azure and Google Cloud document the same assess command for azurerm, azapi and Google provider code, and their mappings differ per service.

The path in seven steps

From a source repository to a reviewed STACKIT configuration.

  1. Collect the roots and their variable files

    List every root module you want assessed, the variable files that give it values in your pipelines (terraform.tfvars, environment files, TF_VAR settings) and the .terraform.lock.hcl. The assessed folder is the folder that declares the infrastructure. Without a lock file the demo run warned that no entry pins the AWS provider 5.100.0, and the docs name the lock file as a condition for validating the source.

  2. Make the inputs complete

    A variable without default and without value refuses every unit that reads it. In the demo all three units read the environment name, so the first run wrote no draft. Files with standard names are read anyway; a file such as prod.tfvars is passed with --var-file, in the order OpenTofu would read the files.

  3. Run the assessment

    stacktoit assess on the root folder. Add --offline when no network access to remote modules is allowed. Local modules are loaded from disk.

  4. Read the module summary

    The console lists each module call, whether it is local or external, and how many units and resources came out of it. A module that was not downloaded or is invalid leaves no STACKIT package for its units.

  5. Review the drafted Terraform

    The draft is a new configuration with its own provider block, not a patched copy of the source. Review it with the platform team before any apply.

  6. Supply values and decisions, then rerun

    Open variables go into review/target-values.tfvars, design answers into review/inputs.yaml, and the next run reads them.

  7. Apply to a fresh state

    Apply the reviewed draft to a state of its own, with the apply order and prerequisites written in execution/terraform/apply.md. The source state stays where it is until you retire the source.

The input: two module calls, one variable file, one queue

A small AWS estate written for this page. The module is instantiated twice, once with a variable and once with a literal value, and prod.tfvars supplies the environment name.

Root modulemain.tf
variable "environment" {
  type = string
}

variable "retention_days" {
  type    = number
  default = 30
}

module "exports" {
  source         = "./modules/bucket"
  bucket_name    = "acme-${var.environment}-exports"
  retention_days = var.retention_days
}

module "archive" {
  source         = "./modules/bucket"
  bucket_name    = "acme-${var.environment}-archive"
  retention_days = 365
}

resource "aws_sqs_queue" "jobs" {
  name                       = "acme-${var.environment}-jobs"
  visibility_timeout_seconds = 60
}
Modulemodules/bucket/main.tf
resource "aws_s3_bucket" "this" {
  bucket = var.bucket_name
}

resource "aws_s3_bucket_versioning" "this" {
  bucket = aws_s3_bucket.this.id
  versioning_configuration {
    status = "Enabled"
  }
}

resource "aws_s3_bucket_lifecycle_configuration" "this" {
  bucket = aws_s3_bucket.this.id
  rule {
    id     = "expire"
    status = "Enabled"
    filter {}
    expiration {
      days = var.retention_days
    }
  }
}
Variable fileprod.tfvars
environment    = "prod"
retention_days = 90
  • Why this shape

    Real estates reuse modules and read values from files. If the tool ignores either, the draft is wrong without looking wrong.

Without values the run refuses to guess

Both runs were made by StackToIT on 4 November 2026 against the same folder. The first has no variable file and stops short of a draft. The second passes --var-file and assesses all seven resources.

Run without the variable fileconsole.txt
Source inputs missing: 1 required input is not in the package, so what reads it was not assessed.
  variable environment · main.tf:39 · aws_sqs_queue.jobs.name, module.archive.aws_s3_bucket.this.bucket
Run with --var-file prod.tfvarsconsole.txt
Resources scanned:      7  (1 in the root module · 6 inside 2 module calls)
Migration units:        3  (3 instances)
Unit dependencies:      0
Modules:                2 module calls (0 external, 2 local): 2 loaded, 0 not downloaded, 0 invalid
  module.archive  ./modules/bucket  1 unit (RECONFIGURE 1) · 3 resources
  module.exports  ./modules/bucket  1 unit (RECONFIGURE 1) · 3 resources
Source checks:          Built-in source checks passed — OpenTofu validation of the source was not run
  • What the refusal protects

    The first run wrote no terraform folder and no waves. All three units read the missing value: the two bucket names and the queue name. The tool invents no value for acme-${var.environment}-exports.

  • Declared omissions

    If an input is missing on purpose, --omitted-inputs declares it. The run is then partial: units that read it are refused by name and the others are drafted.

The drafted STACKIT Terraform

Resource blocks from the second run. Review comments above the blocks are left out here; the S3 API documents are the files the run writes for settings the provider has no attributes for.

STACKIT Terraform by StackToITterraform/terraform.tf
terraform {
  # Write-only attributes (e.g. a VPN pre-shared key) need Terraform/OpenTofu 1.11 or later.
  required_version = ">= 1.11.0"

  required_providers {
    stackit = {
      source  = "stackitcloud/stackit"
      version = "0.121.0"
    }
  }
}

provider "stackit" {
  default_region = var.region
}
STACKIT Terraform by StackToITterraform/storage.tf
resource "stackit_objectstorage_bucket" "archive_this" {
  project_id = var.project_id
  name       = "acme-prod-archive"
}

resource "stackit_objectstorage_bucket" "exports_this" {
  project_id = var.project_id
  name       = "acme-prod-exports"
}
S3 API document, module.exportsterraform/s3api/exports_this/lifecycle.json
{
  "Rules": [
    {
      "ID": "expire",
      "Status": "Enabled",
      "Filter": {
        "Prefix": ""
      },
      "Expiration": {
        "Days": 90
      }
    }
  ]
}
S3 API document, module.archiveterraform/s3api/archive_this/lifecycle.json
{
  "Rules": [
    {
      "ID": "expire",
      "Status": "Enabled",
      "Filter": {
        "Prefix": ""
      },
      "Expiration": {
        "Days": 365
      }
    }
  ]
}
  • Variables were resolved per module

    The lifecycle rule of module.exports carries 90 days from prod.tfvars, the rule of module.archive carries the literal 365 days.

  • One draft, flat

    Each module instance became its own bucket resource (archive_this, exports_this). The module structure is yours to rebuild on the target side if you want it.

What the run did with each source construct

Read from the second run.

Source constructResult in the runWhere to read it
module "exports" and module "archive", local sourceTwo module calls loaded, 6 resources scanned inside them, 1 unit eachConsole summary, architect-report.md
Module resources bucket, versioning, lifecycleOne unit per instance: RECONFIGURE, STACKIT Object Storagemigration/technical-sequencing.md
var.retention_days with default 30, set to 90 in prod.tfvarsRule expires after 90 days; the literal 365 stays 365terraform/s3api/*/lifecycle.json
aws_sqs_queue.jobsREPLATFORM to a STACKIT RabbitMQ instance with open variables for plan, version and access listterraform/messaging.tf, review/target-values.tfvars
provider region eu-central-1Placed in var.region, default eu01; data residency changes and is to be confirmed onceengineering/architect-report.md
required_version >= 1.6The draft requires >= 1.11.0; its header comment ties that floor to write-only attributes, for example a VPN pre-shared key, so check your pipeline versionterraform/terraform.tf

Classes are a proposal. A class names the type of work and does not authorise it.

Decisions your architects own

The run drafts. These choices stay with you.

  • Module structure on the target

    The draft is flat. Decide whether to wrap repeated buckets in a module again and how to name them.

  • State and pipeline

    Decide where the new state lives and which pipeline applies it. Check that your OpenTofu or Terraform version meets the draft requirement of 1.11.0 or later.

  • Provider version

    The draft pins the STACKIT provider at 0.121.0. The registry record read on 4 November 2026 shows 0.122.0 as latest, published on 3 November. Review the version against the registry before you apply.

  • API opt-in for S3 settings

    Versioning and lifecycle go through STACKIT S3 API calls (four scripts under execution/api) that the plan makes only after you answer the opt-in decision. The same decision holds an alternative path for the queue through a STACKIT messaging API that is not generally available; the RabbitMQ blocks of the draft do not wait for it.

  • Queue design

    The RabbitMQ instance needs a plan, a version and an access list that the source cannot supply. The run leaves them as variables with no default.

  • What stays outside Terraform

    Everything the provider cannot express remains a documented step: the run lists those settings as not carried or as S3 API documents. Two notes from the run: once versioning is enabled on STACKIT it can only be suspended, not disabled, and until the lifecycle rules are applied nothing expires.

Questions teams ask about Terraform and OpenTofu

Does StackToIT read my state file?

The documented inputs are configuration, variables and modules, and a state file is not among them. The assessment describes what your code declares. Compare with your state yourself if drift matters.

What about remote modules?

Remote modules are resolved from existing local sources first. Network retrieval is used when needed unless --offline is set. In the demo both module calls were local, and the console counted 0 external.

Can I convert one resource at a time?

The run assesses the whole root module so that dependencies are visible, then orders units into waves. You choose which blocks of the draft you apply first.

Does it validate the source with OpenTofu?

Yes, automatically, when the machine meets the conditions in the docs: OpenTofu on the path, a network-denied sandbox and the providers in a local mirror (--provider-mirror) or plugin cache; for the source also a lock file. Nothing is downloaded. --no-validate skips it, --validate fails the run if it cannot run. The demo runs used --no-validate, and the console says so.

Is the output safe to apply as written?

No. The package is a proposal and nothing in it is approved. Apply only after your review, in the order the apply file gives.

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.