Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 13 additions & 29 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,59 +1,43 @@
# Local .terraform directories
# OpenTofu / Terraform working files
.terraform/

# .tfstate files
*.tfstate
*.tfstate.*

# Crash log files
.terraform.tfstate.lock.info
crash.log
crash.*.log
*tfplan*

# Exclude all .tfvars files, which are likely to contain sensitive data, such as
# password, private keys, and other secrets. These should not be part of version
# control as they are data points which are potentially sensitive and subject
# to change depending on the environment.
# *.tfvars
# *.tfvars.json

# Ignore override files as they are usually used to override resources locally and so
# are not checked in
# Local overrides
override.tf
override.tf.json
*_override.tf
*_override.tf.json

# Ignore transient lock info files created by terraform apply
.terraform.tfstate.lock.info

# Include override files you do wish to add to version control using negated pattern
# !example_override.tf

# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan
# example: *tfplan*
*tfplan*
# Provider lock file
.terraform.lock.hcl

# Ignore CLI configuration files
# CLI configuration
.terraformrc
terraform.rc

.terraform.lock.hcl

# Local AI/model-serving credentials and generated semantic cache
# Local credentials and agent state
openai.token
.env
.env.local
.cache/
.agents
.claude

# Local persistent E2E runtime variables (canonical for this troubleshooting phase)
# Local E2E runtime variables
src/config/e2e.tfvars
src/config/e2e-bootstrap.override.tfvars

# Cached OPNsense API key created by the firewall bootstrap
.firewall-api-credentials.json
src/.firewall-api-credentials.json

# Ignore QEMU disk images (firewall)
# QEMU disk images (firewall)
*.qcow2

# macOS
.DS_Store
18 changes: 15 additions & 3 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,18 @@ repos:
rev: v2.2.2
hooks:
- id: tofu_fmt
- id: tofu_docs
args:
- --args=--config=.terraform-docs.yml

