examples(tf-pg-backend): added terraform pg backend with state lock #39

Merged
mauritz.uphoff merged 4 commits from examples/terraform-pg-state-locking into main 2026-06-26 09:20:07 +00:00
19 changed files with 526 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,27 @@
# 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

@ -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 @@
# 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 = [
"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

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

@ -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 = "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,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 "pg" {}
}

View file

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

@ -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,56 @@
# 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

@ -0,0 +1 @@
conn_str=""

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