Compare commits

..

1 commit

Author SHA1 Message Date
Tim Reibe
6a4a454090
fix(cloud-init): removed license headers from cloud-init.yaml files
Some checks failed
Default CI / Check for Open TODOs (pull_request) Successful in 25s
Default CI / Secret Scanner (TruffleHog) (pull_request) Successful in 49s
Default CI / Pre-Commit Hooks (pull_request) Failing after 1m29s
2026-06-25 18:53:58 +02:00
36 changed files with 20 additions and 1212 deletions

View file

@ -21,6 +21,16 @@ on:
branches: ["main"]
jobs:
secret-scan:
name: "Secret Scanner (TruffleHog)"
runs-on: ${{ github.server_url == 'https://github.com' && 'ubuntu-latest' || 'stackit-docker' }}
steps:
- name: Checkout code
uses: actions/checkout@v7
- name: TruffleHog Scan
uses: edplato/trufflehog-actions-scan@master
todo-check:
name: "Check for Open TODOs"
runs-on: ${{ github.server_url == 'https://github.com' && 'ubuntu-latest' || 'stackit-ubuntu-22' }}

7
.gitignore vendored
View file

@ -1,6 +1,6 @@
.### Terraform template
# Local .terraform directories
**/.terraform/**
**/.terraform/*
# .tfstate files
*.tfstate
@ -72,8 +72,3 @@ keys
### K8s
.kubeconfig
/examples/telemetry-router-hub-spoke-setup/scripts/downloads/
*.DS_Store
# ignore backend.conf files, but keep backend.conf.example
*backend.conf

View file

@ -30,6 +30,11 @@ repos:
args: [--allow-multiple-documents]
- id: check-added-large-files
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.2
hooks:
- id: gitleaks
- repo: https://github.com/antonbabenko/pre-commit-terraform
rev: v1.88.0
hooks:
@ -53,18 +58,13 @@ repos:
- id: prettier
types_or: [javascript, yaml, json, markdown]
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
- repo: local
hooks:
# Requires `addlicense` to be installed locally (go install github.com/google/addlicense@latest)
- id: addlicense
name: Add License Headers
description: Ensures all files have the Apache 2.0 license header (skips files starting with #cloud-config)
entry: bash -c 'for f in "$@"; do head -n 1 "$f" | grep -q "^#cloud-config" || addlicense -c "Schwarz Digits Cloud GmbH & Co. KG" -l apache "$f"; done' --
description: Ensures all files have the Apache 2.0 license header
entry: addlicense -c "Schwarz Digits Cloud GmbH & Co. KG" -l apache
language: system
types_or: [terraform, python, go, javascript, yaml, json, html, css]
pass_filenames: true

View file

@ -59,7 +59,7 @@ To maintain a clean and secure codebase, we enforce a strict CI pipeline on all
```
- **Terraform file naming:** All `.tf` files in examples **must** be prefixed with exactly 3 digits to enforce consistent ordering (e.g., `010-provider.tf`, `020-variables.tf`, `030-resources.tf`, `100-outputs.tf`). Files inside `modules/` directories are exempt from this rule. This check is enforced automatically by pre-commit.
- **Scan for Secrets:** Never commit credentials. We use `gitleaks` in the CI pipeline. Ensure you have no hardcoded tokens or passwords in your code.
- **Scan for Secrets:** Never commit credentials. We use `trufflehog` in the CI pipeline. Ensure you have no hardcoded tokens or passwords in your code.
### Repository structure

View file

@ -30,7 +30,7 @@ To ensure high standards and security, we follow a strict contribution process:
- **Automated Validation:** Every Pull Request must pass the CI pipeline, which includes:
- Linting and formatting checks.
- License header verification (Apache 2.0).
- Secret scanning (GitLeaks).
- Secret scanning (Trufflehog).
- **Best Effort Policy:** While we strive for quality, the content is provided "as-is." Use in production environments requires independent validation by the user.
## 4. Mirroring Process

View file

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

View file

@ -1,24 +0,0 @@
# 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

@ -1,28 +0,0 @@
# 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

@ -1,159 +0,0 @@
# 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

@ -1,30 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 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

@ -1,51 +0,0 @@
# 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

@ -1,9 +0,0 @@
# 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

@ -1,272 +0,0 @@
# 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

@ -1,48 +0,0 @@
# 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"
}

View file

@ -1,24 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 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.
variable "stackit_service_account_key_path" {
description = "Path to the STACKIT service account key file (JSON). Keep this file out of version control."
type = string
}
variable "stackit_organization_id" {
description = "STACKIT Organization ID (UUID). Found in the portal under Organization > Settings."
type = string
}
variable "stackit_project_name" {
description = "Display name of the hub project in STACKIT."
type = string
}
variable "stackit_org_admin" {
description = "Email address of the STACKIT user who will be set as project owner."
type = string
}
variable "stackit_region" {
description = "STACKIT region (e.g. eu01)."
type = string
}
variable "default_zone" {
description = "Availability zone within the region (e.g. eu01-1)."
type = string
}

View file

@ -1,27 +0,0 @@
# 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_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

@ -1,19 +0,0 @@
# 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_resourcemanager_project" "this" {
parent_container_id = var.stackit_organization_id
name = var.stackit_project_name
owner_email = var.stackit_org_admin
}

View file

@ -1,54 +0,0 @@
# 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.
# Provision PostgreSQL Flex Database Instance
resource "stackit_postgresflex_instance" "this" {
project_id = stackit_resourcemanager_project.this.project_id
name = "tf-state-instance"
version = "17"
flavor = {
cpu = 2
ram = 4
}
storage = {
class = "premium-perf2-stackit"
size = 10
}
replicas = 1
backup_schedule = "00 00 * * *"
acl = [
# WARNING: Open ACL is for development only. Restrict to your specific egress IP ranges in production.
"0.0.0.0/0"
]
}
# Provision PostgreSQL Flex Database Owner
resource "stackit_postgresflex_user" "db_owner" {
project_id = stackit_resourcemanager_project.this.project_id
instance_id = stackit_postgresflex_instance.this.instance_id
username = "tf_state_user"
roles = ["login", "createdb"]
}
# Provision PostgreSQL Flex Database
resource "stackit_postgresflex_database" "this" {
project_id = stackit_resourcemanager_project.this.project_id
instance_id = stackit_postgresflex_instance.this.instance_id
owner = stackit_postgresflex_user.db_owner.username
name = "tf-states"
}

View file

@ -1,27 +0,0 @@
# 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.
locals {
pg_username = stackit_postgresflex_user.db_owner.username
pg_password = stackit_postgresflex_user.db_owner.password
pg_host = stackit_postgresflex_user.db_owner.host
pg_port = stackit_postgresflex_user.db_owner.port
pg_database = stackit_postgresflex_database.this.name
}
output "pg_connection_uri" {
description = "PostgreSQL Flex User Connection String"
value = "postgres://${local.pg_username}:${local.pg_password}@${local.pg_host}:${local.pg_port}/${local.pg_database}?sslmode=require"
sensitive = true
}

View file

@ -1,23 +0,0 @@
# Phase 0: Bootstrap
This module provisions the STACKIT PostgreSQL Flex instance, the `terraform_state` database, and the dedicated `tf_state_user`. Its state is kept locally (or in an independent CI/CD backend) to prevent dependency conflicts.
## Implementation Steps
1. Initialize Terraform with the default local backend:
```sh
terraform init
```
2. Provision the PostgreSQL Flex resources:
```sh
terraform apply
```
3. Extract the generated PostgreSQL connection string from the Terraform outputs. This URI is required to configure the remote backend in the next phase.
```sh
terraform output -raw pg_connection_uri
```

View file

@ -1,31 +0,0 @@
# ---------------------------------------------------------------------------
# terraform.tfvars.example
#
# Copy this file into a project directory (e.g. 00-bootcamp/terraform.tfvars)
# and fill in your values. Do NOT commit terraform.tfvars to version control.
#
# Alternatively, export variables as environment variables:
# export TF_VAR_stackit_organization_id="<uuid>"
# ---------------------------------------------------------------------------
# --- STACKIT Identity --------------------------------------------------------
# Your STACKIT Organization ID.
# Portal: Organization > Settings > Organization ID
stackit_organization_id = "<your-organization-id>"
## Name of the bootstrapping project
stackit_project_name = "00-bootstrap"
# Email address of the STACKIT user set as project owner.
stackit_org_admin = "<your-admin@mail.com"
# --- Service Account ---------------------------------------------------------
# Path to the STACKIT service account key file.
# Download: STACKIT portal > Service Accounts > Keys > Create key
stackit_service_account_key_path = "./keys/service-account.json"
# --- Region ------------------------------------------------------------------
stackit_region = "eu01"
default_zone = "eu01-1"

View file

@ -1,24 +0,0 @@
# 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

@ -1,43 +0,0 @@
# 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.
variable "stackit_service_account_key_path" {
description = "Path to the STACKIT service account key file (JSON). Keep this file out of version control."
type = string
}
variable "stackit_organization_id" {
description = "STACKIT Organization ID (UUID). Found in the portal under Organization > Settings."
type = string
}
variable "stackit_project_name" {
description = "Display name of the hub project in STACKIT."
type = string
}
variable "stackit_org_admin" {
description = "Email address of the STACKIT user who will be set as project owner."
type = string
}
variable "stackit_region" {
description = "STACKIT region (e.g. eu01)."
type = string
}
variable "default_zone" {
description = "Availability zone within the region (e.g. eu01-1)."
type = string
}

View file

@ -1,17 +0,0 @@
# 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 {
backend "pg" {}
}

View file

@ -1,27 +0,0 @@
# 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_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

@ -1,19 +0,0 @@
# 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_resourcemanager_project" "this" {
parent_container_id = var.stackit_organization_id
name = var.stackit_project_name
owner_email = var.stackit_org_admin
}

View file

@ -1,56 +0,0 @@
# Phase 1: Example Infrastructure
This module contains the core infrastructure configuration. It uses the `pg` backend to store state and enforce state locks via the STACKIT PostgreSQL Flex database provisioned in Phase 0.
## Implementation Steps
1. Create a `backend.conf` file in this directory and define the connection string using the credentials generated by the bootstrap module:
```ini
conn_str = "postgres://tf_state_user:<PASSWORD>@<STACKIT_INSTANCE_HOST>:5432/terraform_state?sslmode=require"
```
2. Initialize Terraform and bind it to the remote PostgreSQL backend
```sh
terraform init -backend-config=backend.conf
```
3. Provision the infrastructure or run the lock validation script:
```sh
chmod +x ./scripts/validate_lock.sh
./scripts/validate_lock.sh
```
## Log Output
```sh
➜ 01-example-project ~ ./scripts/test-state-lock.sh
[INFO] Initiating background 'terraform apply' to acquire the state lock...
[INFO] Attempting concurrent 'terraform plan'...
[INFO] ------------------------------------------------------------------
│ Error: Error acquiring the state lock
│ Error message: Already locked for workspace creation: default
│ Lock Info:
│ ID: XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXXXXXX
│ Path:
│ Operation: OperationTypePlan
│ Who: XXXX
│ Version: 1.14.9
│ Created: 2026-06-25 16:21:59.636986 +0000 UTC
│ Info:
│ Terraform acquires a state lock to protect the state from being written
│ by multiple users at the same time. Please resolve the issue above and try
│ again. For most commands, you can disable locking with the "-lock=false"
│ flag, but this is not recommended.
[INFO] ------------------------------------------------------------------
[SUCCESS] Concurrent operation rejected. State locking is active and functional.
[INFO] Waiting for the background 'terraform apply' process to terminate...
[INFO] Evaluation complete. Cleaning up temporary logs...
```

View file

@ -1,44 +0,0 @@
#!/bin/bash
# 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.
set -u
echo "[INFO] Initiating background 'terraform apply' to acquire the state lock..."
# Redirecting output to avoid console clutter during the concurrent test
terraform apply -auto-approve > apply_bg.log 2>&1 &
APPLY_PID=$!
echo "[INFO] Attempting concurrent 'terraform plan'..."
echo "[INFO] ------------------------------------------------------------------"
# Disable exit-on-error to capture the expected failure code
set +e
terraform plan
PLAN_EXIT_CODE=$?
set -e
echo "[INFO] ------------------------------------------------------------------"
if [ $PLAN_EXIT_CODE -ne 0 ]; then
echo "[SUCCESS] Concurrent operation rejected. State locking is active and functional."
else
echo "[ERROR] Concurrent operation succeeded. State locking failed or is misconfigured."
fi
echo "[INFO] Waiting for the background 'terraform apply' process to terminate..."
wait $APPLY_PID
echo "[INFO] Evaluation complete. Cleaning up temporary logs..."
rm -f apply_bg.log

View file

@ -1,31 +0,0 @@
# ---------------------------------------------------------------------------
# terraform.tfvars.example
#
# Copy this file into a project directory (e.g. 00-bootcamp/terraform.tfvars)
# and fill in your values. Do NOT commit terraform.tfvars to version control.
#
# Alternatively, export variables as environment variables:
# export TF_VAR_stackit_organization_id="<uuid>"
# ---------------------------------------------------------------------------
# --- STACKIT Identity --------------------------------------------------------
# Your STACKIT Organization ID.
# Portal: Organization > Settings > Organization ID
stackit_organization_id = "<your-organization-id>"
## Name of the bootstrapping project
stackit_project_name = "01-example-project"
# Email address of the STACKIT user set as project owner.
stackit_org_admin = "<your-admin@mail.com"
# --- Service Account ---------------------------------------------------------
# Path to the STACKIT service account key file.
# Download: STACKIT portal > Service Accounts > Keys > Create key
stackit_service_account_key_path = "./keys/service-account.json"
# --- Region ------------------------------------------------------------------
stackit_region = "eu01"
default_zone = "eu01-1"

View file

@ -1,9 +0,0 @@
# Maintainers
General maintainers:
- Tim Reibe (<tim.reibe@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

@ -1,12 +0,0 @@
# STACKIT Terraform PostgreSQL Backend with State Locking
This repository demonstrates how to configure STACKIT PostgreSQL Flex as a Terraform backend to enable remote state storage and native state locking.
To resolve the circular dependency of provisioning a state backend using Terraform, the deployment is split into two isolated stages:
1. **`00-bootstrap/`**: Provisions the backend infrastructure (PostgreSQL Flex instance, database and service user).
2. **`01-example/`**: Represents the primary infrastructure, utilizing the provisioned PostgreSQL database as its remote backend.
---
**⚠️ Security Notice:** The PostgreSQL Flex instance in `00-bootstrap/` is configured with an open ACL (`0.0.0.0/0`) for development convenience. Before deploying to production, restrict the ACL to your specific egress IP ranges to prevent the database from being accessible via the public internet.