- repo: local
hooks:
- id: terraform-docs
name: terraform-docs
entry: >-
mise exec -- terraform-docs markdown table
--recursive
--recursive-path modules
--output-file README.md
--output-mode inject
src
language: system
pass_filenames: false
files: ^src/.*\.(tf|tofu|terraform\.lock\.hcl)$
3 changes: 1 addition & 2 deletions CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1,2 +1 @@
* @lweberru @mahauber @simpe00
docs/* @lweberru
* @lweberru @mahauber @simpe00 @dweezl
65 changes: 65 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# Contributing

Thanks for taking the time to contribute. This document describes the local setup, the conventions this repository follows, and what a pull request is expected to contain.

## Local setup

Tool versions are pinned in `mise.toml`. Install them once:

```bash
mise install
```

That provides OpenTofu, the STACKIT CLI, `terraform-docs` and `pre-commit` at the versions this repository is tested against.

Install the Git hooks:

```bash
pre-commit install
```

The hooks run `tofu fmt` and regenerate the module documentation. If a hook rewrites a file, stage the result and commit again.

The documentation hook invokes `terraform-docs` through `mise exec`, so it picks up the version pinned in `mise.toml` and also works when Git is started from an editor that does not have the mise environment loaded.

Deploying against a real STACKIT organization additionally requires a service account key. `mise.toml` expects it at `~/.stackit/credentials.json`; see [docs/getting-started.md](docs/getting-started.md).

## Before you open a pull request

```bash
cd src
tofu fmt -recursive
tofu init -backend=false
tofu validate
tofu test
```

`tofu test` runs the three flavor test suites in `src/tests/` as plan-only runs. They reach the STACKIT API for validation but create nothing.

## Conventions

**Terraform layout.** Every module keeps its `terraform` block in `terraform.tf`, its variables in `variables.tf` and its outputs in `outputs.tf`. Resource files carry a numeric prefix when reading order matters, and each group of resources is introduced by a section header box.

**Provider versions.** The root module in `src/terraform.tf` pins every provider to an exact version. Child modules under `src/modules/` declare only a minimum with `>=`. Never add an upper bound in a child module; it silently blocks the root from upgrading.

**Naming.** `snake_case` for every block label, variable, output and local. Resource names do not repeat the resource type.

**Variables and outputs.** Every variable needs a `type` and a `description`. Every output needs a `description` before its `value`.

**Comments.** A comment explains a constraint the code cannot show, such as a provider bug or an OPNsense API quirk. Anything that describes what the code does or how the architecture fits together belongs in [docs/architecture.md](docs/architecture.md).

**Documentation.** Module READMEs are generated. Do not edit the block between the `BEGIN_TF_DOCS` and `END_TF_DOCS` markers by hand; run the pre-commit hook instead.

**Example configuration.** The tfvars files in `src/config/` are documentation. Keep them on placeholder values such as `owner@example.com` and a zero UUID for the organization. Never commit an identifier from a real environment.

## Commits and pull requests

Commit messages follow [Conventional Commits](https://www.conventionalcommits.org/), with the module as the optional scope:

```
feat(connectivity): add active/passive CARP support
fix(audit-logs): add HTTPS scheme to URLs
docs: clarify firewall bootstrap steps
```

Open the pull request against `main`. Describe what changes for a consumer of the landing zone, and say which flavor you tested against. CI runs formatting, validation, TFLint and the test suites; all of them have to pass before review.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ The STACKIT Landing Zone Accelerator provides a comprehensive Terraform-based fr

## 🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.
Contributions are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the local setup, the conventions this repository follows, and what a pull request should contain.

## 📄 License

Expand Down
10 changes: 9 additions & 1 deletion docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ A CARP virtual IP on the LAN — `.6` of `lan_network_range` by default — repl
Two things are pushed outside the OPNsense provider, because it only ever talks to the primary:

- `modules/connectivity/scripts/configure-ha.sh` writes the node-local half into each appliance during apply: the CARP VIP, `advskew`, the pfsync peer, and the XMLRPC sync target. These are exactly the settings the config sync does not replicate. It authenticates with the appliance login, so it works on the backup, which has no API key and never needs one.
- `modules/firewall-config/scripts/sync-ha-peer.sh` replicates the policy to the backup after every change. OPNsense's XMLRPC sync only fires on GUI saves, never on API writes, so without this the backup runs an empty ruleset and black-holes traffic the moment it becomes CARP master.
- `modules/firewall-config/scripts/sync-ha-peer.sh` replicates the policy to the backup after every change. OPNsense's XMLRPC sync only fires on GUI saves, never on API writes, so without this the backup runs an empty ruleset and black-holes traffic the moment it becomes CARP master. A failover test without the policy sync measured an 86 second outage, against about one second with it.

The `fw_cluster` alias and the `allow-fw-carp` / `allow-fw-pfsync` rules that let the two nodes talk are injected into `firewall_config` automatically when HA is on, sequenced at 90 and 91 — ahead of every rule the example policy ships. They are not in the `.tfvars` because a `block-lz-to-lz` rule placed above them silently kills the election and the state sync.

Expand Down Expand Up @@ -160,6 +160,14 @@ The `corporate` flag is the key switch:

Source: `src/modules/landing-zone/`

### Landing Zone on Kubernetes

Every entry in `landing_zone_namespace_services` gets a tenant slice of the shared platform cluster: its own namespace, a scoped service account with a Role limited to that namespace, and optionally a Kyverno policy that blocks direct Secret management, so credentials have to come through the Secrets Manager.

The sample workload behind `sample_load` is demo material and not part of the landing zone contract. It deploys a pod, a Gateway API route and a DNS record that together prove the path from the internet to a namespace works. Remove it once real workloads move in.

Source: `src/_landing-zone-kubernetes.tf`, `src/modules/namespace-service-demo/`

### DevOps (optional)

Provisions a separate DevOps project (`<company_code>-pltfm-devops-prod`) with a managed Git instance (Gitea or equivalent, controlled by `git_flavor`). Network access can be restricted to specific CIDR ranges via `allowed_network_ranges`. Disabled by default: enable by setting the `devops` variable.
Expand Down
2 changes: 1 addition & 1 deletion docs/diagrams/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -151,4 +151,4 @@ Semantic decision data is stored in `.semantics/diagram.lock.json` (configurable
- `theme` (default): grouped architecture themes per module.
- `full`: resource-level detail.

Generated diagrams include semantic styling and a legend (Networking, Compute, Kubernetes, Storage, Access/RBAC).
Generated diagrams include semantic styling and a legend (Networking, Compute, Kubernetes, Storage, Access/RBAC).
2 changes: 1 addition & 1 deletion docs/diagrams/scripts/generate_example_architecture.py
Original file line number Diff line number Diff line change
Expand Up @@ -1648,4 +1648,4 @@ def main() -> int:


if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
2 changes: 1 addition & 1 deletion docs/diagrams/scripts/validate_stackit_flavors.py
Original file line number Diff line number Diff line change
Expand Up @@ -276,4 +276,4 @@ def main() -> int:


if __name__ == "__main__":
raise SystemExit(main())
raise SystemExit(main())
2 changes: 1 addition & 1 deletion docs/getting-started.md
Original file line number Diff line number Diff line change
Expand Up @@ -371,4 +371,4 @@ resource "stackit_dns_record_set" "landing_zone_sample_gateway" {
}
```

This ensures a stable, Terraform-managed DNS path without external scripts until provider-native `gatewayApi` DNS extension support is available.
This ensures a stable, Terraform-managed DNS path without external scripts until provider-native `gatewayApi` DNS extension support is available.
Loading
Loading