diff --git a/.gitignore b/.gitignore index bea598b..c2a7c6a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,6 @@ .### Terraform template # Local .terraform directories -**/.terraform/* +**/.terraform/** # .tfstate files *.tfstate diff --git a/examples/iaas-image-upload/.terraform-version b/examples/iaas-image-upload/.terraform-version new file mode 100644 index 0000000..22708fe --- /dev/null +++ b/examples/iaas-image-upload/.terraform-version @@ -0,0 +1 @@ +v1.5.7 diff --git a/examples/iaas-image-upload/.terraform.lock.hcl b/examples/iaas-image-upload/.terraform.lock.hcl new file mode 100644 index 0000000..ac34bd3 --- /dev/null +++ b/examples/iaas-image-upload/.terraform.lock.hcl @@ -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", + ] +} diff --git a/examples/iaas-image-upload/000-provider.tf b/examples/iaas-image-upload/000-provider.tf new file mode 100644 index 0000000..4a26c40 --- /dev/null +++ b/examples/iaas-image-upload/000-provider.tf @@ -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 +} diff --git a/examples/iaas-image-upload/010-variables.tf b/examples/iaas-image-upload/010-variables.tf new file mode 100644 index 0000000..3e2d6ba --- /dev/null +++ b/examples/iaas-image-upload/010-variables.tf @@ -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 " + 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" + } +} diff --git a/examples/iaas-image-upload/020-image.tf b/examples/iaas-image-upload/020-image.tf new file mode 100644 index 0000000..f270f91 --- /dev/null +++ b/examples/iaas-image-upload/020-image.tf @@ -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 + } +} diff --git a/examples/iaas-image-upload/030-outputs.tf b/examples/iaas-image-upload/030-outputs.tf new file mode 100644 index 0000000..c2ad9cb --- /dev/null +++ b/examples/iaas-image-upload/030-outputs.tf @@ -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 +} diff --git a/examples/iaas-image-upload/040-server.tf b/examples/iaas-image-upload/040-server.tf new file mode 100644 index 0000000..a24f5b8 --- /dev/null +++ b/examples/iaas-image-upload/040-server.tf @@ -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 +} diff --git a/examples/iaas-image-upload/MAINTAINERS.md b/examples/iaas-image-upload/MAINTAINERS.md new file mode 100644 index 0000000..345a653 --- /dev/null +++ b/examples/iaas-image-upload/MAINTAINERS.md @@ -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. diff --git a/examples/iaas-image-upload/README.md b/examples/iaas-image-upload/README.md new file mode 100644 index 0000000..1bd0e10 --- /dev/null +++ b/examples/iaas-image-upload/README.md @@ -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= +``` + +--- + +## 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 \ + --role compute.editor \ + --service-account-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 \ + --name "tf-image-upload-sa" + +mkdir -p keys +stackit iam service-account key create \ + --project-id \ + --service-account-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 1–3 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 + +# Show details for the uploaded image +stackit image show --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) diff --git a/examples/iaas-image-upload/examples/terraform.tfvars.example b/examples/iaas-image-upload/examples/terraform.tfvars.example new file mode 100644 index 0000000..d6c7187 --- /dev/null +++ b/examples/iaas-image-upload/examples/terraform.tfvars.example @@ -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" +} diff --git a/examples/iaas-image-upload/images/.gitkeep b/examples/iaas-image-upload/images/.gitkeep new file mode 100644 index 0000000..e69de29