WIP: STACKIT Git as Terraform Backend with State Locking and Versioning #45

Draft
tim.reibe wants to merge 2 commits from examples/terraform-git-backend into main
19 changed files with 503 additions and 0 deletions

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,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.
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

@ -0,0 +1,29 @@
# 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
enable_beta_resources = true
}

View file

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

@ -0,0 +1,24 @@
# 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 Git Instance
resource "stackit_git" "this" {
project_id = stackit_resourcemanager_project.this.project_id
name = "tf-states"
acl = [
# WARNING: Open ACL is for development only. Restrict to your specific egress IP ranges in production.
"0.0.0.0/0"
]
flavor = "git-10"
}

View file

@ -0,0 +1,18 @@
# 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 "git_instance_url" {
description = "Git Instance URL"
value = stackit_git.this.url
}

View file

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

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

@ -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,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.
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

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

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_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
enable_beta_resources = true
}

View file

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

@ -0,0 +1,53 @@
# Phase 1: Example Infrastructure
This module contains the core infrastructure configuration. It uses the `http` backend to store state and enforce state locks via the STACKIT Git instance provisioned in Phase 0.
## Implementation Steps
1. Create a `backend.conf` from `backend.conf.example` and define the required variables using the Git Instance URL generated by the bootstrap module and your personal access token with permissions to write packages (`write:package`).
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

@ -0,0 +1,12 @@
# Replace <GIT_INSTANCE_URL>, <OWNER> (User or Organization), and <STATE_NAME>
address = "<GIT_INSTANCE_URL>/api/packages/<OWNER>/terraform/state/<STATE_NAME>"
lock_address = "<GIT_INSTANCE_URL>/api/packages/<OWNER>/terraform/state/<STATE_NAME>/lock"
unlock_address = "<GIT_INSTANCE_URL>/api/packages/<OWNER>/terraform/state/<STATE_NAME>/lock"
lock_method = "POST"
unlock_method = "DELETE"
retry_wait_min = 5
# STACKIT Git credentials
username = "<YOUR_USERNAME>"
password = "<YOUR_ACCESS_TOKEN>"

View file

@ -0,0 +1,44 @@
#!/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

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

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

@ -0,0 +1,12 @@
# STACKIT Terraform Git Backend with State Locking and Versioning
This repository demonstrates how to configure STACKIT Git as a Terraform backend to enable remote state storage with native state locking and state versioning.
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 (Git instance).
2. **`01-example/`**: Represents the primary infrastructure, utilizing the provisioned Git instance as its remote backend.
---
**⚠️ Security Notice:** The Git 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 Git instance from being accessible via the public internet.