AZURE VIRTUAL MACHINES → STACKIT COMPUTE · TUTORIAL
Azure Virtual Machines to STACKIT Compute, step by step
This walk-through takes a small Azure estate (a Linux VM with a data disk, a Windows VM, one subnet) through StackToIT on 4 November 2026 and follows the output file by file: the input, the two runs, the STACKIT server and volume the engine drafts, and the image, size and login decisions that stay with you.
The walk-through in six steps
Every step names the file where the answer sits, so you can repeat it on your own Terraform.
1. Put the estate in one folder
StackToIT reads azurerm Terraform from a directory and never connects to an Azure account. The demo is one main.tf with a resource group, a virtual network, a subnet, a Linux VM with a managed data disk, and a Windows VM.
2. Run the assessment
The command is stacktoit assess with the input directory and an output directory. Offline mode is the default for this walk-through; OpenTofu validation of the source was switched off because no provider mirror was available.
3. Read what is blocked
A variable without a value stops the units that read it (and what hangs on them). The first run names the variable and the file that needs it, and the console says how many units have no draft.
4. Supply the value and run again
A variable file in the assessed directory is enough. The second run drafts all four units that have a STACKIT resource: subnet, both VMs and the data disk.
5. Read the draft
terraform/compute.tf holds the servers, the volume and the attachment. Each block carries review comments for the settings it could not carry over.
6. Decide, then move the disks
Image, machine type, storage class, login and licensing are variables or decisions. The data disk has no chosen mechanism: migration/data-movement.md says so, and the move is yours to plan.
Step 3 and 4: a missing variable, then a complete run
The same estate assessed twice. Left: the two lines of the console summary after the first run, where the SSH key variable had no value. Right: the same two lines after a terraform.tfvars with demo values was placed next to main.tf.
Source 9 resources · 5 units · visibility: Required input missing
Migration 2 drafted · 1 need no resource · 2 have no draft because an input the source needs is not in the package (of 5 units: 3 the draft considered, 2 have no draft)Source 9 resources · 5 units · visibility: Complete
Migration 4 drafted · 1 need no resource (of 5 units the draft considered)Required input missing
The first run reports the source as incomplete and drafts 2 of 5 units: the subnet and the Windows VM. The Linux VM has no draft because its SSH key comes from a variable nobody supplied, and the data disk has none because it hangs on that VM. The architect report names the variable and the line of main.tf that reads it.
Nothing is guessed
The engine does not invent a key to get past the gap. The values in this demo are placeholders written for this page. In your estate you pass the real variable file with the assessed directory, or declare an input as removed on purpose.
The key lands in the draft
The public key from the variable file is written into the stackit_key_pair block as a literal. Treat the draft folder like source code: review it before it is committed, and keep secrets out of the assessed variable file.
Step 5: the Linux VM becomes a server with a key pair
Left: the azurerm_linux_virtual_machine as written. Right: the STACKIT blocks the engine drafted for it: the key pair, the network interface from the source NIC and the server. The review comments above each block in the file are left out of this pane; the list 'What the Azure VM settings become' further down sums them up.
resource "azurerm_linux_virtual_machine" "app01" {
name = "vm-billing-app01"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
size = "Standard_D4s_v5"
admin_username = "azureops"
network_interface_ids = [azurerm_network_interface.app01.id]
zone = "1"
admin_ssh_key {
username = "azureops"
public_key = var.admin_public_key
}
os_disk {
caching = "ReadWrite"
storage_account_type = "Premium_LRS"
disk_size_gb = 64
}
source_image_reference {
publisher = "Canonical"
offer = "0001-com-ubuntu-server-jammy"
sku = "22_04-lts-gen2"
version = "latest"
}
}resource "stackit_key_pair" "app01" {
name = "vm-billing-app01-key"
public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDemoKeyForThisGuideOnlyNotARealKey00000 ops@example.org"
}
resource "stackit_network_interface" "app01_1" {
project_id = var.project_id
network_id = stackit_network.vms.network_id
ipv4 = "10.40.1.10"
}
resource "stackit_server" "app01" {
project_id = var.project_id
name = "vm-billing-app01"
machine_type = var.app01_machine_type
availability_zone = var.availability_zone
boot_volume = {
source_type = "image"
source_id = var.app01_image_id
size = 64
delete_on_termination = true
}
keypair_name = stackit_key_pair.app01.name
network_interfaces = [stackit_network_interface.app01_1.network_interface_id]
}Boot disk size is kept
The 64 GB OS disk becomes the boot volume with size 64. The boot volume starts from an image variable, so the image is a value you supply.
Size is a variable, not a guess
Standard_D4s_v5 is stated in the variable description as about 4 vCPU and 16 GiB. You choose the STACKIT machine type that matches. The STACKIT machine type page lists fixed combinations of vCPU and RAM, and adapting them individually is not possible.
Login moves to the image
A STACKIT key pair holds only a public key. The Azure administrator user name and password handling are not carried; the login user comes from the image or from cloud-init data, and the draft says so in the block.
The static address is kept
The source NIC had the static private address 10.40.1.10, and the drafted stackit_network_interface carries it as ipv4 in the network drafted from the subnet (10.40.1.0/24). The Windows VM's NIC was dynamic, so its interface gets no fixed address.
The data disk becomes a volume and an attachment
From the same run. Left: the managed disk and its attachment. Right: the volume and the attachment resource of the draft, without their review comments.
resource "azurerm_managed_disk" "data" {
name = "disk-billing-app01-data"
location = azurerm_resource_group.app.location
resource_group_name = azurerm_resource_group.app.name
storage_account_type = "Premium_LRS"
create_option = "Empty"
disk_size_gb = 256
zone = "1"
}
resource "azurerm_virtual_machine_data_disk_attachment" "data" {
managed_disk_id = azurerm_managed_disk.data.id
virtual_machine_id = azurerm_linux_virtual_machine.app01.id
lun = 0
caching = "ReadOnly"
}resource "stackit_volume" "data" {
project_id = var.project_id
name = "disk-billing-app01-data"
availability_zone = var.availability_zone
size = 256
performance_class = var.data_performance_class
}
resource "stackit_server_volume_attach" "data" {
project_id = var.project_id
server_id = stackit_server.app01.server_id
volume_id = stackit_volume.data.volume_id
}Size copied, unit to confirm
256 stays 256. The draft notes that STACKIT documents the size as GB without saying GiB or 10^9 bytes, so check the unit on the target before data is copied.
Zone, LUN and caching are not carried
The volume is created in the zone of the availability_zone variable, which defaults to eu01-1. The LUN and the ReadOnly host caching have no counterpart, so a guest should find the volume by its ID, not by LUN.
Storage class is yours
Premium_LRS does not map to a performance class by itself. The data_performance_class variable lists the classes of the eu01 catalogue and validates your choice.
The values the draft leaves to you
Left: the demo variable file that freed the Linux VM in the second run. Right: the two variables of the draft's variables.tf that the Linux server reads. Neither has a default, so the draft does not plan until you set them.
admin_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIDemoKeyForThisGuideOnlyNotARealKey00000 ops@example.org"
win_admin_password = "placeholder-not-a-secret"variable "app01_image_id" {
type = string
description = "STACKIT image replacing the image of azurerm_linux_virtual_machine.app01, Azure image Canonical:0001-com-ubuntu-server-jammy:22_04-lts-gen2:latest (public image, or an uploaded raw/qcow2 conversion of the VHD)."
}
variable "app01_machine_type" {
type = string
description = "STACKIT machine type replacing azurerm_linux_virtual_machine.app01 (Azure Standard_D4s_v5 ≈ 4 vCPU / 16 GiB). See https://docs.stackit.cloud/products/compute-engine/server/basics/machine-types/"
}Image id
Use a public STACKIT image, or the id of an image you imported from the converted disk. The assessment records the image as a setting a person verifies against the live image catalogue before the id is pinned.
Machine type
The same logic: the variable description states the source size, and the choice is made against the machine type tables.
How the demo estate maps
| Azure resource | STACKIT target | Migration class |
|---|---|---|
| Linux virtual machine | Compute Engine: server, volumes, network interface | Reconfigure |
| Windows virtual machine | Compute Engine: server, volumes, network interface | Reconfigure |
| Managed disk with attachment | Block Storage: volume and server_volume_attach | Reconfigure |
| Subnet | STACKIT network with the same IPv4 prefix | Reconfigure |
| Virtual network | Project networking or Network Area; no resource drafted | Reconfigure |
Classes and confidence (MEDIUM for all five units) as the run decided them. The network interface is not a unit of its own: it is drafted with the server that uses it.
What the Azure VM settings become
Taken from the review comments in the draft and the open settings of the architect report.
Administrator account and password
Not carried. stackit_server has no admin account or password. Put a named user into the image or the cloud-init user data.
Disk caching and storage type
Not carried. The boot volume takes a size; the performance class is chosen by you.
Availability zone
Azure zone 1 is not mapped. The server and the volume sit in the zone of the availability_zone variable; use an affinity group to spread servers.
Windows licence
license_type = Windows_Server is listed as not carried on the Windows server. How Windows licensing works on the target is a question for your STACKIT contract, not for the Terraform.
Resource group
STACKIT has no resource groups. Every resource lands in var.project_id, and a person decides how group boundaries map to projects and roles.
Getting the disks across
The run writes the target and the order. It does not move data.
Export the disk
Azure cannot export a VHD from a disk attached to a running VM. Stop the VM, or snapshot the disk and export the snapshot. A read-only SAS URL comes from az disk grant-access, with a lifetime of at most 60 days, and az disk revoke-access closes it afterwards.
Convert
STACKIT imports raw, qcow2 and iso. A VHD is converted first, for example with qemu-img. Its convert command with -O raw is the one STACKIT's own page shows.
Import the image
Through the STACKIT IaaS API you create an image (it starts queued), upload the file to the upload URL the call returns, and check that the image is AVAILABLE before you use it. The documented maximum image size is 70 GB, so a larger system disk needs another route, such as a rebuilt image.
Create the server
Set app01_image_id to the new image id and apply. Data volumes are created empty and attached; the data on them is copied after that.
Plan the data copy
migration/data-movement.md lists the data disk as a stateful unit with the mechanism UNKNOWN. Provisioned capacity (256 GB) is not data size. Rate of change, tolerated downtime and consistency decide between a block-level copy, a file-level sync and an application-level export.
Decisions the architect owns
Machine type per server
From the source size and real utilisation, not from the name of the Azure size.
Image per server
A public STACKIT image or an imported conversion; verified, then pinned.
Performance class
One variable for the data volumes, so storage is decided in one place.
Login and hardening
Named users, SSH keys, password policy and agents come from the image or cloud-init.
Order of moves
The run puts the network first, servers after it, the data disk last (wave 4). A schedule is yours.
Questions about moving Azure VMs
Does StackToIT convert my VHD or copy the data?
No. It reads Terraform, writes a STACKIT draft and a plan, and never connects to Azure or moves data. Disk export, conversion, import and copy are steps a person carries out.
Why does the first run leave a VM without a draft?
A variable it reads had no value. The engine refuses to draft a unit whose input is missing and names the variable, so the gap is visible instead of filled with a guess. Provide a variable file and run again.
Can I keep the Azure VM size?
Not by name. STACKIT machine types are a fixed list of vCPU and RAM combinations. The draft states the source size as a hint and leaves the machine type to you.
Is the Terraform ready to apply?
No. It is a draft for review: image, machine type and project id are variables without a default, and the open settings need an answer first.
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.