From ea3f1895641ee60be3fe9af52987d0b93ec4f106 Mon Sep 17 00:00:00 2001 From: Matthiator Date: Thu, 23 Apr 2026 05:43:54 +0200 Subject: [PATCH 1/9] refactor stackit edge cloud modules and provisioning docs --- .../1_getting_started/providers/stackit.md | 250 +----------------- .../stackit_provisioning_edgecloud.md | 239 +++++++++++++++++ .../providers/stackit_provisioning_ske.md | 157 +++++++++++ .../providers/stackit_terraform_bootstrap.md | 91 +++++++ docs/mkdocs.yml | 6 +- go-binary/cmd/generate_test.go | 98 +++++++ .../infrastructure/env.auto.tfvars.tplt | 79 +++++- .../example/infrastructure/main.tf.tplt | 59 ++++- .../example/infrastructure/outputs.tf.tplt | 38 ++- .../example/infrastructure/variables.tf.tplt | 104 ++++++-- .../terraform/providers/stackit/README.md | 146 +++++++++- .../stackit/modules/edge-cluster/README.md | 74 ------ .../stackit/modules/edge-cluster/main.tf | 30 --- .../stackit/modules/edge-cluster/network.tf | 47 ---- .../stackit/modules/edge-cluster/outputs.tf | 14 - .../stackit/modules/edge-cluster/variables.tf | 55 ---- .../stackit/modules/edge-hosts/README.md | 45 ++++ .../stackit/modules/edge-hosts/main.tf | 89 +++++++ .../stackit/modules/edge-hosts/outputs.tf | 24 ++ .../{edge-cluster => edge-hosts}/terraform.tf | 0 .../stackit/modules/edge-hosts/variables.tf | 92 +++++++ .../stackit/modules/edge-image/README.md | 24 ++ .../{image_upload => edge-image}/main.tf | 6 +- .../{image_upload => edge-image}/outputs.tf | 2 +- .../{image_upload => edge-image}/terraform.tf | 0 .../stackit/modules/edge-image/variables.tf | 49 ++++ .../stackit/modules/edge-instance/README.md | 29 ++ .../stackit/modules/edge-instance/main.tf | 37 +++ .../stackit/modules/edge-instance/outputs.tf | 14 + .../modules/edge-instance/terraform.tf | 9 + .../modules/edge-instance/variables.tf | 55 ++++ .../stackit/modules/image_upload/README.md | 62 ----- .../stackit/modules/image_upload/variables.tf | 44 --- 33 files changed, 1442 insertions(+), 626 deletions(-) create mode 100644 docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md create mode 100644 docs/content/1_getting_started/providers/stackit_provisioning_ske.md create mode 100644 docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/README.md delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/main.tf delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/network.tf delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/outputs.tf delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/variables.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/outputs.tf rename go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/{edge-cluster => edge-hosts}/terraform.tf (100%) create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md rename go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/{image_upload => edge-image}/main.tf (53%) rename go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/{image_upload => edge-image}/outputs.tf (60%) rename go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/{image_upload => edge-image}/terraform.tf (100%) create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/terraform.tf create mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/README.md delete mode 100644 go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/variables.tf diff --git a/docs/content/1_getting_started/providers/stackit.md b/docs/content/1_getting_started/providers/stackit.md index 58f96dfe..202d5c05 100644 --- a/docs/content/1_getting_started/providers/stackit.md +++ b/docs/content/1_getting_started/providers/stackit.md @@ -1,247 +1,11 @@ # STACKIT Cloud and Edge setup -We recommend using Terraform to provisioning your Kubernetes and additional infrastructure like a Secret Manager instance. -For this purposes we provide the necessary terraform configuration and modules. +We recommend using Terraform to provision your Kubernetes and additional infrastructure like a Secret Manager instance. +For this purpose we provide the necessary Terraform configuration and modules. -!!! warning - In kubara version `0.2.0`, this step does not merge user-customized Terraform values and will overwrite existing Terraform files. - -Generate Terraform modules: - -```bash -kubara generate --terraform -``` - -Commit and push the generated files to your Git repository. - -!!! info - You will need access to the STACKIT API. Setup instructions are available in the [Terraform provider documentation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) & [STACKIT Docs](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/how-tos/manage-service-accounts/). - Make sure your created Service Account has Project Owner permissions. - -## 1. Terraform Bootstrap - -Before the first `terraform init`, prepare and load your environment variables: - -```bash -cd customer-service-catalog/terraform/ -cp set-env-changeme.sh set-env.sh -``` -Set at least `STACKIT_SERVICE_ACCOUNT_KEY_PATH` in `set-env.sh` / `set-env.ps1` before sourcing. -```bash -source set-env.sh -# or for PowerShell -# cp set-env-changeme.ps1 set-env.ps1 -# . .\set-env.ps1 -``` - - -Then navigate to: - -```bash -cd bootstrap-tfstate-backend -``` - -Run: - -=== "Terraform" - - ```bash - terraform init - terraform plan - terraform apply - ``` - -=== "Tofu" - - ```bash - tofu init - tofu plan - tofu apply - ``` - -Use the output to configure Terraform backend credentials: - -=== "Terraform" - - ```bash - terraform output debug | grep -E "credential_access_key|credential_secret_access_key" - ``` - -=== "Tofu" - - ```bash - tofu output debug | grep -E "credential_access_key|credential_secret_access_key" - ``` - -You can set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in `set-env.sh` / `set-env.ps1` and source the file again, or export them directly: - -```bash -export AWS_ACCESS_KEY_ID="" -export AWS_SECRET_ACCESS_KEY="" -``` - -## 2. Provisioning Infrastructure - -!!! warning - If you do not intend to use OAuth2 Proxy you can ignore some of the steps in the guide below pertaining to it, but might encounter some issues later. - For more infos please look at our [FAQ](../../6_reference/faq.md#what-happens-when-oauth2-proxy-is-disabled). - -Then proceed to: - -```bash -cd ../infrastructure -``` - -Run: - -=== "Terraform" - - ```bash - terraform init - terraform plan - ``` - -=== "Tofu" - - ```bash - tofu init - tofu plan - ``` - -Check the values generated in `env.auto.tfvars`, which is [automatically applied in your Terraform deployment.](https://developer.hashicorp.com/terraform/language/values/variables#assign-values-to-variables) - -=== "Terraform" - - ```bash - terraform apply - ``` - -=== "Tofu" - - ```bash - tofu apply - ``` - -This creates the Kubernetes cluster and all required infrastructure. - -Export your kubeconfig: - -=== "Terraform" - - ```bash - # change command accordingly to your needs. For example change the name of your kubeconfig, to not overwrite any files - terraform output -raw kubeconfig_raw > $HOME/.kube/kubara.yaml - ``` - -=== "Tofu" - - ```bash - # change command accordingly to your needs. For example change the name of your kubeconfig, to not overwrite any files - tofu output -raw kubeconfig_raw > $HOME/.kube/kubara.yaml - ``` - -Keep this `kubara.yaml` local and do not commit it to Git. - -Review Terraform outputs: - -=== "Terraform" - - ```bash - terraform output - ``` - -=== "Tofu" - - ```bash - tofu output - ``` - -Use Terraform outputs to update values in `config.yaml` where needed (for example, on Edge: `privateLoadBalancerIP` and `publicLoadBalancerIP`). -Do **not** export Secrets Manager credentials into `.env`; these provider-specific `.env` variables were removed. - -Sensitive output example: - -=== "Terraform" - - ```bash - terraform output vault_user_ro_password_b64 - ``` - -=== "Tofu" - - ```bash - tofu output vault_user_ro_password_b64 - ``` - -If you use OAuth2, create a GitHub application as shown [here](/2_managing_your_platform/add_sso.md). - -If you want Terraform to create OAuth2-related Vault entries: - -* Use `set-env.sh` / `set-env.ps1` for `TF_VAR_*` in `customer-service-catalog/terraform//` -* `TF_Var_image_pull_secret` will already be set by kubara with what is present in the .env -* In `customer-service-catalog/terraform//infrastructure`, copy `secrets.tf-example` to `oauth2-secrets.tf` and adjust values if needed - -Load the variables and apply: - -=== "Terraform" - - ```bash - cp secrets.tf-example oauth2-secrets.tf - source ../set-env.sh - # or for PowerShell - # Copy-Item secrets.tf-example oauth2-secrets.tf - . ..\set-env.ps1 - terraform apply - ``` - -=== "Tofu" - - ```bash - cp secrets.tf-example oauth2-secrets.tf - source ../set-env.sh - # or for PowerShell - # Copy-Item secrets.tf-example oauth2-secrets.tf - . ..\set-env.ps1 - tofu apply - ``` - -!!! warning - You need to set these environment variables again before re-applying Terraform if they are not persisted in your shell/session setup. - -To clean up: - -=== "Terraform" - - ```bash - terraform state rm \ - vault_kv_secret_v2.image_pull_secret \ - vault_kv_secret_v2.oauth2_creds \ - vault_kv_secret_v2.argo_oauth2_creds \ - vault_kv_secret_v2.grafana_oauth2_creds \ - random_password.oauth2_cookie_secret - ``` - -=== "Tofu" - - ```bash - tofu state rm \ - vault_kv_secret_v2.image_pull_secret \ - vault_kv_secret_v2.oauth2_creds \ - vault_kv_secret_v2.argo_oauth2_creds \ - vault_kv_secret_v2.grafana_oauth2_creds \ - random_password.oauth2_cookie_secret - ``` - -## 3. STACKIT Edge-Specific Notes - -The provisioning steps remain the same. The only difference lies in the Terraform output: - -* You'll retrieve additional values like `privateLoadBalancerIP` and `publicLoadBalancerIP` -* These need to be added to `config.yaml` - -You must manually create the Kubernetes cluster via the cloud portal. This will be automated in the future. - -Now continue with the generic guide on the [Bootstrap Your Own Platform](../bootstrapping.md) page. - ---- +Follow this order: +1. Start with [Terraform Bootstrap](stackit_terraform_bootstrap.md). +2. Then choose your cluster type provisioning path: + - [SKE](stackit_provisioning_ske.md) + - [Edge Cloud](stackit_provisioning_edgecloud.md) diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md new file mode 100644 index 00000000..d2661f33 --- /dev/null +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -0,0 +1,239 @@ +# STACKIT Provisioning Infrastructure (Edge Cloud) + +Edge Cloud setups are usually highly individual. +Some teams run only STACKIT VMs, some run only bare metal, and others use mixed environments (VM + bare metal, even across different clouds). + +kubara documents one possible integration path and examples, but your target setup may require different operational steps. + +STEC (STACKIT Edge Cloud) can be operated through UI, CLI, or API. +All paths are valid: + +- UI is often the fastest manual path for image and cluster creation. +- CLI/API with manifests is better for reproducibility and change history (for example in Git). + +Choose the operating model that fits your team. kubara examples use manifest-based flows for traceability, but this is not mandatory. + +## Optional Terraform modules for Edge + +Terraform modules for Edge are optional and can be combined as needed: + +* `edge_instance` is optional. `edge_instance.create` acts as module toggle: + * `true`: create a new instance + * `false` + `instance_id`: reuse existing instance + * `false` + empty `instance_id`: skip module +* `edge_image` is optional. `edge_image.create` toggles image upload; upload runs only when `local_file_path` is set. + In many setups, you can skip `edge_image` and reuse an existing image ID via `edge_hosts.image_id`. +* `edge_hosts` is optional. `edge_hosts.create` toggles host provisioning; host provisioning requires `edge_hosts.image_id` and at least one entry in `nodes`. +* `edge_hosts` does not auto-link to `edge_image`. Set `edge_hosts.image_id` explicitly: + * use an existing image ID, or + * upload via `edge_image` first, then copy `edge_uploaded_image_id` output into `edge_hosts.image_id`. + +If your Edge platform is already provisioned externally, you can skip Terraform Edge modules completely and continue with kubara Helm/GitOps workflows only. + +`EdgeImage` and `EdgeCluster` resources are managed via Kubernetes API (`kubectl`) and intentionally not part of the Terraform state in this setup. + +Important distinction: + +* `EdgeImage` and `image-factory` versions describe STEC boot artifacts and profiles (for example Talos version and extensions). +* `edge_hosts.image_id` must be a STACKIT project image ID (Compute/IaaS image), not an `image-factory` version string. + +## Where to set `create = true/false` + +Set the module toggles in the generated cluster tfvars file: + +- `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` + +## Step-by-step sequence + +Before you start, go to: + +```bash +cd customer-service-catalog/terraform//infrastructure +``` + +1. Run `terraform init` and `terraform plan` (or `tofu init` / `tofu plan`). +2. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` (create or reuse). +3. Keep `edge_image.create = false` for the first apply. +4. Keep `edge_hosts.create = false` for the first apply (unless `edge_hosts.image_id` is already set and you want direct host provisioning). +5. Run first apply: + +=== "Terraform" + + ```bash + terraform apply + ``` + +=== "Tofu" + + ```bash + tofu apply + ``` + +6. Check whether a suitable STACKIT project image already exists. If yes, set `edge_hosts.image_id`, keep `edge_image.create = false`, then continue with step 10. +7. If no suitable image exists, create `EdgeImage` in your STEC instance via `kubectl` and wait for `Ready`. + If you plan to run Longhorn on this cluster, include `siderolabs/iscsi-tools` and `siderolabs/util-linux-tools` in the `EdgeImage` system extensions. +8. Read the generated artifact URL from `EdgeImage.status` and download it locally. +9. Set `edge_image.create = true` and set `edge_image.local_file_path` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` to that local artifact path. +10. Run apply and read `edge_uploaded_image_id` from Terraform output, for example `terraform output edge_uploaded_image_id`. +11. Set `edge_hosts.image_id` to that ID (or keep your existing image ID), set `edge_hosts.create = true`, and configure `edge_hosts.nodes`. +12. Run apply again to create hosts. + +=== "Terraform" + + ```bash + terraform apply + ``` + +=== "Tofu" + + ```bash + tofu apply + ``` + +13. Wait until the booted hosts are visible as `EdgeHost` in STEC. +14. Create `EdgeCluster` via `kubectl` and assign node roles (`controlplane` / `worker`). + +How to read image information: + +* List existing `EdgeImage` resources (STEC side): + +```bash +kubectl get edgeimages.edge.stackit.cloud +kubectl get edgeimage -o yaml +``` + +* List available `image-factory` Talos versions (STEC side): + +```bash +INSTANCE_REGION="eu01" +curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions +``` + + These versions are not directly usable as `edge_hosts.image_id`. + +* For Terraform host provisioning, you need a STACKIT project image ID for `edge_hosts.image_id`: + * copy an existing ID from your STACKIT project image list (UI), or + * use the Terraform upload module once and read: + +```bash +terraform output edge_uploaded_image_id +``` + +If you use an existing image for Longhorn and know the related `EdgeImage`, verify that these extensions are present in that image configuration: + +```bash +kubectl get edgeimage -o yaml +# check spec.schematic -> customization.systemExtensions.officialExtensions +# expected: siderolabs/iscsi-tools and siderolabs/util-linux-tools +``` + +## Why this order + +- `EdgeImage` is needed only when you do not already have a suitable STACKIT project image. +- Terraform `edge_image` upload needs a local artifact path. +- Terraform `edge_hosts` (if used) need an explicit `edge_hosts.image_id`. +- `EdgeCluster` should be created after hosts are registered as `EdgeHost`. +- You can source `edge_hosts.image_id` from an existing image or from `edge_uploaded_image_id` after a prior upload apply. +- In many setups, reusing an existing image ID is the default path; `edge_image` upload is only needed if you explicitly want to upload your own artifact. + +## Talos and Kubernetes version compatibility + +`EdgeImage` sets the Talos version used to build boot artifacts. +`EdgeCluster` sets Talos and Kubernetes versions for the cluster. + +According to STACKIT docs: + +* Talos version during cluster creation may differ from the boot image Talos version. +* If different, Talos automatically upgrades/downgrades during cluster creation. +* STEC validates that the chosen Kubernetes version is supported by the chosen Talos version. + +Recommendation: keep `EdgeImage` Talos version and `EdgeCluster` Talos version aligned whenever possible to reduce moving parts during bootstrap. + +Before applying manifests, verify which Talos image versions are currently available in your STEC region: + +```bash +INSTANCE_REGION="eu01" +curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions +``` + +Use one of the returned versions in both `EdgeImage.spec.talosVersion` and `EdgeCluster.spec.talos.version`. + +## Longhorn extension requirement + +If your kubara cluster will use Longhorn, your Talos image should include these system extensions: + +- `siderolabs/iscsi-tools` +- `siderolabs/util-linux-tools` + +Reason: Longhorn on Talos requires iSCSI tooling, and uses binaries from `util-linux-tools` (for example `fstrim`) for volume operations. +If your existing STACKIT project image was built from an `EdgeImage` profile that already includes these extensions, no extra rebuild is required. + +## `EdgeImage` example + +Use this to generate a Talos image in STEC: + +```yaml +apiVersion: edge.stackit.cloud/v1alpha1 +kind: EdgeImage +metadata: + name: kubara-edge-image + namespace: default +spec: + schematic: | + customization: + extraKernelArgs: [] + systemExtensions: + officialExtensions: + - siderolabs/iscsi-tools + - siderolabs/util-linux-tools + overlay: {} + talosVersion: v1.12.5-stackit.v1.7.1 +``` + +If you are not using Longhorn, keep only the extensions you actually need. + +## `EdgeCluster` example + +Use this after your hosts are registered as `EdgeHost`: + +```yaml +apiVersion: edge.stackit.cloud/v1alpha1 +kind: EdgeCluster +metadata: + name: kubara-cluster + namespace: default +spec: + nodes: + - edgeHost: + installDisk: /dev/vda + role: controlplane + - edgeHost: + installDisk: /dev/vda + role: worker + talos: + version: v1.12.5-stackit.v1.7.1 + kubernetes: + version: v1.30.2 +``` + +## Validate API schema on your instance + +`EdgeImage` and `EdgeCluster` schemas can evolve. Validate against your STEC instance before applying manifests: + +```bash +kubectl api-resources --api-group=edge.stackit.cloud +kubectl explain edgeimages.edge.stackit.cloud.spec +kubectl explain edgeclusters.edge.stackit.cloud.spec +``` + +## Official references + +* [Edge Cloud overview](https://docs.stackit.cloud/products/runtime/edge-cloud/) +* [Using the API](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-the-api/) +* [Creating images](https://docs.stackit.cloud/de/products/runtime/edge-cloud/getting-started/creating-images/) +* [Using extensions](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-extensions/) +* [Creating clusters](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/creating-clusters/) +* [Authentication](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/authentication/) +* [Longhorn Talos Linux support](https://longhorn.io/docs/1.11.0/advanced-resources/os-distro-specific/talos-linux-support/) + +Now continue with the generic guide on the [Bootstrap Your Own Platform](../bootstrapping.md) page. diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_ske.md b/docs/content/1_getting_started/providers/stackit_provisioning_ske.md new file mode 100644 index 00000000..f3177bea --- /dev/null +++ b/docs/content/1_getting_started/providers/stackit_provisioning_ske.md @@ -0,0 +1,157 @@ +# STACKIT Provisioning Infrastructure (SKE) + +!!! warning + If you do not intend to use OAuth2 Proxy you can ignore some of the steps in the guide below pertaining to it, but might encounter some issues later. + For more infos please look at our [FAQ](../../6_reference/faq.md#what-happens-when-oauth2-proxy-is-disabled). + +Proceed to: + +```bash +cd customer-service-catalog/terraform//infrastructure +``` + +Run: + +=== "Terraform" + + ```bash + terraform init + terraform plan + ``` + +=== "Tofu" + + ```bash + tofu init + tofu plan + ``` + +Check the values generated in `env.auto.tfvars`, which is [automatically applied in your Terraform deployment.](https://developer.hashicorp.com/terraform/language/values/variables#assign-values-to-variables) + +Apply: + +=== "Terraform" + + ```bash + terraform apply + ``` + +=== "Tofu" + + ```bash + tofu apply + ``` + +This creates the Kubernetes cluster and all required infrastructure. + +## Export kubeconfig + +=== "Terraform" + + ```bash + # change command accordingly to your needs. For example change the name of your kubeconfig, to not overwrite any files + terraform output -raw kubeconfig_raw > $HOME/.kube/kubara.yaml + ``` + +=== "Tofu" + + ```bash + # change command accordingly to your needs. For example change the name of your kubeconfig, to not overwrite any files + tofu output -raw kubeconfig_raw > $HOME/.kube/kubara.yaml + ``` + +Keep this `kubara.yaml` local and do not commit it to Git. + +## Review Terraform outputs + +=== "Terraform" + + ```bash + terraform output + ``` + +=== "Tofu" + + ```bash + tofu output + ``` + +Use Terraform outputs to update values in `config.yaml` where needed. +Do **not** export Secrets Manager credentials into `.env`; these provider-specific `.env` variables were removed. + +Sensitive output example: + +=== "Terraform" + + ```bash + terraform output vault_user_ro_password_b64 + ``` + +=== "Tofu" + + ```bash + tofu output vault_user_ro_password_b64 + ``` + +## Optional: OAuth2-related Vault entries via Terraform + +If you use OAuth2, create a GitHub application as shown [here](/2_managing_your_platform/add_sso.md). + +If you want Terraform to create OAuth2-related Vault entries: + +* Use `set-env.sh` / `set-env.ps1` for `TF_VAR_*` in `customer-service-catalog/terraform//` +* `TF_Var_image_pull_secret` will already be set by kubara with what is present in the .env +* In `customer-service-catalog/terraform//infrastructure`, copy `secrets.tf-example` to `oauth2-secrets.tf` and adjust values if needed + +Load the variables and apply: + +=== "Terraform" + + ```bash + cp secrets.tf-example oauth2-secrets.tf + source ../set-env.sh + # or for PowerShell + # Copy-Item secrets.tf-example oauth2-secrets.tf + . ..\set-env.ps1 + terraform apply + ``` + +=== "Tofu" + + ```bash + cp secrets.tf-example oauth2-secrets.tf + source ../set-env.sh + # or for PowerShell + # Copy-Item secrets.tf-example oauth2-secrets.tf + . ..\set-env.ps1 + tofu apply + ``` + +!!! warning + You need to set these environment variables again before re-applying Terraform if they are not persisted in your shell/session setup. + +To clean up: + +=== "Terraform" + + ```bash + terraform state rm \ + vault_kv_secret_v2.image_pull_secret \ + vault_kv_secret_v2.oauth2_creds \ + vault_kv_secret_v2.argo_oauth2_creds \ + vault_kv_secret_v2.grafana_oauth2_creds \ + random_password.oauth2_cookie_secret + ``` + +=== "Tofu" + + ```bash + tofu state rm \ + vault_kv_secret_v2.image_pull_secret \ + vault_kv_secret_v2.oauth2_creds \ + vault_kv_secret_v2.argo_oauth2_creds \ + vault_kv_secret_v2.grafana_oauth2_creds \ + random_password.oauth2_cookie_secret + ``` + +Now continue with the generic guide on the [Bootstrap Your Own Platform](../bootstrapping.md) page. diff --git a/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md b/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md new file mode 100644 index 00000000..b2b7b9fc --- /dev/null +++ b/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md @@ -0,0 +1,91 @@ +# STACKIT Terraform Bootstrap + +We recommend using Terraform to provision your Kubernetes and additional infrastructure like a Secret Manager instance. +For this purpose we provide the necessary Terraform configuration and modules. + +!!! warning + In kubara version `0.2.0`, this step does not merge user-customized Terraform values and will overwrite existing Terraform files. + +Generate Terraform modules: + +```bash +kubara generate --terraform +``` + +Commit and push the generated files to your Git repository. + +!!! info + You will need access to the STACKIT API. Setup instructions are available in the [Terraform provider documentation](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) & [STACKIT Docs](https://docs.stackit.cloud/platform/access-and-identity/service-accounts/how-tos/manage-service-accounts/). + Make sure your created Service Account has Project Owner permissions. + +## 1. Prepare Environment Variables + +Before the first `terraform init`, prepare and load your environment variables: + +```bash +cd customer-service-catalog/terraform/ +cp set-env-changeme.sh set-env.sh +``` + +Set at least `STACKIT_SERVICE_ACCOUNT_KEY_PATH` in `set-env.sh` / `set-env.ps1` before sourcing. + +```bash +source set-env.sh +# or for PowerShell +# cp set-env-changeme.ps1 set-env.ps1 +# . .\set-env.ps1 +``` + +## 2. Create Terraform Backend State + +Then navigate to: + +```bash +cd bootstrap-tfstate-backend +``` + +Run: + +=== "Terraform" + + ```bash + terraform init + terraform plan + terraform apply + ``` + +=== "Tofu" + + ```bash + tofu init + tofu plan + tofu apply + ``` + +Use the output to configure Terraform backend credentials: + +=== "Terraform" + + ```bash + terraform output debug | grep -E "credential_access_key|credential_secret_access_key" + ``` + +=== "Tofu" + + ```bash + tofu output debug | grep -E "credential_access_key|credential_secret_access_key" + ``` + +You can set `AWS_ACCESS_KEY_ID` and `AWS_SECRET_ACCESS_KEY` in `set-env.sh` / `set-env.ps1` and source the file again, or export them directly: + +```bash +export AWS_ACCESS_KEY_ID="" +export AWS_SECRET_ACCESS_KEY="" +``` + +## 3. Continue With Provisioning + +Next steps: + +- [Provisioning Infrastructure (SKE)](stackit_provisioning_ske.md) +- [Provisioning Infrastructure (Edge Cloud)](stackit_provisioning_edgecloud.md) diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index aa6d3e19..e42e2c5e 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -11,7 +11,11 @@ nav: - Prerequisites: 1_getting_started/prerequisites.md - Bootstrapping: 1_getting_started/bootstrapping.md - Providers: - - STACKIT: 1_getting_started/providers/stackit.md + - STACKIT: + - Terraform Bootstrap: 1_getting_started/providers/stackit_terraform_bootstrap.md + - Provisioning Infrastructure: + - SKE: 1_getting_started/providers/stackit_provisioning_ske.md + - Edge Cloud: 1_getting_started/providers/stackit_provisioning_edgecloud.md - Managing Your Platform: - Add Worker Cluster: 2_managing_your_platform/add_worker_cluster.md - Add Project: 2_managing_your_platform/add_app_project.md diff --git a/go-binary/cmd/generate_test.go b/go-binary/cmd/generate_test.go index 83722edc..71125931 100644 --- a/go-binary/cmd/generate_test.go +++ b/go-binary/cmd/generate_test.go @@ -434,6 +434,104 @@ func TestGenerateCmd_PlaceholderProviderFailsWithHint(t *testing.T) { assert.Contains(t, err.Error(), "supported providers: stackit") } +func TestGenerateCmd_EdgeTerraformArtifacts(t *testing.T) { + tempDir := t.TempDir() + + configPath := createTestConfig(t, tempDir, config.Cluster{ + Name: "edge-cluster", + Stage: "dev", + Type: "controlplane", + DNSName: "edge.example.com", + Terraform: &config.Terraform{ + Provider: "stackit", + ProjectID: "00000000-0000-0000-0000-000000000000", + KubernetesType: "edge", + KubernetesVersion: "1.34.0", + DNS: config.DNS{Name: "example.com", Email: "admin@example.com"}, + }, + ArgoCD: config.ArgoCD{ + Repo: config.RepoProto{ + HTTPS: &config.RepoType{ + Customer: config.Repository{URL: "https://github.com/example/customer", TargetRevision: "main"}, + Managed: config.Repository{URL: "https://github.com/example/managed", TargetRevision: "main"}, + }, + }, + }, + Services: config.Services{ + Argocd: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + CertManager: config.CertManagerService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}, ClusterIssuer: config.ClusterIssuer{Name: "letsencrypt-staging", Email: "admin@example.com", Server: "https://acme-staging-v02.api.letsencrypt.org/directory"}}, + ExternalDns: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + ExternalSecrets: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + KubePrometheusStack: config.PersistentService{GenericService: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}}, + Traefik: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + Kyverno: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + KyvernoPolicies: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + KyvernoPolicyReport: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + Loki: config.PersistentService{GenericService: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}}, + HomerDashboard: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + Oauth2Proxy: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + MetricsServer: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + MetalLb: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + Longhorn: config.GenericService{ServiceStatus: config.ServiceStatus{Status: config.StatusEnabled}}, + }, + }) + + envPath := createTestEnv(t, tempDir, envmap.EnvMap{ + ProjectName: "project-name", + ProjectStage: "project-stage", + DockerconfigBase64: "DockerConfig", + ArgocdWizardAccountPassword: "wizardpassword", + ArgocdGitHttpsUrl: "https://example.com", + ArgocdGitUsername: "CoolCapybara", + ArgocdGitPatOrPassword: "password", + ArgocdHelmRepoUrl: "https://example.com", + ArgocdHelmRepoUsername: "CoolCapybara", + ArgocdHelmRepoPassword: "password", + DomainName: "example.com", + }) + + app := createTestApp(cmd.NewGenerateCmd()) + args := []string{"kubara", "--config-file", configPath, "--work-dir", tempDir, "--env-file", envPath, "generate", "--terraform"} + err := app.Run(context.Background(), args) + require.NoError(t, err) + + infrastructureDir := filepath.Join(tempDir, "customer-service-catalog", "terraform", "edge-cluster", "infrastructure") + + envTfvars, err := os.ReadFile(filepath.Join(infrastructureDir, "env.auto.tfvars")) + require.NoError(t, err) + assert.Contains(t, string(envTfvars), "edge_instance = {") + assert.Contains(t, string(envTfvars), "edge_image = {") + assert.Contains(t, string(envTfvars), "edge_hosts = {") + assert.Contains(t, string(envTfvars), "create = true") + assert.Contains(t, string(envTfvars), "create = false") + assert.Contains(t, string(envTfvars), "create = false") + assert.Contains(t, string(envTfvars), "local_file_path = \"/path/to/edge-artifacts/openstack-amd64.raw\"") + assert.Contains(t, string(envTfvars), "image_id = \"\"") + assert.Contains(t, string(envTfvars), "nodes = [") + assert.Contains(t, string(envTfvars), "flavor = \"c1.4\"") + assert.NotContains(t, string(envTfvars), "edge_instance.enabled") + assert.NotContains(t, string(envTfvars), "edge_image.enabled") + assert.NotContains(t, string(envTfvars), "edge_hosts.enabled") + + mainTF, err := os.ReadFile(filepath.Join(infrastructureDir, "main.tf")) + require.NoError(t, err) + assert.Contains(t, string(mainTF), "module \"edge_instance\"") + assert.Contains(t, string(mainTF), "module \"edge_image\"") + assert.Contains(t, string(mainTF), "module \"edge_hosts\"") + assert.Contains(t, string(mainTF), "count = try(var.edge_image.create, false) && trimspace(try(var.edge_image.local_file_path, \"\")) != \"\" ? 1 : 0") + assert.Contains(t, string(mainTF), "count = try(var.edge_hosts.create, false) && length(try(var.edge_hosts.nodes, [])) > 0 ? 1 : 0") + assert.Contains(t, string(mainTF), "image_id = trimspace(try(var.edge_hosts.image_id, \"\"))") + assert.NotContains(t, string(mainTF), "module.edge_image[0].image_id") + assert.NotContains(t, string(mainTF), "check \"edge_hosts_image_id\"") + assert.NotContains(t, string(mainTF), "_fallback") + assert.NotContains(t, string(mainTF), "locals {") + + outputsTF, err := os.ReadFile(filepath.Join(infrastructureDir, "outputs.tf")) + require.NoError(t, err) + assert.Contains(t, string(outputsTF), "output \"edge_host_metadata\"") + assert.Contains(t, string(outputsTF), "output \"edge_uploaded_image_id\"") +} + // Helper function func createTestConfig(t *testing.T, dir string, clusters ...config.Cluster) string { diff --git a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt index 416e0bda..429631a0 100644 --- a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt +++ b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt @@ -41,11 +41,80 @@ node_pools = [ {{ end }} {{ if (eq .cluster.terraform.kubernetesType "edge") }} ### Edge -controlplane = { - flavor = "c1.4" - volume_performance_class = "storage_premium_perf1" - volume_size = 30 +edge_instance = { + # Module toggle: + # true = create a new Edge Cloud instance + # false + instance_id set = reuse existing instance + # false + empty instance_id = skip edge_instance module + create = true + instance_id = "" + plan = "preview" + display_name = "edge" + # Optional override. Leave empty to use the global region variable. + region = "" + description = "kubara edge instance for {{ .cluster.name }}-{{ .cluster.stage }}" } -ipv4_prefix = "10.0.50.0/24" +edge_image = { + # Module toggle: + # true = enable Terraform image upload module + # false = skip edge_image module + create = false + # Example local artifact path (downloaded from EdgeImage.status.artifactUrl): + local_file_path = "/path/to/edge-artifacts/openstack-amd64.raw" + name = "talos-{{ .cluster.name }}-{{ .cluster.stage }}" + disk_format = "raw" + min_disk_size = 30 + operating_system = "linux" + operating_system_distro = "talos" + operating_system_version = "v1.9.5" +} + +edge_hosts = { + # Module toggle: + # true = enable Terraform host provisioning module + # false = skip edge_hosts module + create = false + # Required when create=true and nodes are configured. + # You can set an existing image ID directly, e.g. "11111111-2222-3333-4444-555555555555". + # If you used edge_image module in a previous apply, copy `terraform output edge_uploaded_image_id` here. + image_id = "" + name_prefix = "edge-{{ .cluster.name }}-{{ .cluster.stage }}" + network_name = "edge-{{ .cluster.name }}-{{ .cluster.stage }}-network" + security_group_name = "edge-{{ .cluster.name }}-{{ .cluster.stage }}-sg" + ipv4_prefix = "10.0.50.0" + ipv4_prefix_length = 24 + ipv4_nameservers = ["1.1.1.1", "1.0.0.1", "8.8.8.8"] + ingress_tcp_ports = [80, 443] + common_labels = {} + + # Example only: adapt values to your environment. + # Hosts are created only when create=true. + nodes = [ + { + name = "edge-cp-1" + role = "controlplane" + flavor = "c1.4" + volume_size = 40 + volume_performance_class = "storage_premium_perf1" + availability_zone = "eu01-2" + assign_public_ip = true + labels = { + "node-role" = "controlplane" + } + }, + { + name = "edge-worker-1" + role = "worker" + flavor = "c1.4" + volume_size = 40 + volume_performance_class = "storage_premium_perf1" + availability_zone = "eu01-2" + assign_public_ip = true + labels = { + "node-role" = "worker" + } + } + ] +} {{ end }} diff --git a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt index 741cc76c..5ca6b4ce 100644 --- a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt +++ b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt @@ -50,23 +50,56 @@ module "ske_cluster" { {{ if (eq .cluster.terraform.kubernetesType "edge") }} -module "edgecloud_cluster_controlplane" { - source = "../../../../managed-service-catalog/terraform/modules/edge-cluster" +module "edge_instance" { + count = try(var.edge_instance.create, true) || trimspace(try(var.edge_instance.instance_id, "")) != "" ? 1 : 0 - name = "controlplane-${var.name}-${var.stage}" - project_id = var.project_id - ipv4_prefix = var.ipv4_prefix - edgecloud_image_id = var.edgecloud_image_id - controlplane = var.controlplane + source = "../../../../managed-service-catalog/terraform/modules/edge-instance" + + project_id = var.project_id + + create = var.edge_instance.create + instance_id = var.edge_instance.instance_id + plan_name = var.edge_instance.plan + display_name = trimspace(try(var.edge_instance.display_name, "")) + region = trimspace(try(var.edge_instance.region, "")) != "" ? trimspace(try(var.edge_instance.region, "")) : var.region + description = var.edge_instance.description +} + +module "edge_image" { + count = try(var.edge_image.create, false) && trimspace(try(var.edge_image.local_file_path, "")) != "" ? 1 : 0 + + source = "../../../../managed-service-catalog/terraform/modules/edge-image" + + project_id = var.project_id + name = trimspace(try(var.edge_image.name, "")) != "" ? trimspace(try(var.edge_image.name, "")) : "talos-${var.name}-${var.stage}" + local_file_path = var.edge_image.local_file_path + disk_format = var.edge_image.disk_format + min_disk_size = var.edge_image.min_disk_size + + operating_system = var.edge_image.operating_system + operating_system_distro = var.edge_image.operating_system_distro + operating_system_version = var.edge_image.operating_system_version } +module "edge_hosts" { + count = try(var.edge_hosts.create, false) && length(try(var.edge_hosts.nodes, [])) > 0 ? 1 : 0 + + source = "../../../../managed-service-catalog/terraform/modules/edge-hosts" -# module "image_upload" { -# source = "../../../../managed-service-catalog/terraform/modules/image_upload" + name = trimspace(try(var.edge_hosts.name_prefix, "")) + project_id = var.project_id + image_id = trimspace(try(var.edge_hosts.image_id, "")) + network_name = trimspace(try(var.edge_hosts.network_name, "")) + security_group_name = trimspace(try(var.edge_hosts.security_group_name, "")) -# project_id = var.project_id -# name = "talos-v1.9.5-stackit-edgecloud.v0.15.0" #"image-upload-${var.name}-${var.stage}" -# local_file_path = "openstack-amd64.raw" -# } + ipv4_prefix = var.edge_hosts.ipv4_prefix + ipv4_prefix_length = var.edge_hosts.ipv4_prefix_length + ipv4_nameservers = var.edge_hosts.ipv4_nameservers + ingress_tcp_ports = var.edge_hosts.ingress_tcp_ports + common_labels = var.edge_hosts.common_labels + + nodes = var.edge_hosts.nodes + +} {{ end }} diff --git a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt index 46f3bfad..3c1bfb1e 100644 --- a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt +++ b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt @@ -46,18 +46,38 @@ output "kubeconfig_raw" { {{ if (eq .cluster.terraform.kubernetesType "edge") }} ### Edge Cloud Instance -output "server_id" { - description = "ID of the created Edge Cloud instance" - value = module.edgecloud_cluster_controlplane.server_id +output "edge_instance_id" { + description = "ID of the Edge Cloud instance" + value = length(module.edge_instance) > 0 ? module.edge_instance[0].instance_id : null } -output "public_ip" { - description = "Public IP of the created Edge Cloud instance" - value = module.edgecloud_cluster_controlplane.public_ip +output "edge_frontend_url" { + description = "Frontend URL of the Edge Cloud instance (null when reusing an existing instance)" + value = length(module.edge_instance) > 0 ? module.edge_instance[0].frontend_url : null } -output "private_ip" { - description = "Private IP of the created Edge Cloud instance" - value = module.edgecloud_cluster_controlplane.private_ip +output "edge_status" { + description = "Status of the Edge Cloud instance (null when reusing an existing instance)" + value = length(module.edge_instance) > 0 ? module.edge_instance[0].status : null +} + +output "edge_uploaded_image_id" { + description = "ID of the image uploaded by edge_image module. Null when edge_image module is skipped." + value = length(module.edge_image) > 0 ? module.edge_image[0].image_id : null +} + +output "edge_host_metadata" { + description = "Per-node metadata (IDs and IPs). Empty map until edge hosts are provisioned." + value = length(module.edge_hosts) > 0 ? module.edge_hosts[0].host_metadata : {} +} + +output "edge_network_id" { + description = "ID of the shared edge host network. Null until edge hosts are provisioned." + value = length(module.edge_hosts) > 0 ? module.edge_hosts[0].network_id : null +} + +output "edge_security_group_id" { + description = "ID of the shared edge host security group. Null until edge hosts are provisioned." + value = length(module.edge_hosts) > 0 ? module.edge_hosts[0].security_group_id : null } {{ end }} diff --git a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt index bcb6ee42..d6b68dfa 100644 --- a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt +++ b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt @@ -23,12 +23,12 @@ variable "acls" { } variable "users" { - description = </infrastructure/env.auto.tfvars` + +In many setups, you can skip `edge_image` and reuse an existing image ID via `edge_hosts.image_id`. + +1. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` (create or reuse). +2. Keep `edge_image.create = false` for the first apply. +3. Keep `edge_hosts.create = false` for the first apply unless `edge_hosts.image_id` is already set and you want direct host provisioning. +4. Run first apply: + +```bash terraform apply ``` -๐ŸŽ‰ This provisions everything: +5. Check whether a suitable STACKIT project image already exists. If yes, set `edge_hosts.image_id`, keep `edge_image.create = false`, then continue with step 9. +6. If no suitable image exists, create an `EdgeImage` via `kubectl`, wait for `Ready`, and download the artifact. + If you plan to run Longhorn, include `siderolabs/iscsi-tools` and `siderolabs/util-linux-tools` in the `EdgeImage` system extensions. +7. Set `edge_image.create = true` and set `edge_image.local_file_path` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` to the downloaded artifact. +8. Run apply and read `edge_uploaded_image_id` from output (for example `terraform output edge_uploaded_image_id`). +9. Set `edge_hosts.image_id` to that ID (or keep your existing image ID), set `edge_hosts.create = true`, and configure `edge_hosts.nodes`. +10. Run second apply (if configured, `edge_hosts` creates VM/network-based hosts): + +```bash +terraform apply +``` + +11. Wait until hosts register as `EdgeHost`, then create `EdgeCluster` via `kubectl`. + +If your hosts are provisioned externally (for example bare metal, mixed environments, or multi-cloud), skip `edge_hosts` and use only the API-based `EdgeImage`/`EdgeCluster` workflow. + +How to read image information: + +- List existing `EdgeImage` resources (STEC side): + +```bash +kubectl get edgeimages.edge.stackit.cloud +kubectl get edgeimage -o yaml +``` + +- List available `image-factory` Talos versions (STEC side): + +```bash +INSTANCE_REGION="eu01" +curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions +``` + + These versions are not directly usable as `edge_hosts.image_id`. + +- For Terraform host provisioning, you need a STACKIT project image ID for `edge_hosts.image_id`: + - copy an existing ID from your STACKIT project image list (UI), or + - use the Terraform upload module once and read: + +```bash +terraform output edge_uploaded_image_id +``` + +If you use an existing image for Longhorn and know the related `EdgeImage`, verify that these extensions are present in that image configuration: + +```bash +kubectl get edgeimage -o yaml +# check spec.schematic -> customization.systemExtensions.officialExtensions +# expected: siderolabs/iscsi-tools and siderolabs/util-linux-tools +``` + +๐ŸŽ‰ In summary, Terraform provisions your selected infrastructure: -- Kubernetes (Edge or Public) +- Kubernetes (SKE) - DNS zone - IAM roles - Vault (Secrets Manager) - Secrets in Vault - Buckets -- Image uploads (optional) +- Edge Cloud instance (optional) +- Edge image upload (optional) +- Edge host VM/network provisioning (optional) - And more... --- +## ๐Ÿงญ Edge-Specific Notes + +Edge Cloud setups are usually individual. Depending on your environment, you might: + +- provision nodes as STACKIT VMs, +- run bare metal only, +- run a mixed model (VM + bare metal), +- or run hosts across multiple clouds. + +kubara shows one example path and the Edge Terraform modules are optional: + +- `edge_instance` is optional (`create` toggles the module; use `create = false` + `instance_id` to reuse an existing instance) +- `edge_image` is optional (`create` toggles the module; upload runs only if `local_file_path` is set) +- `edge_hosts` is optional (`create` toggles the module; host provisioning runs only if `nodes` is not empty) +- for `edge_hosts`, set `edge_hosts.image_id` explicitly (existing image or `edge_uploaded_image_id` from a prior upload apply) + +You can also skip Edge Terraform entirely and use only the kubara Helm/GitOps stack. + +`EdgeImage` and `EdgeCluster` are managed via Kubernetes API (`kubectl`) and are intentionally outside Terraform state in this setup. + +Important distinction: + +- `EdgeImage` and `image-factory` versions describe STEC boot artifacts and profiles. +- `edge_hosts.image_id` must be a STACKIT project image ID (Compute/IaaS image), not an `image-factory` version string. + +STEC (STACKIT Edge Cloud) can be managed through UI, CLI, or API: + +- UI is often the fastest manual path for image and cluster creation. +- CLI/API with manifests is better for reproducibility and audit trail (for example in Git). + +kubara examples use manifest-based workflows for traceability, but the operating model is your choice. + +When Longhorn is enabled on Talos-based Edge clusters, include these extensions in your `EdgeImage` manifest: + +```yaml +systemExtensions: + officialExtensions: + - siderolabs/iscsi-tools + - siderolabs/util-linux-tools +``` + +If your existing STACKIT project image was built from an `EdgeImage` profile that already includes these extensions, no extra rebuild is required. + +Official references: + +- [Edge Cloud overview](https://docs.stackit.cloud/products/runtime/edge-cloud/) +- [Using the API](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-the-api/) +- [Creating images](https://docs.stackit.cloud/de/products/runtime/edge-cloud/getting-started/creating-images/) +- [Using extensions](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-extensions/) +- [Creating clusters](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/creating-clusters/) +- [Longhorn Talos Linux support](https://longhorn.io/docs/1.11.0/advanced-resources/os-distro-specific/talos-linux-support/) + +--- + ## ๐ŸŒ Visual Overview ### Edge Cloud Architecture ![STACKIT Edge](images/edge-0.png) -In the picture above you can see what happening behind the scenes and which resources are created. The difference to the following public cloud architecture is that the Edge Cloud uses MetalLB for load balancing and does not require a public IP address. Also you create a server over terraform which be used to deploy the kubernetes cluster through providing or referencing a talos image. +In the picture above you can see what happening behind the scenes and which resources are created. For Edge, the actual cluster lifecycle (`EdgeImage` / `EdgeCluster`) is managed via Kubernetes API, while Terraform handles only the selected infrastructure parts (instance/image upload/hosts). ### Public Cloud (SKE) Architecture @@ -240,7 +374,7 @@ In the picture above you can see what happening behind the scenes and which reso ## ๐Ÿ”‘ Step 6 โ€“ Export Kubeconfig -Extract and save the kubeconfig: +Extract and save the kubeconfig (SKE): ```bash terraform output -json kubeconfig_raw | jq -r > ~/.kube/.yaml @@ -252,6 +386,8 @@ Then use it: KUBECONFIG=~/.kube/.yaml kubectl get nodes ``` +For Edge, Terraform outputs infrastructure metadata only (for example `edge_instance_id`, `edge_frontend_url`, `edge_status`, `edge_host_metadata`). + --- ## ๐Ÿ”— Resources diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/README.md deleted file mode 100644 index 325b5f52..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# STACKIT Edge Cloud Control Plane Module - -Terraform module to provision a control plane node on STACKIT Edge Cloud (the server, not the cluster self). -This includes networking, security groups, a boot volume from an image, and a public IP. - -## Usage - -module "edgecloud_cluster_controlplane" { -source = "../modules/edge" - -name = "controlplane-${var.name}-${var.stage}" -project_id = var.project_id -ipv4_prefix = var.ipv4_prefix -edgecloud_image_id = var.edgecloud_image_id -controlplane = { -flavor = "c1.4" -volume_size = 30 -volume_performance_class = "storage_premium_perf1" -} -} - - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >=1.9.3 | -| [stackit](#requirement\_stackit) | 0.90.0 | - -## Providers - -| Name | Version | -|------|---------| -| [stackit](#provider\_stackit) | 0.90.0 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -|------|------| -| [stackit_network.edgecloud-network](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/network) | resource | -| [stackit_network_interface.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/network_interface) | resource | -| [stackit_public_ip.public_ip](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/public_ip) | resource | -| [stackit_security_group.public_ip_sec_group](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/security_group) | resource | -| [stackit_security_group_rule.public_ip_sec_group_ingress_443](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/security_group_rule) | resource | -| [stackit_security_group_rule.public_ip_sec_group_ingress_80](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/security_group_rule) | resource | -| [stackit_server.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/server) | resource | -| [stackit_volume.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/volume) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [availability\_zone](#input\_availability\_zone) | Availability zone of the image. | `string` | `"eu01-1"` | no | -| [controlplane](#input\_controlplane) | Config for control plane nodes |
object({
flavor = string
volume_size = number
volume_performance_class = string
})
| n/a | yes | -| [edgecloud\_image\_id](#input\_edgecloud\_image\_id) | Edgecloud image ID. | `string` | n/a | yes | -| [ipv4\_nameservers](#input\_ipv4\_nameservers) | IPv4 nameservers for the edgecloud network. | `list(string)` |
[
"1.1.1.1",
"1.0.0.1",
"8.8.8.8"
]
| no | -| [ipv4\_prefix](#input\_ipv4\_prefix) | IPv4 prefix for the edgecloud network. | `string` | n/a | yes | -| [labels](#input\_labels) | Labels for the edgecloud cluster. | `map(string)` |
{
"role": "controlplane"
}
| no | -| [name](#input\_name) | Name of the edgecloud cluster. | `string` | n/a | yes | -| [project\_id](#input\_project\_id) | The StackIT ProjectID. | `string` | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| [private\_ip](#output\_private\_ip) | Private IP of the created Edge Cloud instance | -| [public\_ip](#output\_public\_ip) | Public IP of the created Edge Cloud instance | -| [server\_id](#output\_server\_id) | ID of the created Edge Cloud instance | - \ No newline at end of file diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/main.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/main.tf deleted file mode 100644 index e131113f..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/main.tf +++ /dev/null @@ -1,30 +0,0 @@ -resource "stackit_volume" "this" { - project_id = var.project_id - name = var.name - availability_zone = var.availability_zone - size = var.controlplane.volume_size - performance_class = var.controlplane.volume_performance_class - source = { - type = "image" - id = var.edgecloud_image_id - } -} - -resource "stackit_network_interface" "this" { - project_id = var.project_id - network_id = stackit_network.edgecloud-network.network_id - name = var.name - security_group_ids = [stackit_security_group.public_ip_sec_group.security_group_id] -} - -resource "stackit_server" "this" { - project_id = var.project_id - name = var.name - machine_type = var.controlplane.flavor - - boot_volume = { - source_type = "volume" - source_id = stackit_volume.this.volume_id - } - network_interfaces = [stackit_network_interface.this.network_interface_id] -} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/network.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/network.tf deleted file mode 100644 index b9d46dac..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/network.tf +++ /dev/null @@ -1,47 +0,0 @@ -resource "stackit_public_ip" "public_ip" { - project_id = var.project_id - network_interface_id = stackit_network_interface.this.network_interface_id - labels = var.labels - -} -resource "stackit_security_group" "public_ip_sec_group" { - project_id = var.project_id - name = var.name -} - -resource "stackit_security_group_rule" "public_ip_sec_group_ingress_80" { - project_id = var.project_id - security_group_id = stackit_security_group.public_ip_sec_group.security_group_id - direction = "ingress" - description = "allow ingress port 80" - protocol = { - name = "tcp" - } - port_range = { - min = 80 - max = 80 - } -} - -resource "stackit_security_group_rule" "public_ip_sec_group_ingress_443" { - project_id = var.project_id - security_group_id = stackit_security_group.public_ip_sec_group.security_group_id - direction = "ingress" - description = "allow ingress port 443" - protocol = { - name = "tcp" - } - port_range = { - min = 443 - max = 443 - } -} - - -resource "stackit_network" "edgecloud-network" { - project_id = var.project_id - name = var.name - ipv4_nameservers = var.ipv4_nameservers - ipv4_prefix = var.ipv4_prefix - ipv4_prefix_length = 24 -} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/outputs.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/outputs.tf deleted file mode 100644 index 882ff43d..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/outputs.tf +++ /dev/null @@ -1,14 +0,0 @@ -output "server_id" { - description = "ID of the created Edge Cloud instance" - value = stackit_server.this.server_id -} - -output "private_ip" { - description = "Private IP of the created Edge Cloud instance" - value = stackit_network_interface.this.ipv4 -} - -output "public_ip" { - description = "Public IP of the created Edge Cloud instance" - value = stackit_public_ip.public_ip.ip -} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/variables.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/variables.tf deleted file mode 100644 index dadedd60..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/variables.tf +++ /dev/null @@ -1,55 +0,0 @@ -variable "name" { - type = string - description = "Name of the edgecloud cluster." - -} - - -variable "project_id" { - type = string - description = "The StackIT ProjectID." -} - -variable "labels" { - type = map(string) - description = "Labels for the edgecloud cluster." - default = { - "role" = "controlplane" - } -} - -variable "edgecloud_image_id" { - type = string - description = "Edgecloud image ID." -} - - -variable "controlplane" { - description = "Config for control plane nodes" - type = object({ - flavor = string - volume_size = number - volume_performance_class = string - }) -} - - -variable "availability_zone" { - type = string - description = "Availability zone of the image." - default = "eu01-1" - -} - - -variable "ipv4_nameservers" { - type = list(string) - description = "IPv4 nameservers for the edgecloud network." - default = ["1.1.1.1", "1.0.0.1", "8.8.8.8"] - -} - -variable "ipv4_prefix" { - type = string - description = "IPv4 prefix for the edgecloud network." -} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md new file mode 100644 index 00000000..aa55509d --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md @@ -0,0 +1,45 @@ +# STACKIT Edge Hosts Module + +Terraform module to provision edge host infrastructure for one cluster: + +- shared network +- shared security group (+ ingress rules) +- one volume, NIC, and VM per node +- optional public IP per node + +## Usage + +```hcl +module "edge_hosts" { + source = "../modules/edge-hosts" + + name = "edge-demo" + project_id = var.project_id + # Use an existing image ID, e.g. from STACKIT project image list. + # You can also use `terraform output edge_uploaded_image_id` + # after running the optional edge_image upload module. + image_id = "11111111-2222-3333-4444-555555555555" + network_name = "edge-demo-network" + security_group_name = "edge-demo-sg" + ipv4_prefix = "10.0.50.0" + + nodes = [ + { + name = "edge-demo-cp-1" + role = "controlplane" + flavor = "c1.4" + volume_size = 30 + volume_performance_class = "storage_premium_perf1" + availability_zone = "eu01-1" + assign_public_ip = true + labels = {} + } + ] +} +``` + +## Outputs + +- `network_id`: shared network ID +- `security_group_id`: shared security group ID +- `host_metadata`: per-node IDs and IPs diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf new file mode 100644 index 00000000..e9047b32 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf @@ -0,0 +1,89 @@ +locals { + nodes_by_name = { + for node in var.nodes : node.name => node + } + + nodes_with_public_ip = { + for name, node in local.nodes_by_name : name => node + if node.assign_public_ip + } +} + +resource "stackit_network" "this" { + project_id = var.project_id + name = var.network_name + ipv4_nameservers = var.ipv4_nameservers + ipv4_prefix = var.ipv4_prefix + ipv4_prefix_length = var.ipv4_prefix_length +} + +resource "stackit_security_group" "this" { + project_id = var.project_id + name = var.security_group_name +} + +resource "stackit_security_group_rule" "ingress_tcp" { + for_each = toset([for p in var.ingress_tcp_ports : tostring(p)]) + + project_id = var.project_id + security_group_id = stackit_security_group.this.security_group_id + direction = "ingress" + description = "allow ingress tcp ${each.value}" + + protocol = { + name = "tcp" + } + + port_range = { + min = tonumber(each.value) + max = tonumber(each.value) + } +} + +resource "stackit_volume" "this" { + for_each = local.nodes_by_name + + project_id = var.project_id + name = "${var.name}-${each.key}-volume" + availability_zone = each.value.availability_zone + size = each.value.volume_size + performance_class = each.value.volume_performance_class + + source = { + type = "image" + id = var.image_id + } +} + +resource "stackit_network_interface" "this" { + for_each = local.nodes_by_name + + project_id = var.project_id + network_id = stackit_network.this.network_id + name = "${var.name}-${each.key}-nic" + + security_group_ids = [stackit_security_group.this.security_group_id] +} + +resource "stackit_server" "this" { + for_each = local.nodes_by_name + + project_id = var.project_id + name = each.value.name + machine_type = each.value.flavor + + boot_volume = { + source_type = "volume" + source_id = stackit_volume.this[each.key].volume_id + } + + network_interfaces = [stackit_network_interface.this[each.key].network_interface_id] +} + +resource "stackit_public_ip" "this" { + for_each = local.nodes_with_public_ip + + project_id = var.project_id + network_interface_id = stackit_network_interface.this[each.key].network_interface_id + labels = merge(var.common_labels, each.value.labels, { role = lower(each.value.role) }) +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/outputs.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/outputs.tf new file mode 100644 index 00000000..c4db3b36 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/outputs.tf @@ -0,0 +1,24 @@ +output "network_id" { + description = "ID of the shared edge host network." + value = stackit_network.this.network_id +} + +output "security_group_id" { + description = "ID of the shared edge host security group." + value = stackit_security_group.this.security_group_id +} + +output "host_metadata" { + description = "Per-host metadata including IDs, role, and assigned IP addresses." + value = { + for name, node in local.nodes_by_name : name => { + role = node.role + availability_zone = node.availability_zone + server_id = stackit_server.this[name].server_id + volume_id = stackit_volume.this[name].volume_id + network_interface_id = stackit_network_interface.this[name].network_interface_id + private_ip = stackit_network_interface.this[name].ipv4 + public_ip = try(stackit_public_ip.this[name].ip, null) + } + } +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/terraform.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/terraform.tf similarity index 100% rename from go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-cluster/terraform.tf rename to go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/terraform.tf diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf new file mode 100644 index 00000000..9fbd90b9 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf @@ -0,0 +1,92 @@ +variable "name" { + type = string + description = "Name prefix used for generated host resources." +} + +variable "project_id" { + type = string + description = "STACKIT project ID." +} + +variable "image_id" { + type = string + description = "Image ID used to create boot volumes for all hosts." + + validation { + condition = trimspace(var.image_id) != "" + error_message = "image_id must be set and must not be empty." + } +} + +variable "network_name" { + type = string + description = "Name of the shared network created for all edge hosts." +} + +variable "security_group_name" { + type = string + description = "Name of the shared security group attached to all host interfaces." +} + +variable "ipv4_prefix" { + type = string + description = "IPv4 network prefix (without mask) used for the host network." +} + +variable "ipv4_prefix_length" { + type = number + description = "IPv4 prefix length used for the host network." + default = 24 + + validation { + condition = var.ipv4_prefix_length >= 8 && var.ipv4_prefix_length <= 30 + error_message = "ipv4_prefix_length must be between 8 and 30." + } +} + +variable "ipv4_nameservers" { + type = list(string) + description = "List of nameservers for the host network." + default = ["1.1.1.1", "1.0.0.1", "8.8.8.8"] +} + +variable "ingress_tcp_ports" { + type = list(number) + description = "List of TCP ports opened on the shared security group." + default = [80, 443] +} + +variable "common_labels" { + type = map(string) + description = "Labels added to public IP resources for all nodes." + default = {} +} + +variable "nodes" { + description = "Edge hosts to provision." + type = list(object({ + name = string + role = string + flavor = string + volume_size = number + volume_performance_class = string + availability_zone = string + assign_public_ip = optional(bool, true) + labels = optional(map(string), {}) + })) + + validation { + condition = length(var.nodes) > 0 + error_message = "nodes must contain at least one host definition." + } + + validation { + condition = length(var.nodes) == length(toset([for node in var.nodes : node.name])) + error_message = "Each node name must be unique." + } + + validation { + condition = alltrue([for node in var.nodes : contains(["controlplane", "worker"], lower(node.role))]) + error_message = "Each node role must be either controlplane or worker." + } +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md new file mode 100644 index 00000000..7ed3c250 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md @@ -0,0 +1,24 @@ +# STACKIT Edge Image Module + +Terraform module to upload a local Edge image artifact to STACKIT. + +## Usage + +```hcl +module "edge_image" { + source = "../modules/edge-image" + + project_id = var.project_id + name = "talos-edge-image" + local_file_path = "./talos.raw" + min_disk_size = 30 + + operating_system = "linux" + operating_system_distro = "talos" + operating_system_version = "v1.9.5" +} +``` + +## Outputs + +- `image_id`: ID of the uploaded image diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/main.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/main.tf similarity index 53% rename from go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/main.tf rename to go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/main.tf index cad8cddb..0c82c897 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/main.tf +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/main.tf @@ -4,5 +4,9 @@ resource "stackit_image" "this" { disk_format = var.disk_format local_file_path = var.local_file_path min_disk_size = var.min_disk_size - config = var.config + config = { + operating_system = var.operating_system + operating_system_distro = var.operating_system_distro + operating_system_version = var.operating_system_version + } } diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/outputs.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/outputs.tf similarity index 60% rename from go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/outputs.tf rename to go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/outputs.tf index 9f7aabba..cfb8fadb 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/outputs.tf +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/outputs.tf @@ -1,4 +1,4 @@ output "image_id" { - description = "ID of the created image." + description = "ID of the uploaded image." value = stackit_image.this.image_id } diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/terraform.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/terraform.tf similarity index 100% rename from go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/terraform.tf rename to go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/terraform.tf diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf new file mode 100644 index 00000000..ac8563fa --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf @@ -0,0 +1,49 @@ +variable "project_id" { + type = string + description = "STACKIT project ID." +} + +variable "name" { + type = string + description = "Name of the uploaded Edge image." +} + +variable "local_file_path" { + type = string + description = "Local path to the image artifact that Terraform uploads." +} + +variable "disk_format" { + type = string + description = "Disk format of the image." + default = "raw" +} + +variable "min_disk_size" { + type = number + description = "Minimum disk size of the image." + default = 30 + + validation { + condition = var.min_disk_size > 0 + error_message = "min_disk_size must be greater than zero." + } +} + +variable "operating_system" { + type = string + description = "Operating system of the uploaded image." + default = "linux" +} + +variable "operating_system_distro" { + type = string + description = "Operating system distro of the uploaded image." + default = "talos" +} + +variable "operating_system_version" { + type = string + description = "Operating system version of the uploaded image." + default = "v1.9.5" +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md new file mode 100644 index 00000000..5ddcb8a5 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md @@ -0,0 +1,29 @@ +# STACKIT Edge Instance Module + +Terraform module for creating or reusing a STACKIT Edge Cloud instance. + +## What this module does + +- Creates `stackit_edgecloud_instance` when `create = true` +- Reuses an externally provided `instance_id` when `create = false` + +## Usage + +```hcl +module "edge_instance" { + source = "../modules/edge-instance" + + project_id = var.project_id + create = true + display_name = "edge1234" + plan_name = "preview" + region = "eu01" + description = "kubara edge instance" +} +``` + +## Outputs + +- `instance_id`: Edge Cloud instance ID +- `frontend_url`: Edge frontend URL (null for reused instances) +- `status`: Instance status (null for reused instances) diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf new file mode 100644 index 00000000..84ec064a --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf @@ -0,0 +1,37 @@ +data "stackit_edgecloud_plans" "this" { + count = var.create ? 1 : 0 + project_id = var.project_id +} + +locals { + available_plan_names = var.create ? sort([ + for plan in data.stackit_edgecloud_plans.this[0].plans : plan.name + ]) : [] + + matching_plans = var.create ? [ + for plan in data.stackit_edgecloud_plans.this[0].plans : plan + if lower(plan.name) == lower(var.plan_name) + ] : [] +} + +resource "stackit_edgecloud_instance" "this" { + count = var.create ? 1 : 0 + + project_id = var.project_id + region = var.region + display_name = var.display_name + description = var.description == "" ? null : var.description + plan_id = one(local.matching_plans).id + + lifecycle { + precondition { + condition = length(local.matching_plans) == 1 + error_message = format( + "Expected exactly one Edge Cloud plan match for '%s' but found %d. Available plans: %s", + var.plan_name, + length(local.matching_plans), + join(", ", local.available_plan_names), + ) + } + } +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf new file mode 100644 index 00000000..ef4fc99b --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf @@ -0,0 +1,14 @@ +output "instance_id" { + description = "ID of the Edge Cloud instance (created or reused)." + value = var.create ? stackit_edgecloud_instance.this[0].instance_id : var.instance_id +} + +output "frontend_url" { + description = "Frontend URL of the created Edge Cloud instance. Null when reusing an existing instance." + value = var.create ? stackit_edgecloud_instance.this[0].frontend_url : null +} + +output "status" { + description = "Current status of the created Edge Cloud instance. Null when reusing an existing instance." + value = var.create ? stackit_edgecloud_instance.this[0].status : null +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/terraform.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/terraform.tf new file mode 100644 index 00000000..44b1a615 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/terraform.tf @@ -0,0 +1,9 @@ +terraform { + required_version = ">=1.9.3" + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = "0.91.0" + } + } +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf new file mode 100644 index 00000000..0b0d97b8 --- /dev/null +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf @@ -0,0 +1,55 @@ +variable "project_id" { + type = string + description = "STACKIT project ID." +} + +variable "create" { + type = bool + description = "Whether Terraform should create a new Edge Cloud instance. Set to false to reuse an existing instance_id." + default = true +} + +variable "instance_id" { + type = string + description = "Existing Edge Cloud instance ID to reuse when create is false." + default = "" + + validation { + condition = var.create || trimspace(var.instance_id) != "" + error_message = "instance_id must be set when create is false." + } +} + +variable "display_name" { + type = string + description = "Display name of the Edge Cloud instance (4-8 chars, valid hostname label)." + default = "" + + validation { + condition = !var.create || trimspace(var.display_name) != "" + error_message = "display_name must be set when create is true." + } + + validation { + condition = var.display_name == "" || can(regex("^[a-z0-9][a-z0-9-]{2,6}[a-z0-9]$", var.display_name)) + error_message = "display_name must be empty or a valid hostname label with a length between 4 and 8 characters." + } +} + +variable "description" { + type = string + description = "Description for the Edge Cloud instance." + default = "" +} + +variable "plan_name" { + type = string + description = "Name of the Edge Cloud plan to use when create is true." + default = "preview" +} + +variable "region" { + type = string + description = "Region used for Edge Cloud resources." + default = "eu01" +} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/README.md deleted file mode 100644 index b3282bc8..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/README.md +++ /dev/null @@ -1,62 +0,0 @@ -# STACKIT Edge Cloud Image Upload Module - -Terraform module to upload a local image to STACKIT Edge Cloud. -The image can then be used to launch virtual machines on the Edge Cloud platform. - -## Usage - -module "edgecloud_image" { -source = "../modules/image_upload" - -project_id = var.project_id -name = "talos-edge-image" -local_file_path = "./talos.raw" -min_disk_size = 10 -config = { -operating_system = "linux" -operating_system_distro = "talos" -operating_system_version = "v1.9.5" -} -} - - -## Requirements - -| Name | Version | -|------|---------| -| [terraform](#requirement\_terraform) | >=1.9.3 | -| [stackit](#requirement\_stackit) | 0.90.0 | - -## Providers - -| Name | Version | -|------|---------| -| [stackit](#provider\_stackit) | 0.90.0 | - -## Modules - -No modules. - -## Resources - -| Name | Type | -|------|------| -| [stackit_image.this](https://registry.terraform.io/providers/stackitcloud/stackit/0.90.0/docs/resources/image) | resource | - -## Inputs - -| Name | Description | Type | Default | Required | -|------|-------------|------|---------|:--------:| -| [config](#input\_config) | Configuration of the image. |
object({
operating_system = string
operating_system_distro = string
operating_system_version = string
})
|
{
"operating_system": "linux",
"operating_system_distro": "talos",
"operating_system_version": "v1.9.5"
}
| no | -| [disk\_format](#input\_disk\_format) | Disk format of the image. | `string` | `"raw"` | no | -| [local\_file\_path](#input\_local\_file\_path) | Local file path of the image. | `string` | n/a | yes | -| [min\_disk\_size](#input\_min\_disk\_size) | Minimum disk size of the image. | `number` | `10` | no | -| [name](#input\_name) | Name of the image. | `string` | n/a | yes | -| [project\_id](#input\_project\_id) | The STACKIT ProjectID. | `string` | n/a | yes | - -## Outputs - -| Name | Description | -|------|-------------| -| [image\_id](#output\_image\_id) | ID of the created image. | - \ No newline at end of file diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/variables.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/variables.tf deleted file mode 100644 index af7f12b7..00000000 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/image_upload/variables.tf +++ /dev/null @@ -1,44 +0,0 @@ -variable "name" { - type = string - description = "Name of the image." - -} - -variable "project_id" { - type = string - description = "The STACKIT ProjectID." -} - -variable "disk_format" { - type = string - description = "Disk format of the image." - default = "raw" -} - -variable "local_file_path" { - type = string - description = "Local file path of the image." - -} - -variable "min_disk_size" { - type = number - description = "Minimum disk size of the image." - default = 10 - -} - -variable "config" { - type = object({ - operating_system = string - operating_system_distro = string - operating_system_version = string - }) - description = "Configuration of the image." - default = { - operating_system = "linux" - operating_system_distro = "talos" - operating_system_version = "v1.9.5" - } - -} From 778ed62c3de21d79acb113eef42fbb0e7a8b599d Mon Sep 17 00:00:00 2001 From: Matthiator Date: Fri, 24 Apr 2026 15:21:12 +0200 Subject: [PATCH 2/9] refactor: refine stackit edge cloud defaults --- .../stackit_provisioning_edgecloud.md | 36 +++++++++++++++++-- go-binary/cmd/generate_test.go | 4 ++- .../infrastructure/env.auto.tfvars.tplt | 22 ++++-------- .../example/infrastructure/variables.tf.tplt | 2 +- .../terraform/providers/stackit/README.md | 16 ++++++++- .../stackit/modules/edge-hosts/README.md | 2 +- .../stackit/modules/edge-image/README.md | 2 +- .../stackit/modules/edge-image/variables.tf | 4 +-- .../stackit/modules/ske-cluster/README.md | 4 +-- 9 files changed, 64 insertions(+), 28 deletions(-) diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md index d2661f33..a2820e88 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -24,6 +24,7 @@ Terraform modules for Edge are optional and can be combined as needed: * `edge_image` is optional. `edge_image.create` toggles image upload; upload runs only when `local_file_path` is set. In many setups, you can skip `edge_image` and reuse an existing image ID via `edge_hosts.image_id`. * `edge_hosts` is optional. `edge_hosts.create` toggles host provisioning; host provisioning requires `edge_hosts.image_id` and at least one entry in `nodes`. + The generated tfvars starts with a single-node bootstrap example (`controlplane` with public IP). * `edge_hosts` does not auto-link to `edge_image`. Set `edge_hosts.image_id` explicitly: * use an existing image ID, or * upload via `edge_image` first, then copy `edge_uploaded_image_id` output into `edge_hosts.image_id`. @@ -43,6 +44,35 @@ Set the module toggles in the generated cluster tfvars file: - `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` +## Single-node bootstrap and network planning + +The generated `edge_hosts.nodes` defaults to one control plane node as a bootstrap example. +This is intentionally simple for initial rollout and troubleshooting. + +For production and multi-node topologies, design networking explicitly for your environment: + +* where hosts run (VMs, bare metal, mixed, multi-cloud) +* ingress entry strategy (single public node, MetalLB VIP, external NLB, etc.) +* node-to-node traffic and return paths + +There is no one-size-fits-all edge network design. + +## Public IP and IP fields + +In this example, Terraform allocates public IPs for hosts when `edge_hosts.nodes[*].assign_public_ip = true`. +You do not need to pre-enter a public IP for that path. + +Read assigned host public IPs after apply via: + +```bash +terraform output edge_host_metadata +``` + +Use the selected host public IP for DNS `A` records (for example your ingress hostname). + +If you enable MetalLB, the generated MetalLB customer values use `clusters[].privateLoadBalancerIP` from your `config.yaml` as pool address (`/32`). +`publicLoadBalancerIP` is currently not wired into the generated MetalLB chart values. + ## Step-by-step sequence Before you start, go to: @@ -207,15 +237,15 @@ spec: - edgeHost: installDisk: /dev/vda role: controlplane - - edgeHost: - installDisk: /dev/vda - role: worker talos: version: v1.12.5-stackit.v1.7.1 kubernetes: version: v1.30.2 ``` +This minimal example reflects the single-node bootstrap path. +For production, add more control plane and worker nodes based on your network design. + ## Validate API schema on your instance `EdgeImage` and `EdgeCluster` schemas can evolve. Validate against your STEC instance before applying manifests: diff --git a/go-binary/cmd/generate_test.go b/go-binary/cmd/generate_test.go index 71125931..4894cd3e 100644 --- a/go-binary/cmd/generate_test.go +++ b/go-binary/cmd/generate_test.go @@ -508,7 +508,9 @@ func TestGenerateCmd_EdgeTerraformArtifacts(t *testing.T) { assert.Contains(t, string(envTfvars), "local_file_path = \"/path/to/edge-artifacts/openstack-amd64.raw\"") assert.Contains(t, string(envTfvars), "image_id = \"\"") assert.Contains(t, string(envTfvars), "nodes = [") - assert.Contains(t, string(envTfvars), "flavor = \"c1.4\"") + assert.Contains(t, string(envTfvars), "name = \"edge-cp-1\"") + assert.NotContains(t, string(envTfvars), "edge-worker-1") + assert.Contains(t, string(envTfvars), "flavor = \"g2i.8\"") assert.NotContains(t, string(envTfvars), "edge_instance.enabled") assert.NotContains(t, string(envTfvars), "edge_image.enabled") assert.NotContains(t, string(envTfvars), "edge_hosts.enabled") diff --git a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt index 429631a0..99ae5a3f 100644 --- a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt +++ b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt @@ -67,7 +67,8 @@ edge_image = { min_disk_size = 30 operating_system = "linux" operating_system_distro = "talos" - operating_system_version = "v1.9.5" + # Metadata for the uploaded STACKIT project image. Keep this aligned with EdgeImage.spec.talosVersion. + operating_system_version = "v1.12.5-stackit.v1.7.1" } edge_hosts = { @@ -88,32 +89,21 @@ edge_hosts = { ingress_tcp_ports = [80, 443] common_labels = {} - # Example only: adapt values to your environment. + # Bootstrap example only: single-node setup with one public IP. + # Adapt this to your environment before production use. # Hosts are created only when create=true. nodes = [ { name = "edge-cp-1" role = "controlplane" - flavor = "c1.4" + flavor = "g2i.8" volume_size = 40 volume_performance_class = "storage_premium_perf1" - availability_zone = "eu01-2" + availability_zone = "eu01-1" assign_public_ip = true labels = { "node-role" = "controlplane" } - }, - { - name = "edge-worker-1" - role = "worker" - flavor = "c1.4" - volume_size = 40 - volume_performance_class = "storage_premium_perf1" - availability_zone = "eu01-2" - assign_public_ip = true - labels = { - "node-role" = "worker" - } } ] } diff --git a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt index d6b68dfa..e3527d38 100644 --- a/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt +++ b/go-binary/templates/embedded/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt @@ -180,7 +180,7 @@ variable "edge_image" { min_disk_size = optional(number, 30) operating_system = optional(string, "linux") operating_system_distro = optional(string, "talos") - operating_system_version = optional(string, "v1.9.5") + operating_system_version = optional(string, "v1.12.5-stackit.v1.7.1") }) default = {} diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/README.md index 62ab98de..23033ee0 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/README.md +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/README.md @@ -144,7 +144,7 @@ kubernetes_version_min = "1.32.5" node_pools = [ { availability_zones = ["eu01-2"] - machine_type = "c1.5" + machine_type = "c2i.8" maximum = 4 minimum = 2 name = "pool-infra" @@ -227,6 +227,7 @@ Set all Edge module toggles in: - `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` In many setups, you can skip `edge_image` and reuse an existing image ID via `edge_hosts.image_id`. +The generated `edge_hosts.nodes` defaults to a single-node bootstrap example (`controlplane` with public IP). 1. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` (create or reuse). 2. Keep `edge_image.create = false` for the first apply. @@ -279,6 +280,19 @@ curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions terraform output edge_uploaded_image_id ``` +Public IP handling in this example: + +- Terraform allocates host public IPs when `edge_hosts.nodes[*].assign_public_ip = true`. +- Read assigned host IPs from: + +```bash +terraform output edge_host_metadata +``` + +- Use the selected host public IP for DNS `A` records (for example your ingress hostname). +- If MetalLB is enabled, generated customer values currently use `clusters[].privateLoadBalancerIP` (`/32`) as pool address. +- `publicLoadBalancerIP` is currently not wired into generated MetalLB values. + If you use an existing image for Longhorn and know the related `EdgeImage`, verify that these extensions are present in that image configuration: ```bash diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md index aa55509d..7138ee35 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md @@ -27,7 +27,7 @@ module "edge_hosts" { { name = "edge-demo-cp-1" role = "controlplane" - flavor = "c1.4" + flavor = "g2i.8" volume_size = 30 volume_performance_class = "storage_premium_perf1" availability_zone = "eu01-1" diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md index 7ed3c250..18fdc66d 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/README.md @@ -15,7 +15,7 @@ module "edge_image" { operating_system = "linux" operating_system_distro = "talos" - operating_system_version = "v1.9.5" + operating_system_version = "v1.12.5-stackit.v1.7.1" } ``` diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf index ac8563fa..6e3cf0bd 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/edge-image/variables.tf @@ -44,6 +44,6 @@ variable "operating_system_distro" { variable "operating_system_version" { type = string - description = "Operating system version of the uploaded image." - default = "v1.9.5" + description = "Operating system version metadata of the uploaded image. Keep this aligned with the EdgeImage Talos version used to build the artifact." + default = "v1.12.5-stackit.v1.7.1" } diff --git a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/ske-cluster/README.md b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/ske-cluster/README.md index c35f2241..554255cc 100644 --- a/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/ske-cluster/README.md +++ b/go-binary/templates/embedded/managed-service-catalog/terraform/providers/stackit/modules/ske-cluster/README.md @@ -16,7 +16,7 @@ module "ske_cluster" { node_pools = [ { name = "np-frontend" - machine_type = "c1.2" + machine_type = "c2i.8" minimum = 2 maximum = 4 availability_zones = ["eu01-1", "eu01-2"] @@ -27,7 +27,7 @@ module "ske_cluster" { }, { name = "np-backend" - machine_type = "c1.2" + machine_type = "c2i.8" minimum = 2 maximum = 5 availability_zones = ["eu01-1", "eu01-2", "eu01-3"] From 93799012d87f7283edd4c20da13969a10ea7dec3 Mon Sep 17 00:00:00 2001 From: Matthiator Date: Wed, 27 May 2026 18:14:59 +0200 Subject: [PATCH 3/9] feat: refactor edge cloud provisioning --- .../1_getting_started/providers/stackit.md | 4 +- .../stackit_provisioning_edgecloud.md | 376 +++++++++++------- .../providers/stackit_provisioning_ske.md | 2 +- .../providers/stackit_terraform_bootstrap.md | 8 +- src/cmd/generate_test.go | 25 +- .../infrastructure/env.auto.tfvars.tplt | 29 +- .../example/infrastructure/main.tf.tplt | 38 +- .../example/infrastructure/outputs.tf.tplt | 32 +- .../example/infrastructure/variables.tf.tplt | 52 ++- .../terraform/providers/stackit/README.md | 148 +++++-- .../stackit/modules/edge-hosts/README.md | 7 +- .../stackit/modules/edge-hosts/main.tf | 16 +- .../stackit/modules/edge-hosts/variables.tf | 13 +- .../stackit/modules/edge-instance/README.md | 17 +- .../stackit/modules/edge-instance/main.tf | 38 +- .../stackit/modules/edge-instance/outputs.tf | 18 +- .../modules/edge-instance/variables.tf | 38 +- 17 files changed, 497 insertions(+), 364 deletions(-) diff --git a/docs/content/1_getting_started/providers/stackit.md b/docs/content/1_getting_started/providers/stackit.md index 364b514a..2bfb9c5f 100644 --- a/docs/content/1_getting_started/providers/stackit.md +++ b/docs/content/1_getting_started/providers/stackit.md @@ -6,7 +6,5 @@ For this purpose we provide the necessary Terraform configuration and modules. Follow this order: 1. Start with [Terraform Bootstrap](stackit_terraform_bootstrap.md). -2. Then choose the provisioning path for your cluster type: - - [SKE](stackit_provisioning_ske.md) - - [Edge Cloud](stackit_provisioning_edgecloud.md) +2. Choose exactly one provisioning path for your cluster type: [SKE](stackit_provisioning_ske.md) or [Edge Cloud](stackit_provisioning_edgecloud.md). 3. Continue with the generic [Bootstrap Your Own Platform](../bootstrapping.md) guide. diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md index a2820e88..32cc48c8 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -11,36 +11,31 @@ All paths are valid: - UI is often the fastest manual path for image and cluster creation. - CLI/API with manifests is better for reproducibility and change history (for example in Git). +For general STEC background, see the official [Edge Cloud overview](https://docs.stackit.cloud/products/runtime/edge-cloud/), [Authentication](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/authentication/), and [Using the API](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-the-api/) guides. + Choose the operating model that fits your team. kubara examples use manifest-based flows for traceability, but this is not mandatory. -## Optional Terraform modules for Edge +## Terraform modules for the kubara Edge example -Terraform modules for Edge are optional and can be combined as needed: +The generated Edge Terraform example is intentionally create-only: -* `edge_instance` is optional. `edge_instance.create` acts as module toggle: - * `true`: create a new instance - * `false` + `instance_id`: reuse existing instance - * `false` + empty `instance_id`: skip module -* `edge_image` is optional. `edge_image.create` toggles image upload; upload runs only when `local_file_path` is set. - In many setups, you can skip `edge_image` and reuse an existing image ID via `edge_hosts.image_id`. -* `edge_hosts` is optional. `edge_hosts.create` toggles host provisioning; host provisioning requires `edge_hosts.image_id` and at least one entry in `nodes`. - The generated tfvars starts with a single-node bootstrap example (`controlplane` with public IP). -* `edge_hosts` does not auto-link to `edge_image`. Set `edge_hosts.image_id` explicitly: - * use an existing image ID, or - * upload via `edge_image` first, then copy `edge_uploaded_image_id` output into `edge_hosts.image_id`. +* `edge_instance` creates a new Edge Cloud instance. +* `edge_image` uploads one local image artifact to STACKIT. +* `edge_hosts` creates the example VM/network-based hosts and uses the uploaded image from `edge_image`. If your Edge platform is already provisioned externally, you can skip Terraform Edge modules completely and continue with kubara Helm/GitOps workflows only. +For example, this is common when you run bare metal, mixed VM/bare-metal environments, or hosts in another cloud. -`EdgeImage` and `EdgeCluster` resources are managed via Kubernetes API (`kubectl`) and intentionally not part of the Terraform state in this setup. +`EdgeImage` and `EdgeCluster` resources are intentionally not part of the Terraform state in this setup. +The examples use Kubernetes API manifests with `kubectl` because that is easy to automate and keep in Git, but you can also create and manage them manually through the Edge Cloud UI or with STEC CLI/API workflows. Important distinction: -* `EdgeImage` and `image-factory` versions describe STEC boot artifacts and profiles (for example Talos version and extensions). -* `edge_hosts.image_id` must be a STACKIT project image ID (Compute/IaaS image), not an `image-factory` version string. - -## Where to set `create = true/false` +* In STEC, `EdgeImage` defines how the Talos boot image should be built, including Talos version and system extensions. +* STEC then generates a downloadable image artifact and exposes it through `EdgeImage.status.artifactUrl`. +* Terraform does not use the `EdgeImage` object directly. Terraform `edge_image.local_file_path` must point to the downloaded local artifact file. -Set the module toggles in the generated cluster tfvars file: +Configure the generated Edge settings in: - `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` @@ -75,176 +70,284 @@ If you enable MetalLB, the generated MetalLB customer values use `clusters[].pri ## Step-by-step sequence +This flow cannot be a single normal `terraform apply` from an empty environment. +The local image artifact is generated by STEC after the Edge Cloud instance exists, but Terraform needs that local artifact path before it can upload the image and create hosts. + Before you start, go to: ```bash cd customer-service-catalog/terraform//infrastructure ``` -1. Run `terraform init` and `terraform plan` (or `tofu init` / `tofu plan`). -2. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` (create or reuse). -3. Keep `edge_image.create = false` for the first apply. -4. Keep `edge_hosts.create = false` for the first apply (unless `edge_hosts.image_id` is already set and you want direct host provisioning). -5. Run first apply: +### Phase 1: Create the Edge Cloud instance + +1. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars`. + Terraform reads the available Edge Cloud plan from the STACKIT API. + STEC is currently beta; the generated module expects exactly one available plan for the project and fails if the API returns zero or multiple plans. +2. Run `terraform init` and create only the Edge Cloud instance: === "Terraform" ```bash - terraform apply + terraform init + terraform apply -target=module.edge_instance ``` === "Tofu" ```bash - tofu apply + tofu init + tofu apply -target=module.edge_instance ``` -6. Check whether a suitable STACKIT project image already exists. If yes, set `edge_hosts.image_id`, keep `edge_image.create = false`, then continue with step 10. -7. If no suitable image exists, create `EdgeImage` in your STEC instance via `kubectl` and wait for `Ready`. - If you plan to run Longhorn on this cluster, include `siderolabs/iscsi-tools` and `siderolabs/util-linux-tools` in the `EdgeImage` system extensions. -8. Read the generated artifact URL from `EdgeImage.status` and download it locally. -9. Set `edge_image.create = true` and set `edge_image.local_file_path` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` to that local artifact path. -10. Run apply and read `edge_uploaded_image_id` from Terraform output, for example `terraform output edge_uploaded_image_id`. -11. Set `edge_hosts.image_id` to that ID (or keep your existing image ID), set `edge_hosts.create = true`, and configure `edge_hosts.nodes`. -12. Run apply again to create hosts. +3. Save the generated short-lived Edge Cloud kubeconfig for the following `kubectl` steps: === "Terraform" ```bash - terraform apply + terraform output -raw edge_kubeconfig_raw > edge-kubeconfig.yaml + chmod 600 edge-kubeconfig.yaml + export KUBECONFIG="$PWD/edge-kubeconfig.yaml" ``` === "Tofu" ```bash - tofu apply + tofu output -raw edge_kubeconfig_raw > edge-kubeconfig.yaml + chmod 600 edge-kubeconfig.yaml + export KUBECONFIG="$PWD/edge-kubeconfig.yaml" ``` -13. Wait until the booted hosts are visible as `EdgeHost` in STEC. -14. Create `EdgeCluster` via `kubectl` and assign node roles (`controlplane` / `worker`). +### Phase 2: Generate the Edge image artifact -How to read image information: +4. Create an `EdgeImage` in your STEC instance and wait until it is `Ready`. -* List existing `EdgeImage` resources (STEC side): +`EdgeImage` is the Kubernetes resource kind. The examples below use the `default` namespace, matching the STACKIT documentation. +Read more in the official STACKIT guide: [Creating images](https://docs.stackit.cloud/de/products/runtime/edge-cloud/getting-started/creating-images/). -```bash -kubectl get edgeimages.edge.stackit.cloud -kubectl get edgeimage -o yaml -``` - -* List available `image-factory` Talos versions (STEC side): +Before applying manifests, verify which Talos image versions are currently available in your STEC region: ```bash INSTANCE_REGION="eu01" curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions ``` - These versions are not directly usable as `edge_hosts.image_id`. +Use one of the returned versions in `EdgeImage.spec.talosVersion`. +If your kubara cluster will use Longhorn, include `siderolabs/iscsi-tools` and `siderolabs/util-linux-tools` in the `EdgeImage` system extensions. +Read more in the official STACKIT guide: [Using extensions](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-extensions/) and the [Longhorn Talos Linux support](https://longhorn.io/docs/1.11.0/advanced-resources/os-distro-specific/talos-linux-support/) documentation. + +=== "UI" + + 1. Open your STEC instance. + 2. Go to `Images`. + 3. Create a new image. + 4. Select the Talos image version you want to use. + 5. Add required system extensions, for example Longhorn-related extensions. + 6. Wait until the image is `Ready`. + +=== "kubectl" + + Save this as `edge-image.yaml`: + + ```yaml + apiVersion: edge.stackit.cloud/v1alpha1 + kind: EdgeImage + metadata: + name: kubara-edge-image + namespace: default + spec: + schematic: | + customization: + extraKernelArgs: [] + systemExtensions: + officialExtensions: + - siderolabs/iscsi-tools + - siderolabs/util-linux-tools + overlay: {} + talosVersion: v1.12.5-stackit.v1.7.1 + ``` -* For Terraform host provisioning, you need a STACKIT project image ID for `edge_hosts.image_id`: - * copy an existing ID from your STACKIT project image list (UI), or - * use the Terraform upload module once and read: + Apply it and wait until STEC has generated the artifacts: -```bash -terraform output edge_uploaded_image_id -``` + ```bash + kubectl apply -f edge-image.yaml + kubectl wait EdgeImage/kubara-edge-image --namespace default --for=condition=Ready --timeout=60m + ``` -If you use an existing image for Longhorn and know the related `EdgeImage`, verify that these extensions are present in that image configuration: + If you are not using Longhorn, keep only the extensions you actually need. -```bash -kubectl get edgeimage -o yaml -# check spec.schematic -> customization.systemExtensions.officialExtensions -# expected: siderolabs/iscsi-tools and siderolabs/util-linux-tools -``` +### Phase 3: Download image, upload it to STACKIT, and create hosts -## Why this order +Before Terraform can upload the image, you need a local raw image file for STACKIT Compute Engine. +You can get it either through the Edge Cloud UI or through the Kubernetes API. -- `EdgeImage` is needed only when you do not already have a suitable STACKIT project image. -- Terraform `edge_image` upload needs a local artifact path. -- Terraform `edge_hosts` (if used) need an explicit `edge_hosts.image_id`. -- `EdgeCluster` should be created after hosts are registered as `EdgeHost`. -- You can source `edge_hosts.image_id` from an existing image or from `edge_uploaded_image_id` after a prior upload apply. -- In many setups, reusing an existing image ID is the default path; `edge_image` upload is only needed if you explicitly want to upload your own artifact. +5. Download the OpenStack/amd64 raw image artifact, unpack it locally, and set `edge_image.local_file_path`. + +=== "UI" -## Talos and Kubernetes version compatibility + 1. Open your STEC instance. + 2. Go to `Images`. + 3. Select the Ready image. + 4. Click `Download`. + 5. Select the OpenStack/amd64 raw image artifact. + 6. Download the `.raw.xz` file. + 7. Unpack it locally: -`EdgeImage` sets the Talos version used to build boot artifacts. -`EdgeCluster` sets Talos and Kubernetes versions for the cluster. + ```bash + mkdir -p edge-artifacts + mv ~/Downloads/openstack-amd64.raw.xz edge-artifacts/ + unxz edge-artifacts/openstack-amd64.raw.xz + ls edge-artifacts/openstack-amd64.raw + ``` -According to STACKIT docs: +=== "kubectl" -* Talos version during cluster creation may differ from the boot image Talos version. -* If different, Talos automatically upgrades/downgrades during cluster creation. -* STEC validates that the chosen Kubernetes version is supported by the chosen Talos version. + ```bash + IMAGE_NAME="kubara-edge-image" -Recommendation: keep `EdgeImage` Talos version and `EdgeCluster` Talos version aligned whenever possible to reduce moving parts during bootstrap. + kubectl get EdgeImage "${IMAGE_NAME}" --namespace default -o yaml | yq '.status.artifacts' -Before applying manifests, verify which Talos image versions are currently available in your STEC region: + IMAGE_URL=$( + kubectl get EdgeImage "${IMAGE_NAME}" --namespace default -o yaml \ + | yq -r '.status.artifacts[] | select(.type == "openstack") | .artifacts[] | select(.architecture == "amd64") | .url' + ) -```bash -INSTANCE_REGION="eu01" -curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions + mkdir -p edge-artifacts + curl -L "${IMAGE_URL}" -o edge-artifacts/openstack-amd64.raw.xz + unxz edge-artifacts/openstack-amd64.raw.xz + ls edge-artifacts/openstack-amd64.raw + ``` + + If the `yq` query returns an empty URL, inspect `.status.artifacts` and copy the OpenStack/amd64 `.raw.xz` URL manually. + The exact artifact list can differ by STEC version and selected target platform. + +Then set the unpacked path in `env.auto.tfvars`: + +```hcl +edge_image = { + local_file_path = "edge-artifacts/openstack-amd64.raw" + # ... +} ``` -Use one of the returned versions in both `EdgeImage.spec.talosVersion` and `EdgeCluster.spec.talos.version`. +6. Configure `edge_hosts.nodes`. +7. Run full apply. Terraform uploads the image and creates the VM/network-based hosts from that uploaded image: -## Longhorn extension requirement +=== "Terraform" -If your kubara cluster will use Longhorn, your Talos image should include these system extensions: + ```bash + terraform apply + terraform output edge_uploaded_image_id + terraform output edge_host_metadata + ``` -- `siderolabs/iscsi-tools` -- `siderolabs/util-linux-tools` +=== "Tofu" -Reason: Longhorn on Talos requires iSCSI tooling, and uses binaries from `util-linux-tools` (for example `fstrim`) for volume operations. -If your existing STACKIT project image was built from an `EdgeImage` profile that already includes these extensions, no extra rebuild is required. + ```bash + tofu apply + tofu output edge_uploaded_image_id + tofu output edge_host_metadata + ``` -## `EdgeImage` example +8. Wait until the booted hosts are visible as `EdgeHost` in STEC. -Use this to generate a Talos image in STEC: +### Phase 4: Create the EdgeCluster -```yaml -apiVersion: edge.stackit.cloud/v1alpha1 -kind: EdgeImage -metadata: - name: kubara-edge-image - namespace: default -spec: - schematic: | - customization: - extraKernelArgs: [] - systemExtensions: - officialExtensions: - - siderolabs/iscsi-tools - - siderolabs/util-linux-tools - overlay: {} - talosVersion: v1.12.5-stackit.v1.7.1 -``` +9. Create the `EdgeCluster` after the hosts are registered as `EdgeHost`. -If you are not using Longhorn, keep only the extensions you actually need. - -## `EdgeCluster` example - -Use this after your hosts are registered as `EdgeHost`: - -```yaml -apiVersion: edge.stackit.cloud/v1alpha1 -kind: EdgeCluster -metadata: - name: kubara-cluster - namespace: default -spec: - nodes: - - edgeHost: - installDisk: /dev/vda - role: controlplane - talos: - version: v1.12.5-stackit.v1.7.1 - kubernetes: - version: v1.30.2 -``` +Read more in the official STACKIT guide: [Creating clusters](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/creating-clusters/). + +`EdgeCluster` sets the Talos and Kubernetes versions for the cluster. +Use a Talos version that is available in your STEC region and keep it aligned with `EdgeImage.spec.talosVersion` unless you intentionally want STEC to upgrade or downgrade during bootstrap. + +=== "UI" + + 1. Open your STEC instance. + 2. Go to `Clusters`. + 3. Create a new cluster. + 4. Select the registered hosts. + 5. Assign node roles, for example `controlplane` for the single-node bootstrap example. + 6. Select the Talos and Kubernetes versions. + 7. Create the cluster and wait until it is ready. + +=== "kubectl" + + First inspect registered hosts: + + ```bash + kubectl get EdgeHost --namespace default + ``` + + Save this as `edge-cluster.yaml` and replace `` with the host ID from STEC: + + ```yaml + apiVersion: edge.stackit.cloud/v1alpha1 + kind: EdgeCluster + metadata: + name: kubara-cluster + namespace: default + spec: + nodes: + - edgeHost: + installDisk: /dev/vda + role: controlplane + talos: + version: v1.12.5-stackit.v1.7.1 + kubernetes: + version: v1.30.2 + ``` + + Apply it: + + ```bash + kubectl apply -f edge-cluster.yaml + ``` -This minimal example reflects the single-node bootstrap path. -For production, add more control plane and worker nodes based on your network design. + This minimal example reflects the single-node bootstrap path. + For production, add more control plane and worker nodes based on your network design. + +### Phase 5: Export the cluster kubeconfig + +10. Export the kubeconfig for the Kubernetes cluster created by `EdgeCluster`. + +This is not the same file as `edge_kubeconfig_raw`. +`edge_kubeconfig_raw` is the STEC management kubeconfig used for `EdgeImage`, `EdgeHost`, and `EdgeCluster`. +The cluster kubeconfig is used for the actual Kubernetes cluster and for the next kubara bootstrap steps. + +Read more in the official STACKIT guide: [Managing clusters](https://docs.stackit.cloud/de/products/runtime/edge-cloud/getting-started/managing-clusters/). + +=== "UI" + + 1. Open your STEC instance. + 2. Go to `Clusters`. + 3. Open the created cluster. + 4. Download the kubeconfig from the cluster details page. + 5. Save it locally, for example as `edge-cluster.kubeconfig.yaml`. + +=== "kubectl" + + Use the STEC management kubeconfig from Phase 1 and decode the generated cluster kubeconfig secret: + + ```bash + export KUBECONFIG="$PWD/edge-kubeconfig.yaml" + + CLUSTER_NAME="kubara-cluster" + kubectl get secret "${CLUSTER_NAME}-kubeconfig" \ + --namespace default \ + -o yaml \ + | yq -r '.data.value' \ + | base64 --decode > "${CLUSTER_NAME}.kubeconfig.yaml" + ``` + + Then switch to the cluster kubeconfig and verify Kubernetes access: + + ```bash + export KUBECONFIG="$PWD/${CLUSTER_NAME}.kubeconfig.yaml" + kubectl get nodes + kubectl get pods --all-namespaces + ``` + +Use this cluster kubeconfig for the generic kubara bootstrap guide. ## Validate API schema on your instance @@ -256,14 +359,13 @@ kubectl explain edgeimages.edge.stackit.cloud.spec kubectl explain edgeclusters.edge.stackit.cloud.spec ``` -## Official references +## Why this order -* [Edge Cloud overview](https://docs.stackit.cloud/products/runtime/edge-cloud/) -* [Using the API](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-the-api/) -* [Creating images](https://docs.stackit.cloud/de/products/runtime/edge-cloud/getting-started/creating-images/) -* [Using extensions](https://docs.stackit.cloud/products/runtime/edge-cloud/tutorials/using-extensions/) -* [Creating clusters](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/creating-clusters/) -* [Authentication](https://docs.stackit.cloud/products/runtime/edge-cloud/getting-started/authentication/) -* [Longhorn Talos Linux support](https://longhorn.io/docs/1.11.0/advanced-resources/os-distro-specific/talos-linux-support/) +- The Edge Cloud instance must exist before you can create `EdgeImage` in STEC. +- Terraform `edge_image` upload needs the downloaded local artifact path. +- Terraform `edge_hosts` uses the image uploaded by `edge_image`. +- `EdgeCluster` should be created after hosts are registered as `EdgeHost`. +- The cluster kubeconfig is only available after STEC created the Kubernetes cluster. +- The generated Terraform example does not manage externally existing Edge instances, images, or hosts. If you already have those, skip this example infrastructure path and continue with the kubara Helm/GitOps workflow. -Now continue with the generic guide on the [Bootstrap Your Own Platform](../bootstrapping.md) page. +Now continue with the [Helm section of the Bootstrap Your Own Platform guide](../bootstrapping.md#3-helm), using the cluster kubeconfig from Phase 5. diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_ske.md b/docs/content/1_getting_started/providers/stackit_provisioning_ske.md index f3177bea..0e64d913 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_ske.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_ske.md @@ -95,7 +95,7 @@ Sensitive output example: ## Optional: OAuth2-related Vault entries via Terraform -If you use OAuth2, create a GitHub application as shown [here](/2_managing_your_platform/add_sso.md). +If you use OAuth2, create a GitHub application as shown [here](../../2_managing_your_platform/add_sso.md). If you want Terraform to create OAuth2-related Vault entries: diff --git a/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md b/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md index b2b7b9fc..f7557a7d 100644 --- a/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md +++ b/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md @@ -85,7 +85,9 @@ export AWS_SECRET_ACCESS_KEY=" 0 ? 1 : 0") - assert.Contains(t, string(mainTF), "image_id = trimspace(try(var.edge_hosts.image_id, \"\"))") + assert.NotContains(t, string(mainTF), "var.edge_instance.plan_id") + assert.Contains(t, string(mainTF), "expiration = var.edge_instance.expiration") + assert.Contains(t, string(mainTF), "image_id = module.edge_image.image_id") + assert.NotContains(t, string(mainTF), "count =") + assert.NotContains(t, string(mainTF), "try(") assert.NotContains(t, string(mainTF), "module.edge_image[0].image_id") assert.NotContains(t, string(mainTF), "check \"edge_hosts_image_id\"") assert.NotContains(t, string(mainTF), "_fallback") @@ -488,6 +492,7 @@ func TestGenerateCmd_EdgeTerraformArtifacts(t *testing.T) { require.NoError(t, err) assert.Contains(t, string(outputsTF), "output \"edge_host_metadata\"") assert.Contains(t, string(outputsTF), "output \"edge_uploaded_image_id\"") + assert.Contains(t, string(outputsTF), "output \"edge_kubeconfig_raw\"") } // Helper function diff --git a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt index 7100579c..25b57cae 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/env.auto.tfvars.tplt @@ -43,25 +43,14 @@ node_pools = [ {{ if (eq .cluster.terraform.kubernetesType "edge") }} ### Edge edge_instance = { - # Module toggle: - # true = create a new Edge Cloud instance - # false + instance_id set = reuse existing instance - # false + empty instance_id = skip edge_instance module - create = true - instance_id = "" - plan = "preview" + # Terraform reads the available Edge Cloud plan from the STACKIT API. display_name = "edge" - # Optional override. Leave empty to use the global region variable. - region = "" description = "kubara edge instance for {{ .cluster.name }}-{{ .cluster.stage }}" + expiration = 86400 } edge_image = { - # Module toggle: - # true = enable Terraform image upload module - # false = skip edge_image module - create = false - # Example local artifact path (downloaded from EdgeImage.status.artifactUrl): + # Example local artifact path: local_file_path = "/path/to/edge-artifacts/openstack-amd64.raw" name = "talos-{{ .cluster.name }}-{{ .cluster.stage }}" disk_format = "raw" @@ -73,26 +62,16 @@ edge_image = { } edge_hosts = { - # Module toggle: - # true = enable Terraform host provisioning module - # false = skip edge_hosts module - create = false - # Required when create=true and nodes are configured. - # You can set an existing image ID directly, e.g. "11111111-2222-3333-4444-555555555555". - # If you used edge_image module in a previous apply, copy `terraform output edge_uploaded_image_id` here. - image_id = "" name_prefix = "edge-{{ .cluster.name }}-{{ .cluster.stage }}" network_name = "edge-{{ .cluster.name }}-{{ .cluster.stage }}-network" security_group_name = "edge-{{ .cluster.name }}-{{ .cluster.stage }}-sg" - ipv4_prefix = "10.0.50.0" - ipv4_prefix_length = 24 + ipv4_prefix = "10.0.50.0/24" ipv4_nameservers = ["1.1.1.1", "1.0.0.1", "8.8.8.8"] ingress_tcp_ports = [80, 443] common_labels = {} # Bootstrap example only: single-node setup with one public IP. # Adapt this to your environment before production use. - # Hosts are created only when create=true. nodes = [ { name = "edge-cp-1" diff --git a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt index acee28e9..f179ea08 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/main.tf.tplt @@ -63,27 +63,20 @@ resource "random_string" "ske_name_suffix" { {{ if (eq .cluster.terraform.kubernetesType "edge") }} module "edge_instance" { - count = try(var.edge_instance.create, true) || trimspace(try(var.edge_instance.instance_id, "")) != "" ? 1 : 0 - source = "../../../../managed-service-catalog/terraform/modules/edge-instance" - project_id = var.project_id - - create = var.edge_instance.create - instance_id = var.edge_instance.instance_id - plan_name = var.edge_instance.plan - display_name = trimspace(try(var.edge_instance.display_name, "")) - region = trimspace(try(var.edge_instance.region, "")) != "" ? trimspace(try(var.edge_instance.region, "")) : var.region + project_id = var.project_id + display_name = var.edge_instance.display_name + region = var.region description = var.edge_instance.description + expiration = var.edge_instance.expiration } module "edge_image" { - count = try(var.edge_image.create, false) && trimspace(try(var.edge_image.local_file_path, "")) != "" ? 1 : 0 - source = "../../../../managed-service-catalog/terraform/modules/edge-image" project_id = var.project_id - name = trimspace(try(var.edge_image.name, "")) != "" ? trimspace(try(var.edge_image.name, "")) : "talos-${var.name}-${var.stage}" + name = var.edge_image.name local_file_path = var.edge_image.local_file_path disk_format = var.edge_image.disk_format min_disk_size = var.edge_image.min_disk_size @@ -95,21 +88,18 @@ module "edge_image" { } module "edge_hosts" { - count = try(var.edge_hosts.create, false) && length(try(var.edge_hosts.nodes, [])) > 0 ? 1 : 0 - source = "../../../../managed-service-catalog/terraform/modules/edge-hosts" - name = trimspace(try(var.edge_hosts.name_prefix, "")) + name = var.edge_hosts.name_prefix project_id = var.project_id - image_id = trimspace(try(var.edge_hosts.image_id, "")) - network_name = trimspace(try(var.edge_hosts.network_name, "")) - security_group_name = trimspace(try(var.edge_hosts.security_group_name, "")) - - ipv4_prefix = var.edge_hosts.ipv4_prefix - ipv4_prefix_length = var.edge_hosts.ipv4_prefix_length - ipv4_nameservers = var.edge_hosts.ipv4_nameservers - ingress_tcp_ports = var.edge_hosts.ingress_tcp_ports - common_labels = var.edge_hosts.common_labels + image_id = module.edge_image.image_id + network_name = var.edge_hosts.network_name + security_group_name = var.edge_hosts.security_group_name + + ipv4_prefix = var.edge_hosts.ipv4_prefix + ipv4_nameservers = var.edge_hosts.ipv4_nameservers + ingress_tcp_ports = var.edge_hosts.ingress_tcp_ports + common_labels = var.edge_hosts.common_labels nodes = var.edge_hosts.nodes diff --git a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt index 3c1bfb1e..a1875197 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/outputs.tf.tplt @@ -48,36 +48,42 @@ output "kubeconfig_raw" { ### Edge Cloud Instance output "edge_instance_id" { description = "ID of the Edge Cloud instance" - value = length(module.edge_instance) > 0 ? module.edge_instance[0].instance_id : null + value = module.edge_instance.instance_id } output "edge_frontend_url" { - description = "Frontend URL of the Edge Cloud instance (null when reusing an existing instance)" - value = length(module.edge_instance) > 0 ? module.edge_instance[0].frontend_url : null + description = "Frontend URL of the Edge Cloud instance" + value = module.edge_instance.frontend_url } output "edge_status" { - description = "Status of the Edge Cloud instance (null when reusing an existing instance)" - value = length(module.edge_instance) > 0 ? module.edge_instance[0].status : null + description = "Status of the Edge Cloud instance" + value = module.edge_instance.status +} + +output "edge_kubeconfig_raw" { + description = "Raw Edge Cloud kubeconfig (short-lived, sensitive)" + value = module.edge_instance.kubeconfig_raw + sensitive = true } output "edge_uploaded_image_id" { - description = "ID of the image uploaded by edge_image module. Null when edge_image module is skipped." - value = length(module.edge_image) > 0 ? module.edge_image[0].image_id : null + description = "ID of the image uploaded by edge_image module." + value = module.edge_image.image_id } output "edge_host_metadata" { - description = "Per-node metadata (IDs and IPs). Empty map until edge hosts are provisioned." - value = length(module.edge_hosts) > 0 ? module.edge_hosts[0].host_metadata : {} + description = "Per-node metadata (IDs and IPs)." + value = module.edge_hosts.host_metadata } output "edge_network_id" { - description = "ID of the shared edge host network. Null until edge hosts are provisioned." - value = length(module.edge_hosts) > 0 ? module.edge_hosts[0].network_id : null + description = "ID of the shared edge host network." + value = module.edge_hosts.network_id } output "edge_security_group_id" { - description = "ID of the shared edge host security group. Null until edge hosts are provisioned." - value = length(module.edge_hosts) > 0 ? module.edge_hosts[0].security_group_id : null + description = "ID of the shared edge host security group." + value = module.edge_hosts.security_group_id } {{ end }} diff --git a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt index 821841be..b118fcd4 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/terraform/providers/stackit/example/infrastructure/variables.tf.tplt @@ -149,35 +149,31 @@ variable "expiration" { {{ if (eq .cluster.terraform.kubernetesType "edge") }} ### Edge instance variables variable "edge_instance" { - description = "Edge instance behavior. You can create a new instance or reuse an existing instance_id." + description = "Edge Cloud instance created by Terraform." type = object({ - create = optional(bool, true) - instance_id = optional(string, "") - plan = optional(string, "preview") display_name = optional(string, "edge") - region = optional(string, "") description = optional(string, "") + expiration = optional(number, 86400) }) - default = {} validation { - condition = !try(var.edge_instance.create, true) || trimspace(try(var.edge_instance.display_name, "")) != "" - error_message = "edge_instance.display_name must be set when edge_instance.create is true." + condition = trimspace(var.edge_instance.display_name) != "" + error_message = "edge_instance.display_name must be set." } validation { - condition = trimspace(try(var.edge_instance.display_name, "")) == "" || can(regex("^[a-z0-9][a-z0-9-]{2,6}[a-z0-9]$", trimspace(try(var.edge_instance.display_name, "")))) - error_message = "edge_instance.display_name must be empty or a valid hostname label with 4 to 8 characters." + condition = can(regex("^[a-z0-9][a-z0-9-]{2,6}[a-z0-9]$", trimspace(var.edge_instance.display_name))) + error_message = "edge_instance.display_name must be a valid hostname label with 4 to 8 characters." } + } ### Edge image variables variable "edge_image" { - description = "Edge image upload settings. Set create=true to enable upload." + description = "Edge image artifact uploaded by Terraform." type = object({ - create = optional(bool, false) local_file_path = optional(string, "") - name = optional(string, "") + name = optional(string, "talos-edge") disk_format = optional(string, "raw") min_disk_size = optional(number, 30) operating_system = optional(string, "linux") @@ -187,22 +183,19 @@ variable "edge_image" { default = {} validation { - condition = !try(var.edge_image.create, false) || trimspace(try(var.edge_image.local_file_path, "")) != "" - error_message = "edge_image.local_file_path must be set when edge_image.create is true." + condition = trimspace(var.edge_image.local_file_path) != "" + error_message = "edge_image.local_file_path must be set." } } ### Edge host variables variable "edge_hosts" { - description = "Edge host infrastructure definitions. Set create=true to enable host provisioning. edge_hosts.image_id must be set for host provisioning." + description = "Edge host infrastructure created by Terraform." type = object({ - create = optional(bool, false) - image_id = optional(string, "") name_prefix = optional(string, "") network_name = optional(string, "") security_group_name = optional(string, "") - ipv4_prefix = optional(string, "10.0.50.0") - ipv4_prefix_length = optional(number, 24) + ipv4_prefix = optional(string, "10.0.50.0/24") ipv4_nameservers = optional(list(string), ["1.1.1.1", "1.0.0.1", "8.8.8.8"]) ingress_tcp_ports = optional(list(number), [80, 443]) common_labels = optional(map(string), {}) @@ -220,17 +213,22 @@ variable "edge_hosts" { default = {} validation { - condition = !try(var.edge_hosts.create, false) || length(try(var.edge_hosts.nodes, [])) == 0 || trimspace(try(var.edge_hosts.image_id, "")) != "" - error_message = "edge_hosts.image_id must be set when edge_hosts.create is true and edge_hosts.nodes are configured." + condition = length(var.edge_hosts.nodes) > 0 + error_message = "edge_hosts.nodes must contain at least one host definition." + } + + validation { + condition = can(cidrhost(var.edge_hosts.ipv4_prefix, 0)) + error_message = "edge_hosts.ipv4_prefix must be valid CIDR notation, for example 10.0.50.0/24." } validation { - condition = !try(var.edge_hosts.create, false) || length(try(var.edge_hosts.nodes, [])) == 0 || ( - trimspace(try(var.edge_hosts.name_prefix, "")) != "" && - trimspace(try(var.edge_hosts.network_name, "")) != "" && - trimspace(try(var.edge_hosts.security_group_name, "")) != "" + condition = ( + trimspace(var.edge_hosts.name_prefix) != "" && + trimspace(var.edge_hosts.network_name) != "" && + trimspace(var.edge_hosts.security_group_name) != "" ) - error_message = "edge_hosts.name_prefix, edge_hosts.network_name and edge_hosts.security_group_name must be set when edge_hosts.nodes are configured." + error_message = "edge_hosts.name_prefix, edge_hosts.network_name and edge_hosts.security_group_name must be set." } } {{- end }} diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md index e72b8234..b357b05b 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md @@ -223,37 +223,107 @@ This provisions the Kubernetes cluster and the shared infrastructure. Edge follows an ordered flow and usually requires multiple applies: -Set all Edge module toggles in: +Configure the generated Edge settings in: - `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` -In many setups, you can skip `edge_image` and reuse an existing image ID via `edge_hosts.image_id`. +The generated Terraform example is create-only: it creates a new Edge Cloud instance, uploads one local image artifact, and creates the example VM/network-based hosts. The generated `edge_hosts.nodes` defaults to a single-node bootstrap example (`controlplane` with public IP). -1. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` (create or reuse). -2. Keep `edge_image.create = false` for the first apply. -3. Keep `edge_hosts.create = false` for the first apply unless `edge_hosts.image_id` is already set and you want direct host provisioning. -4. Run first apply: +This cannot be a single normal `terraform apply` from an empty environment. +The local image artifact is generated by STEC after the Edge Cloud instance exists, but Terraform needs that local artifact path before it can upload the image and create hosts. + +Phase 1: create the Edge Cloud instance. + +1. Configure `edge_instance` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars`. + Terraform reads the available Edge Cloud plan from the STACKIT API. + STEC is currently beta; the generated module expects exactly one available plan for the project and fails if the API returns zero or multiple plans. +2. Create only the Edge Cloud instance first: ```bash -terraform apply +terraform apply -target=module.edge_instance +``` + +3. Save the generated short-lived Edge Cloud kubeconfig for the following `kubectl` steps: + +```bash +terraform output -raw edge_kubeconfig_raw > edge-kubeconfig.yaml +chmod 600 edge-kubeconfig.yaml +export KUBECONFIG="$PWD/edge-kubeconfig.yaml" ``` -5. Check whether a suitable STACKIT project image already exists. If yes, set `edge_hosts.image_id`, keep `edge_image.create = false`, then continue with step 9. -6. If no suitable image exists, create an `EdgeImage` via `kubectl`, wait for `Ready`, and download the artifact. +Phase 2: generate and download the Edge image artifact. + +4. Create or select an `EdgeImage` in your STEC instance and wait until it is `Ready`. + `EdgeImage` is the Kubernetes resource kind. The examples below use the `default` namespace, matching the STACKIT documentation. If you plan to run Longhorn, include `siderolabs/iscsi-tools` and `siderolabs/util-linux-tools` in the `EdgeImage` system extensions. -7. Set `edge_image.create = true` and set `edge_image.local_file_path` in `customer-service-catalog/terraform//infrastructure/env.auto.tfvars` to the downloaded artifact. -8. Run apply and read `edge_uploaded_image_id` from output (for example `terraform output edge_uploaded_image_id`). -9. Set `edge_hosts.image_id` to that ID (or keep your existing image ID), set `edge_hosts.create = true`, and configure `edge_hosts.nodes`. -10. Run second apply (if configured, `edge_hosts` creates VM/network-based hosts): + +Phase 3: upload the image and create hosts. + +Before Terraform can upload the image, you need a local raw image file for STACKIT Compute Engine. +You can get it either through the Edge Cloud UI or through the Kubernetes API. + +5. Download the OpenStack/amd64 raw image artifact, unpack it locally, and set `edge_image.local_file_path`. + +Via UI: + +1. Open your STEC instance. +2. Go to `Images`. +3. Create an image or select an existing Ready image. +4. Click `Download`. +5. Select the OpenStack/amd64 raw image artifact. +6. Download the `.raw.xz` file. +7. Unpack it locally: + +```bash +mkdir -p edge-artifacts +mv ~/Downloads/openstack-amd64.raw.xz edge-artifacts/ +unxz edge-artifacts/openstack-amd64.raw.xz +ls edge-artifacts/openstack-amd64.raw +``` + +Via `kubectl`: + +```bash +IMAGE_NAME="kubara-edge-image" + +kubectl get EdgeImage "${IMAGE_NAME}" --namespace default -o yaml | yq '.status.artifacts' + +IMAGE_URL=$( + kubectl get EdgeImage "${IMAGE_NAME}" --namespace default -o yaml \ + | yq -r '.status.artifacts[] | select(.type == "openstack") | .artifacts[] | select(.architecture == "amd64") | .url' +) + +mkdir -p edge-artifacts +curl -L "${IMAGE_URL}" -o edge-artifacts/openstack-amd64.raw.xz +unxz edge-artifacts/openstack-amd64.raw.xz +ls edge-artifacts/openstack-amd64.raw +``` + +If the `yq` query returns an empty URL, inspect `.status.artifacts` and copy the OpenStack/amd64 `.raw.xz` URL manually. +The exact artifact list can differ by STEC version and selected target platform. + +Then set the unpacked path in `env.auto.tfvars`: + +```hcl +edge_image = { + local_file_path = "edge-artifacts/openstack-amd64.raw" + # ... +} +``` + +6. Configure `edge_hosts.nodes`. +7. Run full apply. Terraform uploads the image and creates VM/network-based hosts from that uploaded image: ```bash terraform apply ``` -11. Wait until hosts register as `EdgeHost`, then create `EdgeCluster` via `kubectl`. +Phase 4: create the EdgeCluster. -If your hosts are provisioned externally (for example bare metal, mixed environments, or multi-cloud), skip `edge_hosts` and use only the API-based `EdgeImage`/`EdgeCluster` workflow. +8. Wait until hosts register as `EdgeHost`, then create `EdgeCluster` via `kubectl`. + +If your Edge platform is provisioned externally (for example bare metal, mixed environments, or multi-cloud), skip this Terraform example and use the API-based `EdgeImage`/`EdgeCluster` workflow plus kubara Helm/GitOps. How to read image information: @@ -261,7 +331,7 @@ How to read image information: ```bash kubectl get edgeimages.edge.stackit.cloud -kubectl get edgeimage -o yaml +kubectl get EdgeImage --namespace default -o yaml ``` - List available `image-factory` Talos versions (STEC side): @@ -271,11 +341,9 @@ INSTANCE_REGION="eu01" curl https://image-factory.edge.$INSTANCE_REGION.stackit.cloud/versions ``` - These versions are not directly usable as `edge_hosts.image_id`. + These versions are not local files. Use them in `EdgeImage.spec.talosVersion`; then download the generated artifact and set that path as `edge_image.local_file_path`. -- For Terraform host provisioning, you need a STACKIT project image ID for `edge_hosts.image_id`: - - copy an existing ID from your STACKIT project image list (UI), or - - use the Terraform upload module once and read: +- Read the uploaded STACKIT project image ID after Terraform apply: ```bash terraform output edge_uploaded_image_id @@ -294,10 +362,10 @@ terraform output edge_host_metadata - If MetalLB is enabled, generated customer values currently use `clusters[].privateLoadBalancerIP` (`/32`) as pool address. - `publicLoadBalancerIP` is currently not wired into generated MetalLB values. -If you use an existing image for Longhorn and know the related `EdgeImage`, verify that these extensions are present in that image configuration: +If you use a manually prepared image outside this Terraform example and know the related `EdgeImage`, verify that these extensions are present in that image configuration: ```bash -kubectl get edgeimage -o yaml +kubectl get EdgeImage --namespace default -o yaml # check spec.schematic -> customization.systemExtensions.officialExtensions # expected: siderolabs/iscsi-tools and siderolabs/util-linux-tools ``` @@ -310,9 +378,9 @@ kubectl get edgeimage -o yaml - Vault (Secrets Manager) - Secrets in Vault - Buckets -- Edge Cloud instance (optional) -- Edge image upload (optional) -- Edge host VM/network provisioning (optional) +- Edge Cloud instance for the generated Edge example +- Edge image upload for the generated Edge example +- Edge host VM/network provisioning for the generated Edge example - And more... --- @@ -326,21 +394,22 @@ Edge Cloud setups are usually individual. Depending on your environment, you mig - run a mixed model (VM + bare metal), - or run hosts across multiple clouds. -kubara shows one example path and the Edge Terraform modules are optional: +kubara shows one example path. The generated Edge Terraform modules are create-only: -- `edge_instance` is optional (`create` toggles the module; use `create = false` + `instance_id` to reuse an existing instance) -- `edge_image` is optional (`create` toggles the module; upload runs only if `local_file_path` is set) -- `edge_hosts` is optional (`create` toggles the module; host provisioning runs only if `nodes` is not empty) -- for `edge_hosts`, set `edge_hosts.image_id` explicitly (existing image or `edge_uploaded_image_id` from a prior upload apply) +- `edge_instance` creates a new Edge Cloud instance +- `edge_image` uploads one local image artifact +- `edge_hosts` creates VM/network-based hosts from the uploaded image You can also skip Edge Terraform entirely and use only the kubara Helm/GitOps stack. -`EdgeImage` and `EdgeCluster` are managed via Kubernetes API (`kubectl`) and are intentionally outside Terraform state in this setup. +`EdgeImage` and `EdgeCluster` are intentionally outside Terraform state in this setup. +The examples use Kubernetes API manifests with `kubectl` because that is easy to automate and keep in Git, but you can also create and manage them manually through the Edge Cloud UI or with STEC CLI/API workflows. Important distinction: -- `EdgeImage` and `image-factory` versions describe STEC boot artifacts and profiles. -- `edge_hosts.image_id` must be a STACKIT project image ID (Compute/IaaS image), not an `image-factory` version string. +- In STEC, `EdgeImage` defines how the Talos boot image should be built, including Talos version and system extensions. +- STEC then generates a downloadable image artifact and exposes it through `EdgeImage.status.artifactUrl`. +- Terraform does not use the `EdgeImage` object directly. `edge_image.local_file_path` must point to the downloaded local artifact file. STEC (STACKIT Edge Cloud) can be managed through UI, CLI, or API: @@ -401,7 +470,18 @@ Then use it: KUBECONFIG=~/.kube/.yaml kubectl get nodes ``` -For Edge, Terraform outputs infrastructure metadata only (for example `edge_instance_id`, `edge_frontend_url`, `edge_status`, `edge_host_metadata`). +For Edge, Terraform also outputs a short-lived sensitive kubeconfig: + +```bash +terraform output -raw edge_kubeconfig_raw > ~/.kube/-edge.yaml +chmod 600 ~/.kube/-edge.yaml +``` + +Then use it: + +```bash +KUBECONFIG=~/.kube/-edge.yaml kubectl get edgeimage +``` --- diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md index 7138ee35..ea5f294c 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/README.md @@ -15,13 +15,10 @@ module "edge_hosts" { name = "edge-demo" project_id = var.project_id - # Use an existing image ID, e.g. from STACKIT project image list. - # You can also use `terraform output edge_uploaded_image_id` - # after running the optional edge_image upload module. - image_id = "11111111-2222-3333-4444-555555555555" + image_id = module.edge_image.image_id network_name = "edge-demo-network" security_group_name = "edge-demo-sg" - ipv4_prefix = "10.0.50.0" + ipv4_prefix = "10.0.50.0/24" nodes = [ { diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf index e9047b32..88701ce5 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/main.tf @@ -1,8 +1,10 @@ locals { + # Key resources by node name so Terraform can track each host independently. nodes_by_name = { for node in var.nodes : node.name => node } + # Only nodes with assign_public_ip=true get a public IP resource. nodes_with_public_ip = { for name, node in local.nodes_by_name : name => node if node.assign_public_ip @@ -10,11 +12,10 @@ locals { } resource "stackit_network" "this" { - project_id = var.project_id - name = var.network_name - ipv4_nameservers = var.ipv4_nameservers - ipv4_prefix = var.ipv4_prefix - ipv4_prefix_length = var.ipv4_prefix_length + project_id = var.project_id + name = var.network_name + ipv4_nameservers = var.ipv4_nameservers + ipv4_prefix = var.ipv4_prefix } resource "stackit_security_group" "this" { @@ -23,6 +24,7 @@ resource "stackit_security_group" "this" { } resource "stackit_security_group_rule" "ingress_tcp" { + # Create one security-group rule per configured ingress TCP port. for_each = toset([for p in var.ingress_tcp_ports : tostring(p)]) project_id = var.project_id @@ -41,6 +43,7 @@ resource "stackit_security_group_rule" "ingress_tcp" { } resource "stackit_volume" "this" { + # Create one boot volume per configured Edge host. for_each = local.nodes_by_name project_id = var.project_id @@ -56,6 +59,7 @@ resource "stackit_volume" "this" { } resource "stackit_network_interface" "this" { + # Create one NIC per configured Edge host. for_each = local.nodes_by_name project_id = var.project_id @@ -66,6 +70,7 @@ resource "stackit_network_interface" "this" { } resource "stackit_server" "this" { + # Create one VM per configured Edge host. for_each = local.nodes_by_name project_id = var.project_id @@ -81,6 +86,7 @@ resource "stackit_server" "this" { } resource "stackit_public_ip" "this" { + # Public IPs are intentionally optional per node. for_each = local.nodes_with_public_ip project_id = var.project_id diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf index 9fbd90b9..dc02024e 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-hosts/variables.tf @@ -30,17 +30,12 @@ variable "security_group_name" { variable "ipv4_prefix" { type = string - description = "IPv4 network prefix (without mask) used for the host network." -} - -variable "ipv4_prefix_length" { - type = number - description = "IPv4 prefix length used for the host network." - default = 24 + description = "IPv4 network prefix in CIDR notation used for the host network." + default = "10.0.50.0/24" validation { - condition = var.ipv4_prefix_length >= 8 && var.ipv4_prefix_length <= 30 - error_message = "ipv4_prefix_length must be between 8 and 30." + condition = can(cidrhost(var.ipv4_prefix, 0)) + error_message = "ipv4_prefix must be valid CIDR notation, for example 10.0.50.0/24." } } diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md index 5ddcb8a5..0d71fd8f 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/README.md @@ -1,29 +1,32 @@ # STACKIT Edge Instance Module -Terraform module for creating or reusing a STACKIT Edge Cloud instance. +Terraform module for creating a STACKIT Edge Cloud instance. ## What this module does -- Creates `stackit_edgecloud_instance` when `create = true` -- Reuses an externally provided `instance_id` when `create = false` +- Creates one `stackit_edgecloud_instance` +- Reads the available Edge Cloud plan from the STACKIT API ## Usage +The STACKIT provider must have beta resources enabled. +The generated kubara infrastructure template already sets this. + ```hcl module "edge_instance" { source = "../modules/edge-instance" project_id = var.project_id - create = true display_name = "edge1234" - plan_name = "preview" region = "eu01" description = "kubara edge instance" + expiration = 86400 } ``` ## Outputs - `instance_id`: Edge Cloud instance ID -- `frontend_url`: Edge frontend URL (null for reused instances) -- `status`: Instance status (null for reused instances) +- `frontend_url`: Edge frontend URL +- `status`: Instance status +- `kubeconfig_raw`: Raw Edge Cloud kubeconfig (short-lived, sensitive) diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf index 56429124..64c6794c 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/main.tf @@ -1,37 +1,21 @@ data "stackit_edgecloud_plans" "this" { - count = var.create ? 1 : 0 project_id = var.project_id } -locals { - available_plan_names = var.create ? sort([ - for plan in data.stackit_edgecloud_plans.this[0].plans : plan.name - ]) : [] - - matching_plans = var.create ? [ - for plan in data.stackit_edgecloud_plans.this[0].plans : plan - if lower(plan.name) == lower(var.plan_name) - ] : [] -} - resource "stackit_edgecloud_instance" "this" { - count = var.create ? 1 : 0 - project_id = var.project_id region = var.region display_name = var.display_name - description = var.description == "" ? null : var.description - plan_id = length(local.matching_plans) == 1 ? local.matching_plans[0].id : null + # Send null instead of an empty string so the provider treats it as unset. + description = var.description == "" ? null : var.description + # STEC is beta and currently exposes one available plan for the project. + # one() does not choose arbitrarily; it fails if the API returns zero or multiple plans. + plan_id = one(data.stackit_edgecloud_plans.this.plans).id +} - lifecycle { - precondition { - condition = length(local.matching_plans) == 1 - error_message = format( - "Expected exactly one Edge Cloud plan match for '%s' but found %d. Available plans: %s", - var.plan_name, - length(local.matching_plans), - join(", ", local.available_plan_names), - ) - } - } +resource "stackit_edgecloud_kubeconfig" "this" { + project_id = var.project_id + region = var.region + instance_id = stackit_edgecloud_instance.this.instance_id + expiration = var.expiration } diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf index ef4fc99b..5376ac50 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/outputs.tf @@ -1,14 +1,20 @@ output "instance_id" { - description = "ID of the Edge Cloud instance (created or reused)." - value = var.create ? stackit_edgecloud_instance.this[0].instance_id : var.instance_id + description = "ID of the Edge Cloud instance." + value = stackit_edgecloud_instance.this.instance_id } output "frontend_url" { - description = "Frontend URL of the created Edge Cloud instance. Null when reusing an existing instance." - value = var.create ? stackit_edgecloud_instance.this[0].frontend_url : null + description = "Frontend URL of the created Edge Cloud instance." + value = stackit_edgecloud_instance.this.frontend_url } output "status" { - description = "Current status of the created Edge Cloud instance. Null when reusing an existing instance." - value = var.create ? stackit_edgecloud_instance.this[0].status : null + description = "Current status of the created Edge Cloud instance." + value = stackit_edgecloud_instance.this.status +} + +output "kubeconfig_raw" { + description = "Raw Edge Cloud kubeconfig (short-lived, sensitive)." + value = stackit_edgecloud_kubeconfig.this.kubeconfig + sensitive = true } diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf index 0b0d97b8..d94c7158 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/modules/edge-instance/variables.tf @@ -3,36 +3,18 @@ variable "project_id" { description = "STACKIT project ID." } -variable "create" { - type = bool - description = "Whether Terraform should create a new Edge Cloud instance. Set to false to reuse an existing instance_id." - default = true -} - -variable "instance_id" { - type = string - description = "Existing Edge Cloud instance ID to reuse when create is false." - default = "" - - validation { - condition = var.create || trimspace(var.instance_id) != "" - error_message = "instance_id must be set when create is false." - } -} - variable "display_name" { type = string description = "Display name of the Edge Cloud instance (4-8 chars, valid hostname label)." - default = "" validation { - condition = !var.create || trimspace(var.display_name) != "" - error_message = "display_name must be set when create is true." + condition = trimspace(var.display_name) != "" + error_message = "display_name must be set." } validation { - condition = var.display_name == "" || can(regex("^[a-z0-9][a-z0-9-]{2,6}[a-z0-9]$", var.display_name)) - error_message = "display_name must be empty or a valid hostname label with a length between 4 and 8 characters." + condition = can(regex("^[a-z0-9][a-z0-9-]{2,6}[a-z0-9]$", var.display_name)) + error_message = "display_name must be a valid hostname label with a length between 4 and 8 characters." } } @@ -42,14 +24,14 @@ variable "description" { default = "" } -variable "plan_name" { - type = string - description = "Name of the Edge Cloud plan to use when create is true." - default = "preview" -} - variable "region" { type = string description = "Region used for Edge Cloud resources." default = "eu01" } + +variable "expiration" { + type = number + description = "Expiration time for the kubeconfig in seconds." + default = 86400 # 24h +} From 07465344fe097e25df76c68a2a681971bba369ce Mon Sep 17 00:00:00 2001 From: Matthiator Date: Wed, 27 May 2026 18:32:03 +0200 Subject: [PATCH 4/9] fix: render edge external dns target --- .../providers/stackit_provisioning_edgecloud.md | 2 +- .../helm/example/argo-cd/values.yaml.tplt | 4 ++-- .../helm/example/homer-dashboard/values.yaml.tplt | 2 +- .../helm/example/kube-prometheus-stack/values.yaml.tplt | 2 +- .../helm/example/kyverno-policy-reporter/values.yaml.tplt | 2 +- .../helm/example/longhorn/values.yaml.tplt | 2 +- .../helm/example/oauth2-proxy/values.yaml.tplt | 2 +- .../terraform/providers/stackit/README.md | 4 ++-- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md index 32cc48c8..1fd005e4 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -66,7 +66,7 @@ terraform output edge_host_metadata Use the selected host public IP for DNS `A` records (for example your ingress hostname). If you enable MetalLB, the generated MetalLB customer values use `clusters[].privateLoadBalancerIP` from your `config.yaml` as pool address (`/32`). -`publicLoadBalancerIP` is currently not wired into the generated MetalLB chart values. +The generated ingress annotations for `external-dns` use `clusters[].publicLoadBalancerIP` as DNS target. ## Step-by-step sequence diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/argo-cd/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/argo-cd/values.yaml.tplt index e9bbccad..82729425 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/argo-cd/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/argo-cd/values.yaml.tplt @@ -84,7 +84,7 @@ externalSecrets: {{- $argocdUserIngressAnnotations := dig "cluster" "services" "argocd" "networking" "annotations" (dict) . -}} {{- $argocdIngressGrpcAnnotations := dict "cert-manager.io/cluster-issuer" $certManagerIssuer -}} {{- if (eq $metallbStatus "enabled") -}} -{{- $_ := set $argocdIngressGrpcAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $argocdIngressGrpcAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- $argocdIngressGrpcAnnotations = mergeOverwrite $argocdIngressGrpcAnnotations $argocdUserIngressAnnotations -}} @@ -93,7 +93,7 @@ externalSecrets: {{- $_ := set $argocdIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "oauth2-proxy-oauth-auth@kubernetescrd" -}} {{- end -}} {{- if (eq $metallbStatus "enabled") -}} -{{- $_ := set $argocdIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $argocdIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- $argocdIngressAnnotations = mergeOverwrite $argocdIngressAnnotations $argocdUserIngressAnnotations -}} diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/homer-dashboard/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/homer-dashboard/values.yaml.tplt index 4d45938c..d0637cbd 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/homer-dashboard/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/homer-dashboard/values.yaml.tplt @@ -10,7 +10,7 @@ {{- $_ := set $homerIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "oauth2-proxy-oauth-auth@kubernetescrd" -}} {{- end -}} {{- if (eq $metallbStatus "enabled") -}} -{{- $_ := set $homerIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $homerIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- $homerIngressAnnotations = mergeOverwrite $homerIngressAnnotations (dig "cluster" "services" "homer-dashboard" "networking" "annotations" (dict) .) -}} diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/kube-prometheus-stack/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/kube-prometheus-stack/values.yaml.tplt index 444d3e3b..feab01b1 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/kube-prometheus-stack/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/kube-prometheus-stack/values.yaml.tplt @@ -4,7 +4,7 @@ {{- $metallbStatus := dig "cluster" "services" "metallb" "status" "disabled" . -}} {{- $kubePromIngressAnnotations := dict "cert-manager.io/cluster-issuer" $certManagerIssuer -}} {{- if (eq $metallbStatus "enabled") -}} -{{- $_ := set $kubePromIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $kubePromIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- if (and (eq $oauth2ProxyStatus "enabled") (eq $traefikStatus "enabled")) -}} {{- $_ := set $kubePromIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "oauth2-proxy-oauth-auth@kubernetescrd" -}} diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/kyverno-policy-reporter/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/kyverno-policy-reporter/values.yaml.tplt index 2bf55687..3081f32a 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/kyverno-policy-reporter/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/kyverno-policy-reporter/values.yaml.tplt @@ -3,7 +3,7 @@ {{- $metallbStatus := dig "cluster" "services" "metallb" "status" "disabled" . -}} {{- $kyvernoPolicyReporterIngressAnnotations := dict "cert-manager.io/cluster-issuer" (dig "cluster" "services" "cert-manager" "config" "clusterIssuer" "name" "letsencrypt-staging" .) -}} {{- if (eq $metallbStatus "enabled") -}} -{{- $_ := set $kyvernoPolicyReporterIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $kyvernoPolicyReporterIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- if (and (eq $oauth2ProxyStatus "enabled") (eq $traefikStatus "enabled")) -}} {{- $_ := set $kyvernoPolicyReporterIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "oauth2-proxy-oauth-auth@kubernetescrd,\nkyverno-policy-reporter-strip-kyverno-prefix@kubernetescrd" -}} diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt index 4a214eae..8bcfe425 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt @@ -3,7 +3,7 @@ {{- $_ := set $longhornIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "oauth2-proxy-oauth-auth@kubernetescrd" -}} {{- end -}} {{- if (eq (index .cluster.services "metallb" "status") "enabled") -}} -{{- $_ := set $longhornIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $longhornIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- $longhornIngressAnnotations = mergeOverwrite $longhornIngressAnnotations (dig "cluster" "services" "longhorn" "networking" "annotations" (dict) .) -}} diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/oauth2-proxy/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/oauth2-proxy/values.yaml.tplt index 4967192a..23487e8e 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/oauth2-proxy/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/oauth2-proxy/values.yaml.tplt @@ -1,6 +1,6 @@ {{- $oauth2ProxyIngressAnnotations := dict "cert-manager.io/cluster-issuer" (dig "cluster" "services" "cert-manager" "config" "clusterIssuer" "name" "letsencrypt-staging" .) -}} {{- if (eq (index .cluster.services "metallb" "status") "enabled") -}} -{{- $_ := set $oauth2ProxyIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadbalancerIP -}} +{{- $_ := set $oauth2ProxyIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} {{- end -}} {{- $oauth2ProxyIngressAnnotations = mergeOverwrite $oauth2ProxyIngressAnnotations (dig "cluster" "services" "oauth2-proxy" "networking" "annotations" (dict) .) -}} diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md index b357b05b..3baf1948 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md @@ -359,8 +359,8 @@ terraform output edge_host_metadata ``` - Use the selected host public IP for DNS `A` records (for example your ingress hostname). -- If MetalLB is enabled, generated customer values currently use `clusters[].privateLoadBalancerIP` (`/32`) as pool address. -- `publicLoadBalancerIP` is currently not wired into generated MetalLB values. +- If MetalLB is enabled, generated customer values use `clusters[].privateLoadBalancerIP` (`/32`) as pool address. +- Generated ingress annotations for `external-dns` use `clusters[].publicLoadBalancerIP` as DNS target. If you use a manually prepared image outside this Terraform example and know the related `EdgeImage`, verify that these extensions are present in that image configuration: From 3e46202452554c4af9966e0382ae1603875e7486 Mon Sep 17 00:00:00 2001 From: Matthiator Date: Thu, 28 May 2026 10:33:29 +0200 Subject: [PATCH 5/9] docs: clarify edge ingress networking --- .../providers/stackit_provisioning_edgecloud.md | 9 +++++++-- .../terraform/providers/stackit/README.md | 5 +++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md index 1fd005e4..c26b38f0 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -65,8 +65,13 @@ terraform output edge_host_metadata Use the selected host public IP for DNS `A` records (for example your ingress hostname). -If you enable MetalLB, the generated MetalLB customer values use `clusters[].privateLoadBalancerIP` from your `config.yaml` as pool address (`/32`). -The generated ingress annotations for `external-dns` use `clusters[].publicLoadBalancerIP` as DNS target. +For the STACKIT VM-based single-node example, set these values in your `config.yaml` before running `kubara generate --helm`: + +* `clusters[].publicLoadBalancerIP`: the STACKIT public IP assigned to the VM. Generated ingress annotations use this as `external-dns` target. +* `clusters[].privateLoadBalancerIP`: the private IP of the same VM on the STACKIT network. Generated MetalLB customer values use this as pool address (`/32`). + +This works because the STACKIT public IP is routed/NATed to the selected VM, while MetalLB exposes the Kubernetes `LoadBalancer` service on the VM's private network address. +Do not use this as a production multi-node ingress design. For multi-node setups, plan a dedicated ingress architecture, for example a STACKIT Network Load Balancer in front of the nodes. ## Step-by-step sequence diff --git a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md index 3baf1948..6d7336d2 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md +++ b/src/internal/catalog/built-in/managed-service-catalog/terraform/providers/stackit/README.md @@ -359,8 +359,9 @@ terraform output edge_host_metadata ``` - Use the selected host public IP for DNS `A` records (for example your ingress hostname). -- If MetalLB is enabled, generated customer values use `clusters[].privateLoadBalancerIP` (`/32`) as pool address. -- Generated ingress annotations for `external-dns` use `clusters[].publicLoadBalancerIP` as DNS target. +- For the STACKIT VM-based single-node example, set `clusters[].publicLoadBalancerIP` to the STACKIT public IP assigned to the VM. Generated ingress annotations use this as `external-dns` target. +- Set `clusters[].privateLoadBalancerIP` to the private IP of the same VM on the STACKIT network. Generated MetalLB customer values use this as pool address (`/32`). +- This single-node path relies on the STACKIT public IP being routed/NATed to the selected VM while MetalLB exposes the Kubernetes `LoadBalancer` service on the VM's private network address. For multi-node setups, use a dedicated ingress architecture, for example a STACKIT Network Load Balancer in front of the nodes. If you use a manually prepared image outside this Terraform example and know the related `EdgeImage`, verify that these extensions are present in that image configuration: From aecdc63a41b283788264a352d1711f8f520143db Mon Sep 17 00:00:00 2001 From: Matthiator Date: Thu, 28 May 2026 10:54:49 +0200 Subject: [PATCH 6/9] fix: expose longhorn behind traefik subpath --- .../helm/example/longhorn/values.yaml.tplt | 13 +++++++-- .../templates/traefik-middlewares.yaml | 28 +++++++++++++++++++ .../helm/longhorn/values.yaml | 3 ++ 3 files changed, 42 insertions(+), 2 deletions(-) create mode 100644 src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml diff --git a/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt b/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt index 8bcfe425..b075aefd 100644 --- a/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt +++ b/src/internal/catalog/built-in/customer-service-catalog/helm/example/longhorn/values.yaml.tplt @@ -1,6 +1,8 @@ {{- $longhornIngressAnnotations := dict "cert-manager.io/cluster-issuer" (dig "cluster" "services" "cert-manager" "config" "clusterIssuer" "name" "letsencrypt-staging" .) -}} {{- if (and (eq (index .cluster.services "oauth2-proxy" "status") "enabled") (eq (index .cluster.services "traefik" "status") "enabled")) -}} -{{- $_ := set $longhornIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "oauth2-proxy-oauth-auth@kubernetescrd" -}} +{{- $_ := set $longhornIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "longhorn-redirect-noslash@kubernetescrd,oauth2-proxy-oauth-auth@kubernetescrd,longhorn-strip-prefix@kubernetescrd" -}} +{{- else if (eq (index .cluster.services "traefik" "status") "enabled") -}} +{{- $_ := set $longhornIngressAnnotations "traefik.ingress.kubernetes.io/router.middlewares" "longhorn-redirect-noslash@kubernetescrd,longhorn-strip-prefix@kubernetescrd" -}} {{- end -}} {{- if (eq (index .cluster.services "metallb" "status") "enabled") -}} {{- $_ := set $longhornIngressAnnotations "external-dns.alpha.kubernetes.io/target" .cluster.publicLoadBalancerIP -}} @@ -32,10 +34,17 @@ longhorn: host: {{ .cluster.dnsName }} tls: true tlsSecret: tls-longhorn - path: "/longhorn(/|$)(.*)" # see path problems https://github.com/longhorn/longhorn/issues/1745 + path: /longhorn + pathType: Prefix annotations: {{- toYaml $longhornIngressAnnotations | nindent 6 }} +{{- if (eq (index .cluster.services "traefik" "status") "enabled") }} +traefikPathRewrite: + enabled: true + pathPrefix: /longhorn +{{- end }} + namespace: labels: project-name: "{{ .cluster.name }}" diff --git a/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml b/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml new file mode 100644 index 00000000..3753e380 --- /dev/null +++ b/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml @@ -0,0 +1,28 @@ +{{- with .Values.traefikPathRewrite }} +{{- if .enabled }} +{{- $prefix := .pathPrefix | default "/longhorn" }} +# Longhorn UI uses root-relative assets; when exposed below a path prefix, +# Traefik has to normalize the URL before forwarding to longhorn-frontend. +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: redirect-noslash + namespace: {{ $.Release.Namespace }} +spec: + redirectRegex: + permanent: true + regex: "^(https?://[^/]+{{ $prefix }})$" + replacement: "${1}/" +--- +apiVersion: traefik.io/v1alpha1 +kind: Middleware +metadata: + name: strip-prefix + namespace: {{ $.Release.Namespace }} +spec: + replacePathRegex: + regex: "^{{ $prefix }}/(.*)" + replacement: "/${1}" +{{- end }} +{{- end }} diff --git a/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/values.yaml b/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/values.yaml index 7fa5afb0..8777d1b0 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/values.yaml +++ b/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/values.yaml @@ -1,5 +1,8 @@ --- namespace: {} +traefikPathRewrite: + enabled: false + pathPrefix: /longhorn longhorn: ingress: enabled: false From 145d750252820b4728f8aac42c66ff7ab6ad71cb Mon Sep 17 00:00:00 2001 From: Matthiator Date: Thu, 28 May 2026 10:59:34 +0200 Subject: [PATCH 7/9] docs: reference longhorn subpath issue --- .../helm/longhorn/templates/traefik-middlewares.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml b/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml index 3753e380..d6dbaac5 100644 --- a/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml +++ b/src/internal/catalog/built-in/managed-service-catalog/helm/longhorn/templates/traefik-middlewares.yaml @@ -3,6 +3,7 @@ {{- $prefix := .pathPrefix | default "/longhorn" }} # Longhorn UI uses root-relative assets; when exposed below a path prefix, # Traefik has to normalize the URL before forwarding to longhorn-frontend. +# See https://github.com/longhorn/longhorn/issues/1745 for the upstream context. --- apiVersion: traefik.io/v1alpha1 kind: Middleware From bda52f47b0df7d4115832755436ef2c46e1b74e5 Mon Sep 17 00:00:00 2001 From: "fabian-patrice.schmitt" <21021337+fabian-schmitt@users.noreply.github.com> Date: Thu, 28 May 2026 11:22:53 +0200 Subject: [PATCH 8/9] more precise phrasing --- .../providers/stackit_provisioning_edgecloud.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md index c26b38f0..d707edff 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -44,7 +44,7 @@ Configure the generated Edge settings in: The generated `edge_hosts.nodes` defaults to one control plane node as a bootstrap example. This is intentionally simple for initial rollout and troubleshooting. -For production and multi-node topologies, design networking explicitly for your environment: +For production and multi-node topologies, you have to design networking depending on your environment: * where hosts run (VMs, bare metal, mixed, multi-cloud) * ingress entry strategy (single public node, MetalLB VIP, external NLB, etc.) From 89ccf68fd66e6fc8988ae872c021ccce4e6b3a67 Mon Sep 17 00:00:00 2001 From: Matthiator Date: Thu, 28 May 2026 12:06:51 +0200 Subject: [PATCH 9/9] docs: address edge provisioning review feedback --- docs/content/1_getting_started/providers/stackit.md | 3 +-- .../providers/stackit_provisioning_edgecloud.md | 10 +++++----- .../providers/stackit_terraform_bootstrap.md | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/docs/content/1_getting_started/providers/stackit.md b/docs/content/1_getting_started/providers/stackit.md index 3a29f18b..be0e4695 100644 --- a/docs/content/1_getting_started/providers/stackit.md +++ b/docs/content/1_getting_started/providers/stackit.md @@ -1,7 +1,6 @@ # STACKIT Cloud and Edge setup -We recommend using Terraform to provision your Kubernetes and additional infrastructure like a Secret Manager instance. -For this purpose we provide the necessary Terraform configuration and modules. +For STACKIT-based setups, kubara provides Terraform configuration and modules to provision Kubernetes and additional infrastructure like a Secret Manager instance. !!! warning In kubara version `0.2.0`, Terraform generation does not merge user-customized Terraform values and will overwrite existing Terraform files. diff --git a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md index d707edff..b64da7c7 100644 --- a/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md +++ b/docs/content/1_getting_started/providers/stackit_provisioning_edgecloud.md @@ -17,11 +17,11 @@ Choose the operating model that fits your team. kubara examples use manifest-bas ## Terraform modules for the kubara Edge example -The generated Edge Terraform example is intentionally create-only: +The generated Edge Terraform example intentionally creates only the following resources: * `edge_instance` creates a new Edge Cloud instance. * `edge_image` uploads one local image artifact to STACKIT. -* `edge_hosts` creates the example VM/network-based hosts and uses the uploaded image from `edge_image`. +* `edge_hosts` creates the example STACKIT VM hosts and the required network resources, using the uploaded image from `edge_image`. If your Edge platform is already provisioned externally, you can skip Terraform Edge modules completely and continue with kubara Helm/GitOps workflows only. For example, this is common when you run bare metal, mixed VM/bare-metal environments, or hosts in another cloud. @@ -44,10 +44,10 @@ Configure the generated Edge settings in: The generated `edge_hosts.nodes` defaults to one control plane node as a bootstrap example. This is intentionally simple for initial rollout and troubleshooting. -For production and multi-node topologies, you have to design networking depending on your environment: +For production and multi-node topologies, design networking based on your environment: * where hosts run (VMs, bare metal, mixed, multi-cloud) -* ingress entry strategy (single public node, MetalLB VIP, external NLB, etc.) +* external reachability strategy (single public node, MetalLB VIP, provider-managed Network Load Balancer in front of the nodes, etc.) * node-to-node traffic and return paths There is no one-size-fits-all edge network design. @@ -236,7 +236,7 @@ edge_image = { ``` 6. Configure `edge_hosts.nodes`. -7. Run full apply. Terraform uploads the image and creates the VM/network-based hosts from that uploaded image: +7. Run full apply. Terraform uploads the image and creates the configured STACKIT VM hosts and their network resources from that uploaded image: === "Terraform" diff --git a/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md b/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md index f7557a7d..15d2888b 100644 --- a/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md +++ b/docs/content/1_getting_started/providers/stackit_terraform_bootstrap.md @@ -1,7 +1,6 @@ # STACKIT Terraform Bootstrap -We recommend using Terraform to provision your Kubernetes and additional infrastructure like a Secret Manager instance. -For this purpose we provide the necessary Terraform configuration and modules. +For STACKIT-based setups, kubara provides Terraform configuration and modules to provision Kubernetes and additional infrastructure like a Secret Manager instance. !!! warning In kubara version `0.2.0`, this step does not merge user-customized Terraform values and will overwrite existing Terraform files.