examples/iaas-image-upload #34

Merged
mauritz.uphoff merged 5 commits from examples/iaas-image-upload into main 2026-06-26 07:08:32 +00:00
12 changed files with 666 additions and 1 deletions

2
.gitignore vendored
View file

@ -1,6 +1,6 @@
.### Terraform template
# Local .terraform directories
**/.terraform/*
**/.terraform/**
# .tfstate files
*.tfstate

View file

@ -0,0 +1 @@
v1.5.7

View file

@ -0,0 +1,24 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/stackitcloud/stackit" {
version = "0.99.0"
constraints = ">= 0.99.0"
hashes = [
"h1:a9z0j1z/8GmGjz+VygIhgyBbMqxx7jlXGqCvWBDD1NY=",
"zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f",
"zh:396c0392b9ef5ec7f8613c29a64e183545cc16dda0ceb876393fc003dba71c73",
"zh:40d86a1fb1c9ed4579583acb8ecc219edca44f9ee5221bfdcbc1bee2ce6654e7",
"zh:4ccbbecc3575737d87195ad13448d06071be9925760a2da5b7e5e8b91517f876",
"zh:506d786647c4566a82487fc3ffe0792f37a63ec8d6b54821aa3c7485e5ed6760",
"zh:848f638c500f1928f8593ae189472add1a0871c1e056d7df06871652ddee3409",
"zh:9ed739aec2c60cdfae3a33e4f349fa630fd0fd0ab50fcec5745774d42a6d6e70",
"zh:c0ac883dd73bd886e419d912c28ec29bb90a611b023cf4ae1b0534945cce1694",
"zh:df28663578694b25453b9d0a1cd7633a0f7fb1c113870cd3c133e9dc05d35946",
"zh:eaacb4a4512f41d44e46f82f042a19ab96c9d90d470890e2fd82c6cafb33bf0e",
"zh:ef9dd9b10571804f3a4dd6062405d0e473df270d75f05f897901c54d7d6c3d9d",
"zh:f40add9cd4fd4a7cda53f4a418c5f47a220b5ba5c4fc2377f60b1e16368f87d9",
"zh:f65deb30c1e3e8018a888d1aed56e895ea1e26b880f22a5772771e9836c9b5a4",
"zh:f8d14feddfd9d785d3ee6469937234a631998758ea5e8c16ecf61cdb94b07564",
]
}

View file

@ -0,0 +1,28 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
terraform {
required_version = ">= 1.5.7"
required_providers {
stackit = {
source = "stackitcloud/stackit"
version = ">= 0.99.0"
}
}
}
provider "stackit" {
default_region = var.stackit_region
service_account_key_path = var.stackit_service_account_key_path
}

View file

@ -0,0 +1,159 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Provider
variable "stackit_region" {
description = "STACKIT region, e.g. eu01"
type = string
default = "eu01"
}
variable "stackit_service_account_key_path" {
description = "Path to the STACKIT service account key JSON file"
type = string
default = "keys/sa-key.json"
}
# Project
variable "project_id" {
description = "STACKIT project ID to which the image is uploaded — find it in the Portal or via: stackit project list"
type = string
}
# Image
variable "image_name" {
description = "Name of the custom image as it will appear in STACKIT"
type = string
}
variable "image_file_path" {
description = "Local path to the image file to upload, e.g. ./images/custom-image.qcow2 — the file must exist before running terraform apply"
type = string
}
variable "disk_format" {
description = "Disk format of the image. Supported values: qcow2, raw, iso"
type = string
default = "qcow2"
validation {
condition = contains(["qcow2", "raw", "iso"], var.disk_format)
error_message = "disk_format must be one of: qcow2, raw, iso."
}
}
variable "min_disk_size" {
description = "Minimum disk size required to boot instances from this image, in GB"
type = number
default = 20
validation {
condition = var.min_disk_size >= 1
error_message = "min_disk_size must be at least 1 GB."
}
}
variable "min_ram" {
description = "Minimum RAM required to boot instances from this image, in MB"
type = number
default = 2048
validation {
condition = var.min_ram >= 1
error_message = "min_ram must be at least 1 MB."
}
}
# Image Config
variable "uefi" {
description = "Enable UEFI boot for instances created from this image. Set to false for BIOS-only images."
type = bool
default = true
}
variable "secure_boot" {
description = "Enable Secure Boot for instances created from this image. Requires UEFI (uefi = true)."
type = bool
default = false
}
# Server
variable "server_name" {
description = "Name of the server to create from the uploaded image"
type = string
default = "custom-image-server"
}
variable "machine_type" {
description = "STACKIT machine type for the server — list available: stackit server machine-type list --project-id <id>"
type = string
default = "g1.1"
}
variable "availability_zone" {
description = "Availability zone for the server, e.g. eu01-1, eu01-2, eu01-3"
type = string
default = "eu01-1"
}
variable "boot_volume_size_gb" {
description = "Boot volume size in GB — must be >= min_disk_size of the image"
type = number
default = 20
validation {
condition = var.boot_volume_size_gb >= 1
error_message = "boot_volume_size_gb must be at least 1 GB."
}
}
variable "network_cidr" {
description = "IPv4 CIDR block for the server's private network"
type = string
default = "10.10.0.0/24"
validation {
condition = can(cidrnetmask(var.network_cidr))
error_message = "network_cidr must be a valid CIDR, e.g. 10.10.0.0/24."
}
}
variable "keypair_name" {
description = "Name of the SSH key pair to register in STACKIT"
type = string
default = "custom-image-key"
}
variable "ssh_public_key" {
description = "SSH public key string (ssh-ed25519 AAAA... or ssh-rsa AAAA...) — only required when deploy_server = true"
type = string
sensitive = true
default = ""
}
# Labels
variable "labels" {
description = "Key-value labels to attach to the image resource"
type = map(string)
default = {
managed-by = "terraform"
example = "image-upload"
}
}

View file

@ -0,0 +1,30 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
resource "stackit_image" "custom_image" {
project_id = var.project_id
name = var.image_name
disk_format = var.disk_format
local_file_path = var.image_file_path
min_disk_size = var.min_disk_size
min_ram = var.min_ram
labels = var.labels
config = {
uefi = var.uefi
secure_boot = var.secure_boot
}
}

View file

@ -0,0 +1,43 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
output "image_id" {
description = "ID of the uploaded custom image — use this UUID when creating servers or volumes"
value = stackit_image.custom_image.image_id
}
output "image_name" {
description = "Name of the uploaded custom image"
value = stackit_image.custom_image.name
}
output "image_scope" {
description = "Scope of the image (private or public)"
value = stackit_image.custom_image.scope
}
output "checksum_algorithm" {
description = "Algorithm used for the image checksum"
value = stackit_image.custom_image.checksum.algorithm
}
output "checksum_digest" {
description = "Checksum digest of the uploaded image — verify this against your local file"
value = stackit_image.custom_image.checksum.digest
}
output "server_id" {
description = "ID of the server created from the custom image"
value = stackit_server.from_custom_image.server_id
}

View file

@ -0,0 +1,51 @@
# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Example: boot a server from the uploaded custom image.
# The image_id is referenced directly from the stackit_image resource above.
resource "stackit_network" "main" {
project_id = var.project_id
name = "${var.server_name}-network"
ipv4_prefix_length = tonumber(split("/", var.network_cidr)[1])
}
resource "stackit_network_interface" "server" {
project_id = var.project_id
network_id = stackit_network.main.network_id
name = "${var.server_name}-nic"
}
resource "stackit_key_pair" "server" {
name = var.keypair_name
public_key = chomp(var.ssh_public_key)
}
resource "stackit_server" "from_custom_image" {
project_id = var.project_id
name = var.server_name
machine_type = var.machine_type
availability_zone = var.availability_zone
keypair_name = stackit_key_pair.server.name
boot_volume = {
source_type = "image"
source_id = stackit_image.custom_image.image_id
size = var.boot_volume_size_gb
}
network_interfaces = [stackit_network_interface.server.network_interface_id]
labels = var.labels
}

View file

@ -0,0 +1,9 @@
# Maintainers
General maintainers:
- Sven Schmidt (sven.schmidt@digits.schwarz)
This example is actively maintained. The owner is responsible for reviewing and updating dependencies and functionalities on a monthly basis.
For questions, issues, or feature requests, please email general maintainers.
Please include the BP name and version in your request. We will track your request as an issue.

View file

@ -0,0 +1,272 @@
# iaas-image-upload
Upload a custom VM image to STACKIT using Terraform.
This example provisions a single `stackit_image` resource from a local image file. It covers the minimal configuration needed to make a custom image available in a STACKIT project, including disk format, boot requirements, and UEFI/Secure Boot settings.
---
## Architecture
```mermaid
sequenceDiagram
participant User
participant FS as Local Filesystem
participant TF as Terraform / STACKIT API
participant IMG as STACKIT Image Service
participant SRV as STACKIT Compute
Note over User,SRV: terraform apply
User->>FS: read image file (local_file_path)
FS-->>TF: binary image data
User->>TF: create image metadata (name, format, min_disk, min_ram, config)
TF-->>IMG: POST /v1/projects/{project_id}/images
IMG-->>TF: image_id · status: uploading
TF-->>IMG: PUT upload binary image data
IMG-->>IMG: compute checksum · validate format
IMG-->>TF: status: active · checksum_digest
TF-->>User: ✅ image_id · checksum_digest
Note over User,SRV: 04-server.tf
User->>TF: create Key Pair + Server (source_id = image_id)
TF-->>SRV: POST /v1/projects/{project_id}/servers
SRV-->>TF: server_id
TF-->>User: ✅ server_id
```
```
STACKIT Project
├── Image: custom-image-v1 (scope: private)
│ ├── Format: qcow2
│ ├── Min disk: 20 GB
│ ├── Min RAM: 2048 MB
│ └── Config: uefi=true · secure_boot=false
└── Server: custom-image-server
├── Machine: g1.1
├── Zone: eu01-1
└── Boot: source_type=image · source_id=<image_id>
```
---
## Overview
| Component | Description |
| ------------ | -------------------------------------------------------- |
| Image upload | Uploads a local image file to the STACKIT Image Service |
| Boot config | Configures UEFI and Secure Boot flags via `config` block |
| Labels | Attaches key-value labels for resource management |
| In this example | Not in this example |
| ----------------------------- | ----------------------------- |
| Upload a custom image | Image sharing across projects |
| UEFI and Secure Boot settings | Auto-scaling / multiple VMs |
| Boot a server from the image | Storage encryption |
| Image labels | DNS / Load Balancer |
---
## Prerequisites
| Tool | Version |
| --------------- | -------------- |
| Terraform | >= 1.5.7 |
| STACKIT CLI | latest |
| STACKIT account | Project access |
### Required STACKIT permissions
The service account used by Terraform must have the following roles assigned in the target project:
| Service | Role | Required for |
| --------------- | -------- | ----------------------------------- |
| Compute / Image | `editor` | Upload and manage custom images |
| Compute | `editor` | Create servers, networks, key pairs |
Assign roles via the STACKIT Portal (Project → Access → Service Accounts) or the CLI:
```bash
stackit project role assign \
--project-id <project-id> \
--role compute.editor \
--service-account-email <sa-email>
```
### Image file
The image file must exist locally before running `terraform apply`. It is **not** included in this repository and must never be committed.
Place your image file in the `images/` directory:
```
images/
└── custom-image.qcow2 ← your local image file (gitignored)
```
Supported formats: `qcow2`, `raw`, `iso`
The most common format for Linux-based virtual machine images is `qcow2`.
### Service account
Create a service account and download its key:
```bash
stackit iam service-account create \
--project-id <project-id> \
--name "tf-image-upload-sa"
mkdir -p keys
stackit iam service-account key create \
--project-id <project-id> \
--service-account-email <sa-email> \
--output-format json > keys/sa-key.json
```
---
## Deployment
### 1. Place the image file
```bash
cp /path/to/your/image.qcow2 images/custom-image.qcow2
```
### 2. Configure variables
```bash
cp examples/terraform.tfvars.example terraform.tfvars
# Fill in: project_id, image_name, image_file_path
```
Find your project ID:
```bash
stackit project list
```
### 3. Deploy
```bash
terraform init
terraform plan
terraform apply
```
Duration: depends on image size and upload speed. A 2 GB image typically takes 13 minutes.
### 4. Outputs
```bash
terraform output
```
---
## Validation
After a successful `terraform apply`, verify the image in STACKIT:
```bash
# List images in your project
stackit image list --project-id <project-id>
# Show details for the uploaded image
stackit image show --project-id <project-id> --image-id $(terraform output -raw image_id)
```
The `checksum_digest` output can be used to verify the uploaded image matches your local file:
```bash
# qcow2 image checksum (SHA-256)
sha256sum images/custom-image.qcow2
terraform output checksum_digest
```
---
## File Structure
```
iaas-image-upload/
├── .terraform-version # Terraform version pin (v1.5.7)
├── .gitignore
├── 000-provider.tf # stackitcloud/stackit provider
├── 010-variables.tf # All variables with descriptions and defaults
├── 020-image.tf # stackit_image resource
├── 030-outputs.tf # Image ID, name, scope, checksum, server outputs
├── 040-server.tf # Optional: server + network from the uploaded image
├── examples/
│ └── terraform.tfvars.example # Example variable values (safe to commit)
├── images/ # Place your image file here (gitignored)
│ └── .gitkeep
└── keys/ # SA key JSON — gitignored
```
---
## UEFI and Secure Boot
| Variable | Default | Description |
| ------------- | ------- | -------------------------------------------------------- |
| `uefi` | `true` | Enables UEFI boot; set to `false` for legacy BIOS images |
| `secure_boot` | `false` | Enables Secure Boot; requires `uefi = true` |
Most modern Linux distributions support UEFI. If your image was built for BIOS boot only, set `uefi = false` and `secure_boot = false`.
Secure Boot requires a signed bootloader. Enable it only if your image explicitly supports it.
---
## Deploy a server from the uploaded image
`04-server.tf` creates a server that boots directly from the uploaded image.
The `source_id` is wired to `stackit_image.custom_image.image_id` — no manual copy-paste of the image UUID needed.
Provide your SSH public key in `terraform.tfvars`:
```hcl
ssh_public_key = "ssh-ed25519 AAAA... your-key-comment"
```
Extend `04-server.tf` with `stackit_network` and `stackit_network_interface` resources if network connectivity is required.
The default SSH user depends on the operating system of your image (e.g. `debian`, `ubuntu`, `root`).
---
## Security
| File | Git status | Contains |
| ------------------ | ---------- | ---------------------------- |
| `terraform.tfvars` | gitignored | Project ID, sensitive config |
| `keys/` | gitignored | Service account JSON key |
| `images/` | gitignored | Local image files |
Never commit image files, service account keys, or `terraform.tfvars` to the repository.
---
## Cleanup
```bash
terraform destroy
```
Removes the uploaded image from STACKIT. This does not affect any running instances that were created from the image.
---
## References
- [STACKIT Terraform Provider — stackit_image](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs/resources/image)
- [STACKIT Developer Documentation](https://docs.stackit.cloud)
- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli)

View file

@ -0,0 +1,48 @@
# Copy this file to terraform.tfvars and fill in the required values.
# terraform.tfvars is gitignored — never commit real project IDs or credentials.
# ─── Provider ─────────────────────────────────────────────────────────────────
stackit_region = "eu01"
stackit_service_account_key_path = "keys/sa-key.json"
# ─── Project ──────────────────────────────────────────────────────────────────
# Find your project ID in the STACKIT Portal or via: stackit project list
project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
# ─── Image ────────────────────────────────────────────────────────────────────
image_name = "custom-image-v1"
# Path to the local image file — this file must exist before running terraform apply.
# The images/ directory is gitignored; place your image file there.
image_file_path = "./images/custom-image.qcow2"
disk_format = "qcow2" # supported: qcow2, raw, iso
min_disk_size = 20
min_ram = 2048
# ─── Image Config ─────────────────────────────────────────────────────────────
uefi = true
secure_boot = false
# ─── Server ───────────────────────────────────────────────────────────────────
server_name = "custom-image-server"
machine_type = "g1.1"
availability_zone = "eu01-1"
boot_volume_size_gb = 20
network_cidr = "10.10.0.0/24"
keypair_name = "custom-image-key"
# Paste your SSH public key here (never the private key)
ssh_public_key = "ssh-ed25519 AAAA... your-key-comment"
# ─── Labels ───────────────────────────────────────────────────────────────────
labels = {
managed-by = "terraform"
example = "image-upload"
}