From 80e081f8fe4675e7605e306e93334240997f682c Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Fri, 12 Jun 2026 16:40:30 +0200 Subject: [PATCH 1/4] examples: add alb-tls-examples showcase --- examples/alb-tls-examples/LICENSE | 73 ++++ examples/alb-tls-examples/README.md | 175 ++++++++++ examples/alb-tls-examples/alb-k8s/.gitignore | 32 ++ examples/alb-tls-examples/alb-k8s/README.md | 280 +++++++++++++++ .../alb-k8s/docs/architecture.md | 144 ++++++++ .../cert-manager/00-stackit-sa-secret.yaml | 33 ++ .../cert-manager/01-cluster-issuer.yaml | 23 ++ .../cert-manager/02-certificate.yaml | 21 ++ .../kubernetes/nginx/00-namespace.yaml | 7 + .../kubernetes/nginx/01-deployment.yaml | 63 ++++ .../alb-k8s/kubernetes/nginx/02-service.yaml | 21 ++ .../alb-k8s/kubernetes/nginx/03-ingress.yaml | 27 ++ .../alb-k8s/scripts/deploy.sh | 125 +++++++ .../alb-k8s/terraform/00-backend.tf | 9 + .../alb-k8s/terraform/00-provider.tf | 33 ++ .../alb-k8s/terraform/01-variables.tf | 98 ++++++ .../terraform/02-resource-hierarchy.tf | 29 ++ .../alb-k8s/terraform/03-network.tf | 18 + .../alb-k8s/terraform/04-compute.tf | 57 ++++ .../alb-k8s/terraform/05-dns.tf | 22 ++ .../alb-k8s/terraform/06-outputs.tf | 63 ++++ .../alb-k8s/terraform/07-alb.tf | 71 ++++ .../alb-k8s/terraform/backend.conf.example | 15 + .../terraform/terraform.tfvars.example | 28 ++ .../vm-alb-certbot-letsencrypt/.gitignore | 51 +++ .../.terraform-version | 1 + .../.terraform.lock.hcl | 65 ++++ .../vm-alb-certbot-letsencrypt/00-backend.tf | 17 + .../vm-alb-certbot-letsencrypt/00-provider.tf | 34 ++ .../01-variables.tf | 179 ++++++++++ .../02-resource-hierarchy.tf | 38 +++ .../vm-alb-certbot-letsencrypt/03-network.tf | 101 ++++++ .../vm-alb-certbot-letsencrypt/04-compute.tf | 85 +++++ .../vm-alb-certbot-letsencrypt/05-dns.tf | 36 ++ .../vm-alb-certbot-letsencrypt/06-outputs.tf | 141 ++++++++ .../vm-alb-certbot-letsencrypt/07-alb.tf | 89 +++++ .../vm-alb-certbot-letsencrypt/README.md | 322 ++++++++++++++++++ .../docs/architecture.md | 135 ++++++++ .../examples/backend.conf.example | 15 + .../examples/terraform.tfvars.example | 51 +++ .../stackit-acme-alb/.env.default | 20 ++ .../stackit-acme-alb/.gitignore | 64 ++++ .../stackit-acme-alb/Dockerfile | 71 ++++ .../stackit-acme-alb/README.md | 201 +++++++++++ .../app/Main_CertRenew_CLI.ps1 | 263 ++++++++++++++ .../app/lib/Get-StackitAlbCertStatus_CLI.ps1 | 94 +++++ .../app/lib/Stackit-CleanupHook.ps1 | 70 ++++ .../app/lib/Stackit-DnsHook.ps1 | 75 ++++ .../app/lib/StackitHelper_CLI.ps1 | 127 +++++++ .../docs/images/mode1_detail_level.png | Bin 0 -> 34827 bytes .../docs/images/mode1_high_level.png | Bin 0 -> 14067 bytes .../docs/images/mode2_detail_level.png | Bin 0 -> 58038 bytes .../docs/images/mode2_high_level.png | Bin 0 -> 25101 bytes .../templates/cloud-init.yaml.tpl | 39 +++ .../vm-alb-self-signed-cert/.gitignore | 20 ++ .../.terraform-version | 1 + .../.terraform.lock.hcl | 45 +++ .../vm-alb-self-signed-cert/00-backend.tf | 17 + .../vm-alb-self-signed-cert/00-provider.tf | 34 ++ .../vm-alb-self-signed-cert/01-variables.tf | 176 ++++++++++ .../02-resource-hierarchy.tf | 43 +++ .../vm-alb-self-signed-cert/03-network.tf | 84 +++++ .../vm-alb-self-signed-cert/04-compute.tf | 67 ++++ .../vm-alb-self-signed-cert/05-certificate.tf | 44 +++ .../vm-alb-self-signed-cert/05-dns.tf | 31 ++ .../vm-alb-self-signed-cert/06-alb.tf | 96 ++++++ .../vm-alb-self-signed-cert/07-outputs.tf | 73 ++++ .../vm-alb-self-signed-cert/README.md | 259 ++++++++++++++ .../docs/architecture.md | 128 +++++++ .../examples/backend.conf.example | 15 + .../examples/terraform.tfvars.example | 52 +++ .../templates/cloud-init.yaml.tpl | 37 ++ 72 files changed, 5073 insertions(+) create mode 100644 examples/alb-tls-examples/LICENSE create mode 100644 examples/alb-tls-examples/README.md create mode 100644 examples/alb-tls-examples/alb-k8s/.gitignore create mode 100644 examples/alb-tls-examples/alb-k8s/README.md create mode 100644 examples/alb-tls-examples/alb-k8s/docs/architecture.md create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml create mode 100644 examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml create mode 100755 examples/alb-tls-examples/alb-k8s/scripts/deploy.sh create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/03-network.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example create mode 100644 examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_detail_level.png create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_high_level.png create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_detail_level.png create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_high_level.png create mode 100644 examples/alb-tls-examples/vm-alb-certbot-letsencrypt/templates/cloud-init.yaml.tpl create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/README.md create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example create mode 100644 examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl diff --git a/examples/alb-tls-examples/LICENSE b/examples/alb-tls-examples/LICENSE new file mode 100644 index 0000000..23e2ace --- /dev/null +++ b/examples/alb-tls-examples/LICENSE @@ -0,0 +1,73 @@ +Apache License +Version 2.0, January 2004 +http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + +"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. + +"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. + +"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. + +"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. + +"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. + +"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. + +"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). + +"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. + +"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." + +"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: + + (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. + + You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + +To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. + +Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + +http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/examples/alb-tls-examples/README.md b/examples/alb-tls-examples/README.md new file mode 100644 index 0000000..fb759fa --- /dev/null +++ b/examples/alb-tls-examples/README.md @@ -0,0 +1,175 @@ +# alb-tls-examples + +A collection of STACKIT Application Load Balancer (ALB) showcases with different TLS strategies — from self-signed to Let's Encrypt, from a single VM to Kubernetes. + +Each subfolder is a self-contained, runnable Terraform showcase with its own README, state, and variables. The showcases are intentionally independent — no shared state, no shared modules. + +--- + +## Overview + +``` +alb-tls-examples/ +│ +├── vm-alb-self-signed-cert/ ← Starting point: 1 VM + ALB + Self-Signed Cert (Terraform) +├── vm-alb-certbot-letsencrypt/ ← Production: VM + ALB + Let's Encrypt via certbot + ACME DNS-01 +└── alb-k8s/ ← Kubernetes: SKE + cert-manager + Let's Encrypt +``` + +--- + +## Showcase Comparison + +| | `vm-alb-self-signed-cert` | `vm-alb-certbot-letsencrypt` | `alb-k8s` | +| ------------------------- | ---------------------------- | ----------------------------- | ----------------------------- | +| **Goal** | Getting started / quickstart | Production-grade | Kubernetes path | +| **Certificate** | Self-signed (Terraform) | Let's Encrypt (certbot) | Let's Encrypt (cert-manager) | +| **Backend** | 1 VM + Docker nginx | 1 VM + Docker nginx | SKE cluster + nginx Pod | +| **Terraform** | Yes | Yes (Phase 1) | Yes | +| **Docker** | Yes (nginx) | Yes (nginx + certbot) | No | +| **Kubernetes** | No | No | Yes (SKE) | +| **Auto-renewal** | No (re-apply) | Yes (cron on VM) | Yes (cert-manager) | +| **External dependencies** | None | Let's Encrypt, DNS delegation | Let's Encrypt, DNS delegation | +| **Time to HTTPS** | ~5 min | ~20 min | ~20 min | + +--- + +## Learning Path + +**1. [`vm-alb-self-signed-cert/`](vm-alb-self-signed-cert/README.md)** + +- How does a STACKIT ALB work? +- How is a TLS certificate attached to the ALB? +- How does the ALB terminate HTTPS and forward to a backend? + +**2. [`vm-alb-certbot-letsencrypt/`](vm-alb-certbot-letsencrypt/README.md)** + +- How do I replace a self-signed cert with a trusted one? +- How does the ACME DNS-01 challenge work with STACKIT DNS? +- How does automatic certificate renewal work via certbot in Docker? + +**3. [`alb-k8s/`](alb-k8s/README.md)** + +- How does the same work on Kubernetes? +- How do cert-manager, Ingress, and STACKIT SKE interact? + +--- + +## Common Prerequisites + +| Requirement | Details | +| ----------------------- | -------------------------------------------------------- | +| STACKIT account | Access to the STACKIT Portal | +| Terraform | >= 1.5.7 (recommended: use `tfenv`) | +| STACKIT CLI | For image UUIDs, project IDs, debugging | +| SSH key pair | Ed25519 or RSA — only the public key goes into Terraform | +| STACKIT service account | JSON key with the roles required for the showcase | +| STACKIT Object Storage | For the S3-compatible Terraform remote state backend | + +### Create a service account + +```bash +stackit iam service-account create \ + --project-id \ + --name "tf-workshop-sa" + +mkdir -p keys +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > keys/sa-key.json +``` + +### Useful CLI commands + +```bash +# List available Debian 12 images +stackit image list --all --project-id + +# List available machine types +stackit server machine-type list --project-id + +# List projects +stackit project list + +# Find your egress IP (for admin_cidr) +curl -s https://ifconfig.schwarz +``` + +--- + +## Showcase Descriptions + +### [`vm-alb-self-signed-cert/`](vm-alb-self-signed-cert/README.md) + +**Introductory showcase — recommended starting point.** + +A single Debian 12 VM with Docker and `nginx:alpine` behind a STACKIT Application Load Balancer. The TLS certificate is self-signed and fully managed by Terraform — no external tools, no DNS delegation required. + +``` +Internet → ALB (HTTPS :443, Self-Signed Cert) → VM (Docker nginx :80) +``` + +- Terraform generates RSA key + self-signed cert (`hashicorp/tls` provider) +- Terraform uploads the certificate via `stackit_alb_certificate` +- The ALB terminates TLS, the VM receives plain HTTP +- STACKIT DNS Zone + A-Record created as part of the same apply + +**When to use:** When you want to understand the ALB + TLS mechanism without extra complexity. + +--- + +### [`vm-alb-certbot-letsencrypt/`](vm-alb-certbot-letsencrypt/README.md) + +**Production showcase with automatic certificate renewal.** + +Same infrastructure as `vm-alb-self-signed-cert`, but with a full ACME pipeline on the VM. Terraform provisions the ALB without a certificate; a certbot Docker container then issues and renews Let's Encrypt certificates via DNS-01 challenge. + +``` +Phase 1 (Terraform): VM + ALB (HTTP + target) + DNS Zone +Phase 2 (certbot): ACME DNS-01 → Let's Encrypt Cert → ALB HTTPS listener +Phase 3+ (cron): Monthly automatic renewal +``` + +- Requires a delegated DNS zone (set NS records at your registrar) +- `lifecycle { ignore_changes = [listeners] }` allows certbot to update the ALB outside of Terraform + +**When to use:** When you want a production-realistic, fully automated certificate lifecycle. + +--- + +### [`alb-k8s/`](alb-k8s/README.md) + +**Kubernetes showcase with STACKIT SKE and cert-manager.** + +Terraform creates a STACKIT Kubernetes Engine (SKE) cluster. nginx is deployed as a Kubernetes Deployment, the STACKIT NLB acts as the ingress point. cert-manager handles automatic certificate management via Let's Encrypt DNS-01 with STACKIT DNS. + +``` +Internet → STACKIT NLB (L4, auto) → Traefik IC (L7, in-cluster) → nginx Pod + cert-manager (Let's Encrypt DNS-01) +``` + +**When to use:** When you want to show how TLS works on a Kubernetes-based platform with STACKIT. + +--- + +## Common Architecture Principles + +**TLS termination at the load balancer** +The ALB (or NLB + Traefik IC on Kubernetes) terminates HTTPS. Backend VMs or pods only receive plain HTTP on port 80 over the private network. + +**Infrastructure as Code** +All resources are managed via Terraform. No manual clicking in the portal. + +**Gitignored secrets** +`terraform.tfvars`, `backend.conf`, and `keys/` are gitignored in every showcase. No secret ends up in the repository. + +--- + +## References + +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [hashicorp/tls Provider](https://registry.terraform.io/providers/hashicorp/tls/latest/docs) +- [cert-manager](https://cert-manager.io/docs/) diff --git a/examples/alb-tls-examples/alb-k8s/.gitignore b/examples/alb-tls-examples/alb-k8s/.gitignore new file mode 100644 index 0000000..8879cd5 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/.gitignore @@ -0,0 +1,32 @@ +# Terraform +terraform/.terraform/ +terraform/.terraform.lock.hcl +terraform/terraform.tfstate +terraform/terraform.tfstate.backup +terraform/*.tfstate* +terraform/*.tfvars +terraform/backend.conf + +# Kubeconfig (contains cluster credentials) +.kubeconfig +kubeconfig +*.kubeconfig + +# STACKIT Service Account Keys (sensitive) +keys/ +secrets/ +sa.json +*.sa.json + +# Local environment files +.env +.env.local + +# macOS +.DS_Store + +# Editor +.idea/ +.vscode/ +*.swp +*.swo diff --git a/examples/alb-tls-examples/alb-k8s/README.md b/examples/alb-tls-examples/alb-k8s/README.md new file mode 100644 index 0000000..0923b8d --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/README.md @@ -0,0 +1,280 @@ +# SKE TLS Showcase — NLB + Traefik + cert-manager + +Deploys a STACKIT SKE cluster with a Traefik Ingress Controller behind an auto-provisioned NLB. +TLS is handled in-cluster by cert-manager (Let's Encrypt DNS-01 via STACKIT webhook) — no ALB involved. +Serves as a conceptual baseline for the upcoming STACKIT ALB integration on Kubernetes. + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant K8s as Kubernetes / SKE + participant CM as cert-manager + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Project + SKE Cluster + DNS Zone + TF-->>User: cluster_endpoint · zone_id + + Note over User,K8s: Phase 2 — deploy.sh + + User->>K8s: install Traefik Ingress Controller (Helm) + K8s-->>User: nlb_ip (auto-provisioned by SKE) + + User->>TF: create DNS A-Record (domain → nlb_ip) + TF-->>User: zone ready + + User->>K8s: install cert-manager + STACKIT webhook (Helm) + User->>K8s: apply SA Secret + ClusterIssuer + + User->>K8s: deploy nginx app + Ingress + Certificate + + Note over K8s,LE: Phase 3 — cert-manager (automatic) + + CM->>LE: request certificate (DNS-01 challenge) + LE-->>CM: .crt · .pem + + CM->>K8s: store in Secret nginx-tls + K8s-->>User: ✅ https://your-domain ready +``` + +``` +Client → STACKIT DNS → STACKIT NLB (auto) → Traefik Ingress Controller → nginx Pod + └── TLS: cert-manager + Let's Encrypt +``` + +--- + +## Overview + +| Component | Description | +| ---------------------- | ------------------------------------------------------------------------ | +| Resource hierarchy | Project under an existing STACKIT organisation | +| Kubernetes | STACKIT Kubernetes Engine (SKE) cluster | +| Network | NLB auto-provisioned by SKE when Traefik LoadBalancer Service is created | +| Ingress | Traefik Ingress Controller (Helm) | +| Certificate automation | cert-manager + STACKIT DNS-01 webhook (Helm) | +| DNS | STACKIT primary zone + A-record pointing to the NLB IP | +| Application | nginx Deployment + Service + Ingress + Certificate manifest | + +| In this showcase | Not in this showcase | +| ----------------------------------- | -------------------------------------------------- | +| cert-manager + Let's Encrypt DNS-01 | STACKIT ALB Ingress Controller (not yet available) | +| SKE cluster via Terraform | Self-signed or manually managed certs | +| Traefik Ingress Controller | ALB direct Kubernetes integration | +| STACKIT DNS-01 webhook | Multiple namespaces / production Helm values | + +--- + +## Prerequisites + +| Tool | Version | +| ----------- | ------- | +| Terraform | >= 1.3 | +| helm | >= 3.x | +| kubectl | >= 1.28 | +| STACKIT CLI | >= 0.64 | +| jq | any | + +### Required STACKIT permissions + +| Service Account | Permissions | Used by | +| --------------- | -------------------------------------- | --------------------------------- | +| Terraform SA | SKE Admin, DNS Admin, Resource Manager | `terraform apply` | +| DNS SA | DNS Admin (project-scoped) | DNS-01 challenge via cert-manager | + +Place the DNS SA key at `terraform/keys/sa-key.json`. `deploy.sh` creates the Kubernetes Secret from this file automatically. + +### Create service accounts + +```bash +# Terraform SA +stackit iam service-account create \ + --project-id \ + --name "tf-workshop-sa" + +mkdir -p terraform/keys +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > terraform/keys/sa-key.json + +# DNS SA (for cert-manager webhook) +stackit iam service-account create \ + --project-id \ + --name "dns-certmanager-sa" + +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > terraform/keys/sa-key.json +``` + +--- + +## Deployment + +### 1. Configure credentials + +```bash +cd terraform +cp backend.conf.example backend.conf # fill in access_key + secret_key +cp terraform.tfvars.example terraform.tfvars # fill in all values +``` + +### 2. Provision infrastructure + +```bash +terraform init -backend-config=backend.conf +terraform plan +terraform apply +``` + +### 3. Set kubeconfig + +```bash +export KUBECONFIG=$(pwd)/../.kubeconfig +kubectl get nodes +``` + +### 4. Deploy cluster components + +`deploy.sh` sets the DNS A record, installs Helm charts, and applies manifests. +The DNS SA key must exist at `terraform/keys/sa-key.json` (DNS Admin role required). + +```bash +cd .. && bash scripts/deploy.sh +``` + +--- + +## Validation + +```bash +# Monitor certificate issuance +kubectl describe certificate nginx-tls -n nginx-showcase + +# Test HTTPS once the certificate is Ready +curl https://. + +# Verify NLB IP is wired to DNS +dig . +``` + +--- + +## TLS Flow (DNS-01) + +1. cert-manager creates an ACME order with Let's Encrypt +2. Let's Encrypt requests a `_acme-challenge` TXT record +3. STACKIT webhook creates the TXT record via the STACKIT DNS API +4. Let's Encrypt validates and issues the certificate +5. cert-manager stores the certificate in Secret `nginx-tls` +6. Traefik reads the secret and terminates TLS +7. cert-manager auto-renews 30 days before expiry + +--- + +## File Structure + +``` +alb-k8s/ +├── terraform/ +│ ├── 00-backend.tf # S3 backend declaration +│ ├── 00-provider.tf # Provider versions + STACKIT provider config +│ ├── 01-variables.tf +│ ├── 02-resource-hierarchy.tf # Creates STACKIT folder + project +│ ├── 03-network.tf # NLB is auto-provisioned by SKE (no explicit resources) +│ ├── 04-compute.tf # SKE cluster + kubeconfig +│ ├── 05-dns.tf # DNS zone (A record set by deploy.sh) +│ ├── 06-outputs.tf +│ ├── backend.conf # Backend credentials (gitignored) +│ ├── backend.conf.example +│ ├── terraform.tfvars # Active config (gitignored) +│ └── terraform.tfvars.example +├── kubernetes/ +│ ├── cert-manager/ +│ │ ├── 00-stackit-sa-secret.yaml # SA secret template (deploy.sh creates from keys/) +│ │ ├── 01-cluster-issuer.yaml # ClusterIssuer: Let's Encrypt production +│ │ └── 02-certificate.yaml +│ └── nginx/ +│ ├── 00-namespace.yaml +│ ├── 01-deployment.yaml +│ ├── 02-service.yaml +│ └── 03-ingress.yaml +├── docs/ +│ └── architecture.md # Deployment sequence diagram + component overview +├── scripts/ +│ └── deploy.sh +└── terraform/keys/ # SA key JSON files — gitignored +``` + +--- + +## Security + +| File | Git status | Contains | +| ---------------------------- | ---------- | -------------------------- | +| `terraform/terraform.tfvars` | gitignored | Sensitive configuration | +| `terraform/backend.conf` | gitignored | Object Storage access keys | +| `terraform/keys/` | gitignored | Service account JSON keys | + +- SSH access is not directly exposed — cluster access via kubeconfig only +- Never commit SA key JSON files to the repository +- The DNS SA key is the most sensitive credential — it has write access to your DNS zone + +--- + +## Cleanup + +```bash +cd terraform && terraform destroy +``` + +> **Note:** After `terraform destroy`, STACKIT projects remain in "Pending Deletion" for up to 7 days. This is expected STACKIT platform behaviour. + +--- + +## Troubleshooting + +**Certificate stuck in Pending:** + +```bash +kubectl describe certificate nginx-tls -n nginx-showcase +kubectl describe certificaterequest -n nginx-showcase +kubectl logs -n cert-manager deploy/cert-manager +kubectl logs -n cert-manager -l app=stackit-cert-manager-webhook +``` + +Common causes: wrong `projectId` in ClusterIssuer, SA missing DNS permissions, DNS zone in wrong project. + +**Traefik has no external IP:** + +```bash +kubectl get svc -n traefik traefik +``` + +Wait 2–3 minutes — STACKIT provisions the NLB asynchronously. + +**kubeconfig expired:** + +```bash +cd terraform && terraform apply +export KUBECONFIG=$(pwd)/../.kubeconfig +``` + +--- + +## References + +- [Full architecture details](docs/architecture.md) +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [cert-manager](https://cert-manager.io/docs/) +- [stackit-cert-manager-webhook](https://github.com/stackitcloud/stackit-cert-manager-webhook) diff --git a/examples/alb-tls-examples/alb-k8s/docs/architecture.md b/examples/alb-tls-examples/alb-k8s/docs/architecture.md new file mode 100644 index 0000000..27d8ed5 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/docs/architecture.md @@ -0,0 +1,144 @@ +# Architecture: alb-k8s + +## Deployment Sequence + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant K8s as Kubernetes / SKE + participant CM as cert-manager + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Project + SKE Cluster + DNS Zone + TF-->>User: cluster_endpoint · zone_id + + Note over User,K8s: Phase 2 — deploy.sh + + User->>K8s: install Traefik Ingress Controller (Helm) + K8s-->>User: nlb_ip (auto-provisioned by SKE) + + User->>TF: create DNS A-Record (domain → nlb_ip) + TF-->>User: zone ready + + User->>K8s: install cert-manager + STACKIT webhook (Helm) + User->>K8s: apply SA Secret + ClusterIssuer + + User->>K8s: deploy nginx app + Ingress + Certificate + + Note over K8s,LE: Phase 3 — cert-manager (automatic) + + CM->>LE: request certificate (DNS-01 challenge) + LE-->>CM: .crt · .pem + + CM->>K8s: store in Secret nginx-tls + K8s-->>User: ✅ https://your-domain ready +``` + +--- + +# SKE with TLS — NLB + Traefik + cert-manager + +## Traffic Flow + +``` + Client + │ + │ DNS lookup: nginx.alb-k8s-showcase.stackit.gg + ▼ + STACKIT DNS + │ resolves to NLB public IP (set by deploy.sh) + │ + │ HTTPS :443 + ▼ + STACKIT NLB (L4, provisioned automatically by SKE) + │ + │ TCP passthrough + ▼ + Traefik Ingress Controller (L7, in-cluster) + │ TLS termination (cert from Secret: nginx-tls) + │ Host-based routing + HTTP→HTTPS redirect + ▼ + ClusterIP Service: nginx (namespace: nginx-showcase) + │ + ▼ + Pod: nginxinc/nginx-unprivileged:1.27-alpine +``` + +**NLB provisioning:** STACKIT creates the NLB automatically when the Traefik +`Service` of type `LoadBalancer` is applied. The assigned IP is dynamic; `deploy.sh` +reads it and creates the DNS A record via the STACKIT CLI. + +--- + +## TLS Certificate Flow (DNS-01) + +``` + cert-manager stackit-cert-manager-webhook STACKIT DNS API Let's Encrypt + │ │ │ │ + │── new Certificate ────────►│ │ │ + │ │ │ │ + │◄── ACME order ────────────────────────────────────────────────────────│ + │ │ │ │ + │── solve DNS-01 ───────────►│ │ │ + │ │── create TXT record ──►│ │ + │ │ _acme-challenge.nginx │ │ + │ │ .alb-k8s-showcase │ │ + │ │ .stackit.gg │ │ + │ │ │ │ + │◄── challenge ready ──────────────────────────────────────────────────│ + │ │◄── verify TXT ─────────│ │ + │ │ │ │ + │◄── certificate issued ────────────────────────────────────────────────│ + │ │ │ │ + │── delete TXT record ──────►│── delete TXT ─────────►│ │ + │ │ │ │ + │── store in Secret: nginx-tls (namespace: nginx-showcase) +``` + +cert-manager renews automatically 30 days before expiry. + +--- + +## Component Responsibility + +| Component | Provisioned by | Purpose | +| -------------------------- | -------------------------------------- | -------------------------------------------------- | +| STACKIT Folder + Project | Terraform (`02-resource-hierarchy.tf`) | Resource boundary | +| SKE Cluster | Terraform (`04-compute.tf`) | Kubernetes control plane + nodes | +| DNS Zone | Terraform (`05-dns.tf`) | `alb-k8s-showcase.stackit.gg` | +| DNS A Record | `deploy.sh` (stackit CLI) | `nginx.alb-k8s-showcase.stackit.gg → NLB IP` | +| STACKIT NLB | STACKIT (automatic on LB Service) | L4 load balancer | +| Traefik Ingress Controller | Helm (`deploy.sh`) | L7 routing + TLS termination + HTTP→HTTPS redirect | +| cert-manager | Helm (`deploy.sh`) | Certificate lifecycle | +| STACKIT DNS webhook | Helm (`deploy.sh`) | DNS-01 solver | +| SA Secret | `deploy.sh` (kubectl) | Webhook authenticates against STACKIT API | +| nginx Pod | kubectl (`deploy.sh`) | Demo workload | + +--- + +## Namespace Layout + +``` +traefik + └── Deployment: traefik (Traefik IC) + └── Service: traefik (LoadBalancer → NLB) + +cert-manager + └── Deployment: cert-manager + └── Deployment: cert-manager-webhook + └── Deployment: stackit-cert-manager-webhook + └── Secret: stackit-sa-authentication (STACKIT SA key) + └── ClusterIssuer: letsencrypt-prod + +nginx-showcase + └── Deployment: nginx + └── Service: nginx (ClusterIP) + └── Certificate: nginx-tls + └── Secret: nginx-tls (managed by cert-manager) + └── Ingress: nginx +``` + +--- diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml new file mode 100644 index 0000000..1baaeeb --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml @@ -0,0 +1,33 @@ +# Reference template only. Do NOT commit with real credentials. +# deploy.sh creates this secret automatically from terraform/keys/sa-key.json. +# To create manually: kubectl create secret generic stackit-sa-authentication \ +# --namespace cert-manager --from-file=sa.json=/path/to/sa.json +apiVersion: v1 +kind: Secret +metadata: + name: stackit-sa-authentication + namespace: cert-manager + labels: + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: cert-manager +type: Opaque +stringData: + sa.json: | + { + "id": "", + "publicKey": "-----BEGIN PUBLIC KEY-----\n\n-----END PUBLIC KEY-----", + "createdAt": "", + "validUntil": "", + "keyType": "USER_MANAGED", + "keyOrigin": "GENERATED", + "keyAlgorithm": "RSA_2048", + "active": true, + "credentials": { + "kid": "", + "iss": "", + "sub": "", + "aud": "", + "privateKey": "-----BEGIN PRIVATE KEY-----\n\n-----END PRIVATE KEY-----" # gitleaks:allow + } + } diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml new file mode 100644 index 0000000..21b74ba --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml @@ -0,0 +1,23 @@ +apiVersion: cert-manager.io/v1 +kind: ClusterIssuer +metadata: + name: letsencrypt-prod + labels: + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: cert-manager +spec: + acme: + server: https://acme-v02.api.letsencrypt.org/directory + email: + privateKeySecretRef: + name: letsencrypt-prod-account-key + solvers: + - dns01: + webhook: + solverName: stackit + groupName: acme.stackit.de + config: + projectId: + apiBasePath: https://dns.api.stackit.cloud + acmeTxtRecordTTL: 60 diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml new file mode 100644 index 0000000..a1cb5cf --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml @@ -0,0 +1,21 @@ +apiVersion: cert-manager.io/v1 +kind: Certificate +metadata: + name: nginx-tls + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: cert-manager +spec: + secretName: nginx-tls + issuerRef: + name: letsencrypt-prod + kind: ClusterIssuer + commonName: "nginx.alb-k8s-showcase.stackit.gg" + duration: 2160h0m0s + renewBefore: 720h0m0s + dnsNames: + - "nginx.alb-k8s-showcase.stackit.gg" diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml new file mode 100644 index 0000000..0455b17 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml @@ -0,0 +1,7 @@ +apiVersion: v1 +kind: Namespace +metadata: + name: nginx-showcase + labels: + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml new file mode 100644 index 0000000..a0d7767 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml @@ -0,0 +1,63 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: nginx + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: web +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + template: + metadata: + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/component: web + spec: + automountServiceAccountToken: false + terminationGracePeriodSeconds: 30 + containers: + - name: nginx + image: nginxinc/nginx-unprivileged:1.27-alpine + imagePullPolicy: IfNotPresent + ports: + - containerPort: 8080 + name: http + protocol: TCP + readinessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 5 + periodSeconds: 10 + failureThreshold: 3 + livenessProbe: + httpGet: + path: / + port: http + initialDelaySeconds: 10 + periodSeconds: 30 + failureThreshold: 3 + resources: + requests: + cpu: "50m" + memory: "32Mi" + limits: + cpu: "100m" + memory: "64Mi" + securityContext: + allowPrivilegeEscalation: false + capabilities: + drop: + - ALL + seccompProfile: + type: RuntimeDefault diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml new file mode 100644 index 0000000..62ee40e --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml @@ -0,0 +1,21 @@ +apiVersion: v1 +kind: Service +metadata: + name: nginx + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl + app.kubernetes.io/component: web +spec: + type: ClusterIP + selector: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + ports: + - name: http + protocol: TCP + port: 80 + targetPort: http diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml new file mode 100644 index 0000000..9587e52 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml @@ -0,0 +1,27 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: nginx + namespace: nginx-showcase + labels: + app.kubernetes.io/name: nginx + app.kubernetes.io/instance: nginx-showcase + app.kubernetes.io/part-of: alb-k8s-showcase + app.kubernetes.io/managed-by: kubectl +spec: + ingressClassName: traefik + rules: + - host: "nginx.alb-k8s-showcase.stackit.gg" + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: nginx + port: + name: http + tls: + - hosts: + - "nginx.alb-k8s-showcase.stackit.gg" + secretName: nginx-tls diff --git a/examples/alb-tls-examples/alb-k8s/scripts/deploy.sh b/examples/alb-tls-examples/alb-k8s/scripts/deploy.sh new file mode 100755 index 0000000..90e999d --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/scripts/deploy.sh @@ -0,0 +1,125 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" + +KUBECONFIG="$(realpath "${KUBECONFIG:-${REPO_ROOT}/.kubeconfig}")" +export KUBECONFIG + +if [[ ! -f "${KUBECONFIG}" ]]; then + echo "Error: kubeconfig not found at ${KUBECONFIG}" + exit 1 +fi + +echo "Using kubeconfig: ${KUBECONFIG}" + +HELM="helm --kubeconfig ${KUBECONFIG}" +KUBECTL="kubectl --kubeconfig ${KUBECONFIG}" + +TERRAFORM_DIR="${REPO_ROOT}/terraform" + +echo "==> [1/6] Traefik Ingress Controller" +helm repo add traefik https://traefik.github.io/charts +helm repo update traefik +${HELM} upgrade --install traefik traefik/traefik \ + --namespace traefik \ + --create-namespace \ + --set "ports.web.redirectTo.port=websecure" \ + --set service.type=LoadBalancer \ + --wait --timeout 5m + +echo " Waiting for LoadBalancer IP..." +for i in $(seq 1 24); do + LB_IP=$(${KUBECTL} get svc -n traefik traefik \ + -o jsonpath='{.status.loadBalancer.ingress[0].ip}' 2>/dev/null || true) + [[ -n "${LB_IP}" ]] && break + sleep 5 +done + +if [[ -z "${LB_IP:-}" ]]; then + echo "Error: LoadBalancer IP not assigned after 2 minutes." + exit 1 +fi +echo " LoadBalancer IP: ${LB_IP}" + +echo "==> [2/6] DNS A record" +PROJECT_ID=$(cd "${TERRAFORM_DIR}" && terraform output -raw project_id) +ZONE_ID=$(cd "${TERRAFORM_DIR}" && terraform output -raw dns_zone_id) +APP_FQDN=$(cd "${TERRAFORM_DIR}" && terraform output -raw app_fqdn) +APP_HOSTNAME="${APP_FQDN%%.*}" + +RS_ID=$(stackit dns record-set list \ + --project-id "${PROJECT_ID}" \ + --zone-id "${ZONE_ID}" \ + -o json 2>/dev/null | \ + jq -r --arg name "${APP_FQDN}." '.[] | select(.name==$name and .type=="A") | .id' 2>/dev/null || true) + +if [[ -n "${RS_ID}" ]]; then + echo " DNS A record already exists, skipping." +else + stackit dns record-set create \ + --project-id "${PROJECT_ID}" \ + --zone-id "${ZONE_ID}" \ + --name "${APP_HOSTNAME}" \ + --type A \ + --record "${LB_IP}" \ + --ttl 300 \ + --async \ + -y + echo " DNS A record created: ${APP_FQDN} → ${LB_IP}" +fi + +echo "==> [3/6] cert-manager" +helm repo add jetstack https://charts.jetstack.io +helm repo update jetstack +if ${HELM} status cert-manager --namespace cert-manager &>/dev/null; then + echo " cert-manager already installed, skipping." +else + ${HELM} install cert-manager jetstack/cert-manager \ + --namespace cert-manager \ + --create-namespace \ + --set crds.enabled=true \ + --wait --timeout 5m +fi + +echo "==> [4/6] STACKIT SA secret for cert-manager webhook" +SA_KEY="${REPO_ROOT}/terraform/keys/sa-key.json" +if [[ ! -f "${SA_KEY}" ]]; then + echo "Error: SA key not found at ${SA_KEY}" + exit 1 +fi +${KUBECTL} create secret generic stackit-sa-authentication \ + --namespace cert-manager \ + --from-file=sa.json="${SA_KEY}" \ + --dry-run=client -o yaml | ${KUBECTL} apply -f - + +echo "==> [5/6] STACKIT cert-manager webhook" +${KUBECTL} wait deployment/cert-manager-webhook \ + --namespace cert-manager \ + --for=condition=Available \ + --timeout=120s +helm repo add stackit-cert-manager-webhook \ + https://stackitcloud.github.io/stackit-cert-manager-webhook +helm repo update stackit-cert-manager-webhook +if ${HELM} status stackit-cert-manager-webhook --namespace cert-manager &>/dev/null; then + echo " stackit-cert-manager-webhook already installed, skipping." +else + ${HELM} install stackit-cert-manager-webhook \ + stackit-cert-manager-webhook/stackit-cert-manager-webhook \ + --namespace cert-manager \ + --set stackitSaAuthentication.enabled=true \ + --wait --timeout 5m +fi + +echo "==> [6/6] Kubernetes manifests" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/cert-manager/01-cluster-issuer.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/00-namespace.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/01-deployment.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/02-service.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/cert-manager/02-certificate.yaml" +${KUBECTL} apply -f "${REPO_ROOT}/kubernetes/nginx/03-ingress.yaml" + +echo "" +echo "==> Done. App: https://${APP_FQDN}" +echo " ${KUBECTL} describe certificate nginx-tls -n nginx-showcase" diff --git a/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf b/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf new file mode 100644 index 0000000..58dd5c9 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf @@ -0,0 +1,9 @@ +/*Copyright 2025 STACKIT GmbH & Co. KG + +Use of this source code is governed by an MIT-style +license that can be found in the LICENSE file or at +https://opensource.org/licenses/MIT.*/ + +terraform { + backend "s3" {} +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf b/examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf new file mode 100644 index 0000000..0653ec3 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/00-provider.tf @@ -0,0 +1,33 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.3.0" + + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.97.0" + } + local = { + source = "hashicorp/local" + version = ">= 2.4.0" + } + } +} + +provider "stackit" { + default_region = var.region + service_account_key_path = var.service_account_key_path +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf b/examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf new file mode 100644 index 0000000..7b4303e --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/01-variables.tf @@ -0,0 +1,98 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +variable "service_account_key_path" { + type = string + description = "Path to the STACKIT Service Account JSON key file (e.g. 'keys/sa-key.json')." +} + +variable "region" { + type = string + description = "STACKIT region." +} + +variable "organization_id" { + type = string + description = "STACKIT Organization ID (parent of the folder)." +} + +variable "folder_name" { + type = string + description = "Name for the STACKIT folder." +} + +variable "owner_email" { + type = string + description = "Project owner email address." +} + +variable "project_name" { + type = string + description = "Name for the new STACKIT project." +} + +variable "cluster_name" { + type = string + description = "SKE cluster name. Maximum 11 characters." +} + +variable "kubernetes_version_min" { + type = string + description = "Minimum Kubernetes version. STACKIT auto-updates within this minor version." +} + +variable "node_machine_type" { + type = string + description = "Worker node machine type (e.g. 'g2i.2' = 2 vCPU / 8 GB RAM)." +} + +variable "node_os_name" { + type = string + description = "Node OS. SKE supports 'flatcar'." +} + +variable "node_min" { + type = string + description = "Minimum node count." +} + +variable "node_max" { + type = string + description = "Maximum node count." +} + +variable "availability_zone" { + type = string + description = "Availability zone for the node pool." +} + +variable "dns_zone_name" { + type = string + description = "STACKIT DNS zone display name." +} + +variable "dns_zone_fqdn" { + type = string + description = "DNS zone FQDN (e.g. 'showcase.example.com'). Must be a domain you control." +} + +variable "dns_contact_email" { + type = string + description = "Contact email for the DNS zone." +} + +variable "app_hostname" { + type = string + description = "Hostname label for the nginx app (e.g. 'nginx' → nginx.showcase.example.com)." +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf b/examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf new file mode 100644 index 0000000..3e03fd2 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/02-resource-hierarchy.tf @@ -0,0 +1,29 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_resourcemanager_folder" "this" { + name = var.folder_name + parent_container_id = var.organization_id + owner_email = var.owner_email + + lifecycle { + ignore_changes = [labels] + } +} + +resource "stackit_resourcemanager_project" "this" { + name = var.project_name + parent_container_id = stackit_resourcemanager_folder.this.folder_id + owner_email = var.owner_email +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/03-network.tf b/examples/alb-tls-examples/alb-k8s/terraform/03-network.tf new file mode 100644 index 0000000..dee2475 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/03-network.tf @@ -0,0 +1,18 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# No explicit network resources needed. +# STACKIT NLB is provisioned automatically by SKE when the NGINX Ingress +# Controller Service of type LoadBalancer is created. The assigned IP is +# retrieved by deploy.sh and used to create the DNS A record. diff --git a/examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf b/examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf new file mode 100644 index 0000000..db4e0ec --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/04-compute.tf @@ -0,0 +1,57 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_ske_cluster" "this" { + project_id = stackit_resourcemanager_project.this.project_id + name = var.cluster_name + kubernetes_version_min = var.kubernetes_version_min + + node_pools = [ + { + name = "default" + machine_type = var.node_machine_type + os_name = var.node_os_name + minimum = var.node_min + maximum = var.node_max + availability_zones = [var.availability_zone] + volume_type = "storage_premium_perf1" + volume_size = "32" + } + ] + + maintenance = { + enable_kubernetes_version_updates = true + enable_machine_image_version_updates = true + start = "01:00:00Z" + end = "02:00:00Z" + } + + network = { + control_plane = { + access_scope = "PUBLIC" + } + } +} + +resource "stackit_ske_kubeconfig" "this" { + project_id = stackit_resourcemanager_project.this.project_id + cluster_name = stackit_ske_cluster.this.name + refresh = true +} + +resource "local_sensitive_file" "kubeconfig" { + content = stackit_ske_kubeconfig.this.kube_config + filename = "${path.module}/../.kubeconfig" + file_permission = "0600" +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf b/examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf new file mode 100644 index 0000000..19524ca --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/05-dns.tf @@ -0,0 +1,22 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_dns_zone" "this" { + project_id = stackit_resourcemanager_project.this.project_id + name = var.dns_zone_name + dns_name = var.dns_zone_fqdn + contact_email = var.dns_contact_email + type = "primary" + default_ttl = 300 +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf b/examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf new file mode 100644 index 0000000..ea07993 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/06-outputs.tf @@ -0,0 +1,63 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "project_id" { + value = stackit_resourcemanager_project.this.project_id + description = "STACKIT Project ID. Required for kubernetes/ manifests and cert-manager ClusterIssuer." +} + +output "cluster_name" { + value = stackit_ske_cluster.this.name + description = "Name of the SKE cluster." +} + +output "dns_zone_id" { + value = stackit_dns_zone.this.zone_id + description = "STACKIT DNS Zone ID." +} + +output "dns_zone_fqdn" { + value = var.dns_zone_fqdn + description = "DNS zone FQDN. The app is reachable at .." +} + +output "app_fqdn" { + value = "${var.app_hostname}.${var.dns_zone_fqdn}" + description = "Fully qualified domain name of the nginx showcase app." +} + +output "kubeconfig_path" { + value = local_sensitive_file.kubeconfig.filename + description = "Path to the generated kubeconfig." +} + +output "next_steps" { + value = <<-EOT + + ✅ Infrastructure deployed. + + 1. Set kubeconfig: + export KUBECONFIG=$(terraform output -raw kubeconfig_path) + + 2. Verify cluster: + kubectl get nodes + + 3. Note project_id for cert-manager config: + terraform output project_id + + 4. Deploy cluster components (sets DNS A record automatically): + cd .. && bash scripts/deploy.sh + EOT + description = "Next steps after terraform apply." +} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf b/examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf new file mode 100644 index 0000000..6aef16c --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/07-alb.tf @@ -0,0 +1,71 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# A native STACKIT ALB Ingress Controller for Kubernetes does not yet exist (mid-2026). +# +# Current traffic path: +# Internet → STACKIT NLB (yawol, auto-provisioned) → Traefik IC → Pod +# +# Target path when available: +# Internet → STACKIT ALB → Kubernetes Service / Ingress → Pod +# +# Expected changes: +# - ingressClassName: traefik → ingressClassName: +# - Traefik IC replaced by ALB as L7 termination point +# - TLS terminates at the ALB; cert-manager integration TBD +# - nginx.ingress.kubernetes.io/* annotations replaced by ALB equivalents +# +# For an external ALB in front of the cluster (available today), see ../alb/. + +# resource "stackit_application_load_balancer" "this" { +# project_id = stackit_resourcemanager_project.this.project_id +# region = var.region +# name = "${var.cluster_name}-alb" +# plan_id = "p10" +# external_address = "" +# +# listeners = [ +# { +# name = "https" +# port = 443 +# protocol = "PROTOCOL_HTTPS" +# https = { +# certificate_config = { +# certificate_ids = [""] +# } +# } +# http = { +# hosts = [{ +# host = var.app_hostname +# rules = [{ target_pool = "ske-nodeport" }] +# }] +# } +# } +# ] +# +# networks = [ +# { +# network_id = "" +# role = "ROLE_LISTENERS_AND_TARGETS" +# } +# ] +# +# target_pools = [ +# { +# name = "ske-nodeport" +# target_port = 30080 +# targets = [] +# } +# ] +# } diff --git a/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example b/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example new file mode 100644 index 0000000..de0f400 --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example @@ -0,0 +1,15 @@ +bucket = "tfstatealbworkshop" +key = "alb-k8s/terraform.tfstate" +region = "eu01" + +endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" +} + +access_key = "" +secret_key = "" + +skip_credentials_validation = true +skip_region_validation = true +skip_requesting_account_id = true +skip_s3_checksum = true diff --git a/examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example b/examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example new file mode 100644 index 0000000..a9be74d --- /dev/null +++ b/examples/alb-tls-examples/alb-k8s/terraform/terraform.tfvars.example @@ -0,0 +1,28 @@ +# cp terraform.tfvars.example terraform.tfvars + +# ── Auth ──────────────────────────────────────────────────────────────────── +service_account_key_path = "keys/sa-key.json" + +# ── STACKIT ───────────────────────────────────────────────────────────────── +region = "eu01" + +# ── Resource Hierarchy ────────────────────────────────────────────────────── +organization_id = "" +folder_name = "alb-k8s-showcase" +owner_email = "firstname.lastname@example.com" +project_name = "alb-k8s-showcase" + +# ── SKE Cluster ───────────────────────────────────────────────────────────── +cluster_name = "alb-k8s" +kubernetes_version_min = "1.33" +node_machine_type = "g2i.2" +node_os_name = "flatcar" +node_min = "1" +node_max = "3" +availability_zone = "eu01-1" + +# ── DNS ───────────────────────────────────────────────────────────────────── +dns_zone_name = "alb-k8s-showcase" +dns_zone_fqdn = "showcase.example.com" +dns_contact_email = "firstname.lastname@example.com" +app_hostname = "nginx" diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore new file mode 100644 index 0000000..d807818 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.gitignore @@ -0,0 +1,51 @@ +# ---> Terraform +# Local .terraform directories +**/.terraform/* + +# .tfstate files +*.tfstate +*.tfstate.* + +# Crash log files +crash.log +crash.*.log + +# Exclude all .tfvars files, which are likely to contain sensitive data, such as +# password, private keys, and other secrets. These should not be part of version +# control as they are data points which are potentially sensitive and subject +# to change depending on the environment. +*.tfvars +*.tfvars.json + +# Ignore override files as they are usually used to override resources locally and so +# are not checked in +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Ignore transient lock info files created by terraform apply +.terraform.tfstate.lock.info + +# Include override files you do wish to add to version control using negated pattern +# !example_override.tf + +# Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan +# example: *tfplan* + +# Ignore CLI configuration files +.terraformrc +terraform.rc + +.env +*.qcow2 +.DS_Store +*.bkp +.idea + +keys/* +backend.conf + +# stackit-acme-alb +stackit-acme-alb/.env +stackit-acme-alb/letsencrypt_data/ diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version new file mode 100644 index 0000000..22708fe --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform-version @@ -0,0 +1 @@ +v1.5.7 diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl new file mode 100644 index 0000000..59108ab --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/.terraform.lock.hcl @@ -0,0 +1,65 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/random" { + version = "3.6.3" + constraints = "3.6.3" + hashes = [ + "h1:zG9uFP8l9u+yGZZvi5Te7PV62j50azpgwPunq2vTm1E=", + "zh:04ceb65210251339f07cd4611885d242cd4d0c7306e86dda9785396807c00451", + "zh:448f56199f3e99ff75d5c0afacae867ee795e4dfda6cb5f8e3b2a72ec3583dd8", + "zh:4b4c11ccfba7319e901df2dac836b1ae8f12185e37249e8d870ee10bb87a13fe", + "zh:4fa45c44c0de582c2edb8a2e054f55124520c16a39b2dfc0355929063b6395b1", + "zh:588508280501a06259e023b0695f6a18149a3816d259655c424d068982cbdd36", + "zh:737c4d99a87d2a4d1ac0a54a73d2cb62974ccb2edbd234f333abd079a32ebc9e", + "zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3", + "zh:a357ab512e5ebc6d1fda1382503109766e21bbfdfaa9ccda43d313c122069b30", + "zh:c51bfb15e7d52cc1a2eaec2a903ac2aff15d162c172b1b4c17675190e8147615", + "zh:e0951ee6fa9df90433728b96381fb867e3db98f66f735e0c3e24f8f16903f0ad", + "zh:e3cdcb4e73740621dabd82ee6a37d6cfce7fee2a03d8074df65086760f5cf556", + "zh:eff58323099f1bd9a0bec7cb04f717e7f1b2774c7d612bf7581797e1622613a0", + ] +} + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = "~> 4.0" + hashes = [ + "h1:5bCU/c+2HUh7GhclzNSH6gAuoCS4inW3obEtRAwu6WQ=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.98.0" + constraints = "> 0.95.0" + hashes = [ + "h1:/FB0wBnvmjumjykX+j90kSck6LMScDaYo1STO5Vp/kw=", + "zh:031028340fbaeeb5c4c6b1d5c6d6287a70cf253cfb89f04d462a1c0ab6237ffc", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:0eee18f9a262fa58966c960f1f0863eed92cd953d0f0306ecc456b58cc2911f8", + "zh:1646966ebac0eb5d6c78ac5aa1528921d7a635f14d81300463a402c55e33cfd3", + "zh:5374ab9e5e6d837787b4f18bcf0125a1bf3ee2da40c022cc7695d6879fed111b", + "zh:6a5b9e1307055f8d358373da625ffcb4d77ec44f260d14473b10e5777380765e", + "zh:6c90090504474695ab7290d64386dd988f4fb65c90c74c9cf3a6da6226ae8a70", + "zh:8317218828f29be95ce712863646dc8968e146ec14e5ab258cb1e8f8b649245b", + "zh:9eef08e4fb7a75760f9dc8a422446f19a210ebf8177dd5aeb97444295f0120cf", + "zh:9f2147eee63feae75b96f17f3b3ebab8a29cd7164cdd08eb2bb871e5c425a77f", + "zh:b63ea754eea233292fb73d87a9810104da2bd347abf2ca0da44ac76591dcdddb", + "zh:de60bd928828a836e446f9f89e7a3bfc4e6dd73bac6827914087b34e4ad0c978", + "zh:f22d295b2e4e94ae1566e20fd752825e008a62250cf7243f1161c0bf4e986518", + "zh:f7e57bc7be2cc016983ff3ad50d2733b85e90bfaa7aa9e2192563dc9d422fb07", + ] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf new file mode 100644 index 0000000..dfc2f09 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-backend.tf @@ -0,0 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + backend "s3" {} +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf new file mode 100644 index 0000000..3a04dbd --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/00-provider.tf @@ -0,0 +1,34 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.5.0" + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.98.0" + } + random = { + source = "hashicorp/random" + version = "3.6.3" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + # required for stackit_application_load_balancer (beta resource) + enable_beta_resources = true +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf new file mode 100644 index 0000000..392343c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/01-variables.tf @@ -0,0 +1,179 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +variable "stackit_region" { + description = "STACKIT region (e.g. eu01)" + type = string + default = "eu01" +} + +variable "stackit_service_account_key_path" { + description = "Relative path to the STACKIT service account key JSON file" + type = string + default = "keys/sa-key.json" +} + +# ─── Resource Hierarchy ─────────────────────────────────────────────────────── + +variable "organization_id" { + description = "STACKIT organisation container ID — parent into which the folder is created (find in the STACKIT Portal under Organisation settings)" + type = string +} + +variable "owner_email" { + description = "Email of the resource owner; used for folder and project creation (must be an existing STACKIT user in the organisation)" + type = string +} + +variable "folder_name" { + description = "Name of the folder to create under the organisation" + type = string + default = "alb-certbot-workshop" +} + +variable "project_name" { + description = "Name of the project to create inside the folder" + type = string + default = "alb-certbot-dev" +} + +# ─── Network ────────────────────────────────────────────────────────────────── + +variable "network_name" { + description = "Name of the private network" + type = string + default = "alb-certbot-net" +} + +variable "network_cidr" { + description = "IPv4 CIDR block for the network (e.g. 10.10.0.0/24)" + type = string + default = "10.10.0.0/24" + + validation { + condition = can(cidrnetmask(var.network_cidr)) + error_message = "network_cidr must be a valid CIDR notation, e.g. 10.10.0.0/24." + } +} + +variable "admin_cidr" { + description = "Source CIDR allowed for SSH (port 22) — use your egress IP, e.g. 203.0.113.10/32. Avoid 0.0.0.0/0." + type = string + + validation { + condition = can(cidrnetmask(var.admin_cidr)) + error_message = "admin_cidr must be a valid CIDR notation, e.g. 203.0.113.10/32." + } +} + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +variable "vm_name" { + description = "Name of the Docker host VM" + type = string + default = "alb-certbot-docker-host" +} + +variable "machine_type" { + description = "STACKIT machine type — list available: stackit server machine-type list --project-id " + type = string + default = "g1.1" +} + +variable "availability_zone" { + description = "Availability zone for the VM (e.g. eu01-1, eu01-2, eu01-3)" + type = string + default = "eu01-1" +} + +variable "image_id" { + description = "UUID of the boot image (Debian 12 recommended) — list available: stackit image list --all --project-id " + type = string +} + +variable "boot_volume_size_gb" { + description = "Root disk size in GB" + type = number + default = 32 + + validation { + condition = var.boot_volume_size_gb >= 10 + error_message = "boot_volume_size_gb must be at least 10 GB." + } +} + +variable "keypair_name" { + description = "Name of the SSH key pair to register in STACKIT" + type = string + default = "alb-certbot-workshop-key" +} + +variable "ssh_public_key" { + description = "SSH public key string (ssh-ed25519 AAAA... or ssh-rsa AAAA...) — never commit the private key" + type = string + sensitive = true +} + +variable "start_nginx_test_container" { + description = "Start an nginx:alpine test container on port 80 via cloud-init (useful for ALB backend health-check validation)" + type = bool + default = true +} + +# ─── DNS ────────────────────────────────────────────────────────────────────── + +variable "dns_zone_name" { + description = "Human-readable label for the DNS zone resource" + type = string + default = "alb-certbot-workshop-zone" +} + +variable "dns_name" { + description = "DNS zone apex FQDN, e.g. workshop.example.com — must be a domain you control or can delegate" + type = string +} + +variable "dns_contact_email" { + description = "SOA contact email for the DNS zone" + type = string +} + +# ─── ALB (Option A) ─────────────────────────────────────────────────────────── + +variable "alb_name" { + description = "Name of the Application Load Balancer" + type = string + default = "alb-certbot-workshop" +} + +variable "alb_plan_id" { + description = "ALB service plan — p10 is the smallest available plan" + type = string + default = "p10" +} + + +variable "alb_target_pool_name" { + description = "Name of the ALB target pool pointing to the Docker host VM" + type = string + default = "docker-host-pool" +} + +variable "alb_acl_ranges" { + description = "List of source CIDRs allowed to reach the ALB. Use [\"0.0.0.0/0\"] to allow all traffic." + type = list(string) + default = ["0.0.0.0/0"] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf new file mode 100644 index 0000000..3aa679b --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/02-resource-hierarchy.tf @@ -0,0 +1,38 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_resourcemanager_folder" "showcase" { + name = var.folder_name + owner_email = var.owner_email + parent_container_id = var.organization_id + + labels = { + environment = "workshop" + managed-by = "terraform" + use-case = "alb-certbot" + } +} + +resource "stackit_resourcemanager_project" "showcase" { + name = var.project_name + owner_email = var.owner_email + parent_container_id = stackit_resourcemanager_folder.showcase.id + + labels = { + environment = "workshop" + managed-by = "terraform" + folder = var.folder_name + use-case = "alb-certbot" + } +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf new file mode 100644 index 0000000..c23f57c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/03-network.tf @@ -0,0 +1,101 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_network" "workshop" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.network_name + ipv4_prefix = var.network_cidr + ipv4_nameservers = ["8.8.8.8", "1.1.1.1"] + routed = true + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_security_group" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.vm_name}-sg" + description = "Security group for the ALB/Certbot workshop Docker host" + stateful = true + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_security_group_rule" "ssh_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "SSH from admin CIDR" + + protocol = { name = "tcp" } + port_range = { min = 22, max = 22 } + ip_range = var.admin_cidr +} + +resource "stackit_security_group_rule" "http_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "HTTP for ALB backend and certbot HTTP-01" + + protocol = { name = "tcp" } + port_range = { min = 80, max = 80 } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "https_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "HTTPS ingress" + + protocol = { name = "tcp" } + port_range = { min = 443, max = 443 } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_tcp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound TCP" + + protocol = { name = "tcp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_udp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound UDP" + + protocol = { name = "udp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_icmp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow outbound ICMP" + + protocol = { name = "icmp" } + ip_range = "0.0.0.0/0" +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf new file mode 100644 index 0000000..78285b8 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/04-compute.tf @@ -0,0 +1,85 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +locals { + cloud_init_rendered = templatefile("${path.root}/templates/cloud-init.yaml.tpl", { + start_nginx_test_container = var.start_nginx_test_container + }) +} + +resource "stackit_key_pair" "workshop" { + name = var.keypair_name + public_key = chomp(var.ssh_public_key) + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_network_interface" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_id = stackit_network.workshop.network_id + name = "${var.vm_name}-nic" + security_group_ids = [stackit_security_group.vm.security_group_id] + + # The ALB injects its target_security_group into this NIC after creation — + # ignore_changes prevents Terraform from reverting it on subsequent applies + lifecycle { + ignore_changes = [security_group_ids] + } +} + +resource "stackit_public_ip" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_interface_id = stackit_network_interface.vm.network_interface_id + + labels = { + environment = "workshop" + managed-by = "terraform" + } +} + +resource "stackit_server" "docker_host" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.vm_name + machine_type = var.machine_type + availability_zone = var.availability_zone + keypair_name = stackit_key_pair.workshop.name + user_data = local.cloud_init_rendered + + boot_volume = { + source_type = "image" + source_id = var.image_id + size = var.boot_volume_size_gb + } + + network_interfaces = [stackit_network_interface.vm.network_interface_id] + + agent = { + provisioning_policy = "ALWAYS" + } + + labels = { + environment = "workshop" + managed-by = "terraform" + role = "docker-host" + use-case = "alb-certbot" + } + + depends_on = [ + stackit_network_interface.vm, + stackit_key_pair.workshop, + ] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf new file mode 100644 index 0000000..0eb06cc --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/05-dns.tf @@ -0,0 +1,36 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Primary DNS zone — used for ACME DNS-01 challenge delegation +# After apply: delegate this zone at your registrar using the output nameservers +resource "stackit_dns_zone" "workshop" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.dns_zone_name + dns_name = var.dns_name + contact_email = var.dns_contact_email + type = "primary" + default_ttl = 300 + + description = "Workshop zone for ALB + Certbot ACME DNS-01 challenge" +} + +# A-record pointing the zone apex to the ALB public IP +resource "stackit_dns_record_set" "alb_a" { + project_id = stackit_resourcemanager_project.showcase.project_id + zone_id = stackit_dns_zone.workshop.zone_id + name = var.dns_name + type = "A" + ttl = 300 + records = [stackit_public_ip.alb.ip] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf new file mode 100644 index 0000000..95ad50f --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/06-outputs.tf @@ -0,0 +1,141 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "folder_id" { + description = "Container ID of the created folder" + value = stackit_resourcemanager_folder.showcase.id +} + +output "project_id" { + description = "UUID of the created project — use this for all subsequent STACKIT CLI commands" + value = stackit_resourcemanager_project.showcase.project_id +} + +output "network_id" { + description = "UUID of the private network" + value = stackit_network.workshop.network_id +} + +output "vm_public_ip" { + description = "Public IPv4 address of the Docker host" + value = stackit_public_ip.vm.ip +} + +output "dns_zone_id" { + description = "UUID of the DNS zone" + value = stackit_dns_zone.workshop.id +} + +output "dns_primary_nameserver" { + description = "Primary nameserver FQDN for the DNS zone — use for delegation and ACME DNS-01" + value = stackit_dns_zone.workshop.primary_name_server +} + +output "ssh_command" { + description = "SSH command to connect to the Docker host (Debian 12 default user)" + value = "ssh debian@${stackit_public_ip.vm.ip}" +} + +output "nginx_test_url" { + description = "URL to verify the nginx test container" + value = "http://${stackit_public_ip.vm.ip}" +} + +# ─── ALB Outputs ────────────────────────────────────────────────────────────── + +output "alb_public_ip" { + description = "Public IP address of the ALB" + value = stackit_public_ip.alb.ip +} + +output "alb_url" { + description = "HTTP URL of the ALB (DNS must be delegated first)" + value = "http://${var.dns_name}" +} + +output "alb_name" { + description = "ALB name — use to look up the public IP: stackit load-balancer describe --name --project-id " + value = stackit_application_load_balancer.workshop.name +} + +output "alb_private_address" { + description = "ALB private (internal) IP address" + value = stackit_application_load_balancer.workshop.private_address +} + +output "alb_target_security_group" { + description = "Security group ID automatically assigned to ALB targets — already injected into VM NIC" + value = stackit_application_load_balancer.workshop.target_security_group +} + +output "acme_next_steps" { + description = "Phase 2 instructions: replace the bootstrap cert with a Let's Encrypt cert via stackit-acme-alb on the VM" + value = <<-EOT + + ═══════════════════════════════════════════════════════════════ + PHASE 2 — Let's Encrypt certificate via stackit-acme-alb + VM: ${stackit_public_ip.vm.ip} | Domain: ${var.dns_name} + ═══════════════════════════════════════════════════════════════ + + PREREQUISITE: DNS delegation must be active before certbot can run. + Set NS records for ${var.dns_name} to: $(terraform output -raw dns_primary_nameserver) + Verify: dig NS ${var.dns_name} + + ── Step 1: Local — upload files to the VM ──────────────────── + + scp -r stackit-acme-alb debian@${stackit_public_ip.vm.ip}:~/stackit-acme-alb + scp keys/sa-key.json debian@${stackit_public_ip.vm.ip}:~/stackit-acme-alb/sa-key.json + + ── Step 2: Local — encode the SA key as Base64 (macOS) ─────── + + base64 -i keys/sa-key.json | tr -d '\n' + + ── Step 3: SSH to the VM and fill in .env ──────────────────── + + ssh debian@${stackit_public_ip.vm.ip} + cd ~/stackit-acme-alb + + sed -i "s|^PROJECT_ID=.*|PROJECT_ID=${stackit_resourcemanager_project.showcase.project_id}|" .env + sed -i "s|^ALB_NAME=.*|ALB_NAME=${stackit_application_load_balancer.workshop.name}|" .env + sed -i "s|^DOMAIN_WHITELIST=.*|DOMAIN_WHITELIST=${var.dns_name}|" .env + sed -i "s|^DAYS_WARNING=.*|DAYS_WARNING=99999|" .env + sed -i "s|^ALB_SA_KEY_B64=.*|ALB_SA_KEY_B64=$(base64 -w 0 sa-key.json)|" .env + sed -i "s|^DNS_SA_KEY_B64=.*|DNS_SA_KEY_B64=$(base64 -w 0 sa-key.json)|" .env + + grep '^ALB_SA_KEY_B64=' .env | cut -d= -f2- | base64 -d >/dev/null && echo "ALB key OK" + grep '^DNS_SA_KEY_B64=' .env | cut -d= -f2- | base64 -d >/dev/null && echo "DNS key OK" + + # DAYS_WARNING=99999 forces replacement regardless of bootstrap cert expiry. + # Reset to 30 after the first successful run (Step 6). + + ── Step 4: On VM — build the Docker image ──────────────────── + + docker build -t stackit-alb-cert-updater . + + ── Step 5: On VM — run certificate issuance ────────────────── + + docker run --rm \ + --env-file ~/stackit-acme-alb/.env \ + -v ~/stackit-acme-alb/letsencrypt_data:/etc/letsencrypt \ + stackit-alb-cert-updater + + ── Step 6: On VM — cron job for automatic renewal ──────────── + + # Monthly on the 1st at 03:00 (LE certs valid 90 days, warning at 30) + (crontab -l 2>/dev/null; echo "0 3 1 * * docker run --rm --env-file /home/debian/stackit-acme-alb/.env -v /home/debian/stackit-acme-alb/letsencrypt_data:/etc/letsencrypt stackit-alb-cert-updater >> /home/debian/stackit-acme-alb/renewal.log 2>&1") | crontab - + + crontab -l + + EOT +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf new file mode 100644 index 0000000..ef14277 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/07-alb.tf @@ -0,0 +1,89 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# Dedicated public IP so Terraform can wire the DNS A-record in a single apply +resource "stackit_public_ip" "alb" { + project_id = stackit_resourcemanager_project.showcase.project_id + + labels = { + environment = "workshop" + managed-by = "terraform" + use-case = "alb" + } +} + +resource "stackit_application_load_balancer" "workshop" { + project_id = stackit_resourcemanager_project.showcase.project_id + region = var.stackit_region + name = var.alb_name + plan_id = var.alb_plan_id + external_address = stackit_public_ip.alb.ip + + networks = [ + { + network_id = stackit_network.workshop.network_id + role = "ROLE_LISTENERS_AND_TARGETS" + } + ] + + listeners = [ + { + name = "http" + port = 80 + protocol = "PROTOCOL_HTTP" + http = { + hosts = [ + { + host = "*" + rules = [{ target_pool = var.alb_target_pool_name }] + } + ] + } + } + ] + + target_pools = [ + { + name = var.alb_target_pool_name + target_port = 80 + targets = [ + { + display_name = "docker-host" + ip = stackit_network_interface.vm.ipv4 + } + ] + } + ] + + options = { + private_network_only = false + access_control = { + allowed_source_ranges = var.alb_acl_ranges + } + } + + labels = { + environment = "workshop" + managed-by = "terraform" + use-case = "alb-certbot" + } + + # certbot patches the HTTPS listener out-of-band via STACKIT API — + # ignore_changes prevents Terraform from reverting those updates + lifecycle { + ignore_changes = [listeners, target_pools] + } + + depends_on = [stackit_network_interface.vm] +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md new file mode 100644 index 0000000..c070b8c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md @@ -0,0 +1,322 @@ +# STACKIT ALB + Let's Encrypt Workshop + +Infrastructure-as-Code workshop environment demonstrating automated TLS certificate lifecycle on STACKIT — Terraform provisions the ALB with an HTTP listener, certbot issues a Let's Encrypt certificate via ACME DNS-01 and attaches it as an HTTPS listener to the ALB. + +--- + +## Overview + +This repository provisions a fully reproducible demo environment on STACKIT using Terraform. It covers the complete path from infrastructure provisioning to automated certificate management. + +| Component | Description | +| ---------------------- | ------------------------------------------------------------------------------------------------------------------- | +| Resource hierarchy | Folder and project under an existing STACKIT organisation | +| Network | Private routed network (`10.10.0.0/24`) with security groups | +| Compute | Debian 12 VM with Docker Engine (provisioned via cloud-init) | +| DNS | Primary zone for ACME DNS-01 challenge validation | +| Load Balancer | Application Load Balancer with HTTP and HTTPS listeners | +| Certificate automation | [vm-alb-certbot-letsencrypt](vm-alb-certbot-letsencrypt/readme.md) — PowerShell/certbot container running on the VM | + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant VM as certbot VM + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Network + VM + DNS Zone + TF-->>User: vm_ip · zone ready + + User->>TF: create ALB (HTTP listener + target: vm_ip) + TF-->>User: alb_id · alb_public_ip · http ready + + Note over User,LE: Phase 2 — certbot on VM + + User->>VM: run cert scripts (alb_id) + VM->>LE: request certificate (DNS-01 challenge) + LE-->>VM: .crt · .pem + + VM->>TF: store certificate on STACKIT + VM->>TF: attach HTTPS listener + cert to ALB (alb_id) + TF-->>User: ✅ https://your-domain ready + + Note over VM: Phase 3+ — monthly cron + VM->>LE: renew certificate (auto, < 30 days remaining) +``` + +``` +STACKIT Organisation +└── Folder: alb-certbot-workshop + └── Project: alb-certbot-dev + ├── Network: alb-certbot-net (10.10.0.0/24, routed) + │ └── Security Group: alb-certbot-docker-host-sg + │ ├── Ingress TCP 22 ← admin_cidr only + │ ├── Ingress TCP 80 ← 0.0.0.0/0 (ALB backend traffic) + │ ├── Ingress TCP 443 ← 0.0.0.0/0 + │ └── Egress all → 0.0.0.0/0 + ├── VM: alb-certbot-docker-host (Debian 12) + │ ├── Public IP + │ └── Docker Engine + nginx:alpine (health-check backend) + ├── DNS Zone: alb-workshop.stackit.gg (primary) + └── ALB: alb-certbot-workshop + ├── Public IP (dedicated, wired to DNS A-record by Terraform) + ├── Listener HTTP :80 → VM:80 + └── Listener HTTPS :443 → VM:80 (TLS terminated at ALB) +``` + +The ALB terminates TLS — the backend VM only receives plain HTTP on port 80. This is standard practice: one TLS endpoint, no certificate management on individual backend hosts. + +### Certificate lifecycle + +``` +Phase 1 — terraform apply + ALB provisioned with HTTP listener + target pool (VM private IP). + DNS A-record for the zone apex is created pointing to the ALB public IP. + +Phase 2 — certbot (Docker container, runs on the VM) + Runs certbot DNS-01: writes _acme-challenge TXT record via STACKIT DNS API + Let's Encrypt validates the challenge and issues the certificate + Uploads the certificate to STACKIT Certificate Manager + Patches the ALB — adds HTTPS listener with the new certificate ID via API + +Phase 3+ — monthly cron job on the VM + Checks remaining certificate validity; renews automatically when < 30 days remain +``` + +> `lifecycle.ignore_changes = [listeners]` is set on the ALB resource so that `terraform apply` does not revert certificate updates made by vm-alb-certbot-letsencrypt. + +--- + +## Prerequisites + +### Required tools + +| Tool | Version | Notes | +| -------------------------------------------------------------- | -------- | -------------------------------------------------- | +| [Terraform](https://developer.hashicorp.com/terraform/install) | >= 1.5.0 | Infrastructure provisioning | +| [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) | latest | Image/machine-type lookups, role assignment | +| SSH client | — | VM access | +| Docker | — | Build and run vm-alb-certbot-letsencrypt on the VM | + +### STACKIT service account permissions + +| Scope | Role | Purpose | +| ------------ | -------------------------------- | ---------------------------- | +| Organisation | `resourcemanager.folders.admin` | Create folder | +| Folder | `resourcemanager.projects.admin` | Create project | +| Project | `compute.admin` | VM, network, security groups | +| Project | `dns.admin` | DNS zone and records | +| Project | `load-balancer.admin` | ALB and Certificate Manager | + +> **Note:** Folder and project creation via Terraform requires elevated permissions at the organisation level. As an alternative, create the folder and project manually in the STACKIT Portal and import them into Terraform state. + +### Create and configure the service account + +```bash +# Create service account +stackit iam service-account create \ + --project-id \ + --name "tf-workshop-sa" + +# Generate a key — stored in keys/ (gitignored) +mkdir -p keys +stackit iam service-account key create \ + --project-id \ + --service-account-email \ + --output-format json > keys/sa-key.json + +# Assign load-balancer.admin at project level (required for ALB API) +stackit project member add \ + --project-id \ + --role load-balancer.admin +``` + +--- + +## Deployment + +### 1. Look up required image and machine-type IDs + +```bash +# Debian 12 image UUID +stackit image list --all --project-id + +# Available machine types +stackit server machine-type list --project-id +``` + +### 2. Configure variables + +```bash +cp examples/terraform.tfvars.example terraform.tfvars +``` + +Key values to set: + +```hcl +organization_id = "" +owner_email = "your.name@example.com" +image_id = "" +admin_cidr = "/32" # curl -s https://ifconfig.schwarz +ssh_public_key = "ssh-ed25519 AAAA..." +dns_name = "alb-workshop.stackit.gg" +``` + +### 3. Configure the remote state backend + +```bash +cp examples/backend.conf.example backend.conf +# Fill in: bucket, key, access_key, secret_key (STACKIT Object Storage) +``` + +### 4. Deploy + +```bash +terraform init -backend-config=backend.conf +terraform plan +terraform apply +``` + +--- + +## Validation + +```bash +# Show all outputs — includes pre-filled Phase 2 instructions +terraform output + +# SSH to the VM +ssh debian@$(terraform output -raw vm_public_ip) + +# Verify Docker and the nginx test container +curl http://$(terraform output -raw vm_public_ip) + +# Inspect DNS zone nameservers +terraform output dns_primary_nameserver +``` + +### DNS delegation + +After `terraform apply`, delegate the DNS zone at your registrar by pointing its NS records to the value returned by: + +```bash +terraform output dns_primary_nameserver +``` + +Verify propagation: + +```bash +dig NS alb-workshop.stackit.gg +``` + +--- + +## Phase 2 — Let's Encrypt certificate + +Run `terraform output acme_next_steps` for the complete step-by-step guide with all values pre-filled from your deployment. + +**Quick summary:** + +```bash +# 1. Upload files to the VM +scp -r vm-alb-certbot-letsencrypt debian@:~/vm-alb-certbot-letsencrypt +scp keys/sa-key.json debian@:~/vm-alb-certbot-letsencrypt/sa-key.json + +# 2. SSH to the VM and patch .env with deployment-specific values +ssh debian@ +cd ~/vm-alb-certbot-letsencrypt +# (see: terraform output acme_next_steps → Step 3) + +# 3. Build the image +docker build -t stackit-alb-cert-updater . + +# 4. Staging run first — no Let's Encrypt rate-limit risk +docker run --rm \ + --env-file ~/vm-alb-certbot-letsencrypt/.env \ + -v ~/vm-alb-certbot-letsencrypt/letsencrypt_data:/etc/letsencrypt \ + stackit-alb-cert-updater + +# 5. Switch to production ACME server and run again +# 6. Set up the monthly renewal cron job +# (see: terraform output acme_next_steps → Step 7) +``` + +See [vm-alb-certbot-letsencrypt/readme.md](vm-alb-certbot-letsencrypt/readme.md) for full documentation on the certificate automation container. + +--- + +## File Structure + +``` +. +├── 00-backend.tf # S3-compatible remote state (STACKIT Object Storage) +├── 00-provider.tf # Provider declarations (stackit, random) +├── 01-variables.tf # All input variables with descriptions and defaults +├── 02-resource-hierarchy.tf # Folder and project +├── 03-network.tf # VPC, security group, ingress/egress rules +├── 04-compute.tf # SSH key, NIC, public IP, Debian 12 VM +├── 05-dns.tf # Primary DNS zone + ALB A-record +├── 06-outputs.tf # All outputs including Phase 2 instructions +├── 07-alb.tf # Public IP + Application Load Balancer (HTTP only initially) +│ +├── docs/ +│ └── architecture.md # Deployment sequence diagram + certificate lifecycle +│ +├── templates/ +│ └── cloud-init.yaml.tpl # Docker Engine install + nginx test container +│ +├── examples/ +│ ├── terraform.tfvars.example # Variable template — copy to terraform.tfvars +│ └── backend.conf.example # Backend template — copy to backend.conf +│ +├── vm-alb-certbot-letsencrypt/ # Phase 2: certificate automation (Docker / PowerShell) +│ ├── Dockerfile +│ ├── .env.default # Configuration template +│ ├── .env # Active config — gitignored, never commit +│ └── app/ +│ ├── Main_CertRenew_CLI.ps1 +│ └── lib/ +│ +└── keys/ # Service account key JSON — gitignored +``` + +--- + +## Security + +| File / Directory | Git status | Contains | +| ---------------------------------------------- | ---------- | --------------------------------------- | +| `terraform.tfvars` | gitignored | SSH public key, sensitive configuration | +| `backend.conf` | gitignored | Object Storage access and secret keys | +| `keys/` | gitignored | Service account JSON key | +| `vm-alb-certbot-letsencrypt/.env` | gitignored | Base64-encoded SA key, ACME config | +| `vm-alb-certbot-letsencrypt/letsencrypt_data/` | gitignored | Private keys issued by Let's Encrypt | + +- SSH access is restricted to `admin_cidr` — do not use `0.0.0.0/0` +- Never commit private SSH keys or service account keys to the repository + +--- + +## Cleanup + +```bash +terraform destroy +``` + +> **Note:** After `terraform destroy`, STACKIT projects remain in "Pending Deletion" for up to 7 days. The parent folder cannot be deleted until all child projects are fully removed. This is expected STACKIT platform behaviour. + +--- + +## References + +- [Full architecture details](docs/architecture.md) +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [certbot-dns-stackit](https://pypi.org/project/certbot-dns-stackit/) diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md new file mode 100644 index 0000000..0d72412 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/docs/architecture.md @@ -0,0 +1,135 @@ +# Architecture: vm-alb-certbot-letsencrypt + +## Deployment Sequence + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant VM as certbot VM + participant LE as Let's Encrypt + + Note over User,TF: Phase 1 — terraform apply + + User->>TF: create Network + VM + DNS Zone + TF-->>User: vm_ip · zone ready + + User->>TF: create ALB (HTTP listener + target: vm_ip) + TF-->>User: alb_id · alb_public_ip · http ready + + Note over User,LE: Phase 2 — certbot on VM + + User->>VM: run cert scripts (alb_id) + VM->>LE: request certificate (DNS-01 challenge) + LE-->>VM: .crt · .pem + + VM->>TF: store certificate on STACKIT + VM->>TF: attach HTTPS listener + cert to ALB (alb_id) + TF-->>User: ✅ https://your-domain ready + + Note over VM: Phase 3+ — monthly cron + VM->>LE: renew certificate (auto, < 30 days remaining) +``` + +--- + +# STACKIT ALB + Let's Encrypt (ACME) + +## Traffic Flow + +``` + Client + │ + │ DNS lookup: alb-workshop.stackit.gg + ▼ + STACKIT DNS + │ resolves to ALB public IP (Terraform A-record) + │ + │ HTTPS :443 + ▼ + STACKIT ALB (L7, TLS termination) + │ certificate: Let's Encrypt (managed by vm-alb-certbot-letsencrypt) + │ HTTP routing to target pool + │ + │ HTTP :80 + ▼ + STACKIT VM (Debian 12, Docker Engine) + │ + ▼ + Container: nginx:alpine (port 80, health-check backend) +``` + +The ALB terminates TLS. The VM only receives plain HTTP on port 80 — no +certificate management on the backend. + +--- + +## Three-Phase Deployment + +### Phase 1 — `terraform apply` + +Terraform provisions the complete infrastructure in a single apply: + +| Resource | File | +| ----------------------------------------------------------------- | -------------------------- | +| STACKIT Folder + Project | `02-resource-hierarchy.tf` | +| Network + Security Group | `03-network.tf` | +| VM (Debian 12, Docker Engine) | `04-compute.tf` | +| DNS Zone + A-record → ALB IP | `05-dns.tf` | +| Application Load Balancer (HTTP listener + target: VM private IP) | `07-alb.tf` | + +The ALB is created with an HTTP listener and target pool pointing to the VM. The HTTPS listener is added by certbot in Phase 2. `lifecycle { ignore_changes = [listeners, target_pools] }` prevents Terraform from reverting out-of-band changes. + +### Phase 2 — certbot (Docker container on the VM) + +``` + vm-alb-certbot-letsencrypt container + │ + ├── 1. Run certbot DNS-01 + │ └── write _acme-challenge TXT via STACKIT DNS API + ├── 2. Let's Encrypt validates + issues certificate + ├── 3. Upload certificate to STACKIT Certificate Manager + └── 4. Patch ALB HTTPS listener with new certificate ID +``` + +`lifecycle { ignore_changes = [listeners] }` is set on the ALB resource so +that subsequent `terraform apply` runs do not revert the certificate update. + +### Phase 3 — `terraform apply` + +Wire up target pool (VM private IP) to the ALB so traffic reaches the backend. + +### Phase 4+ — monthly renewal + +A cron job on the VM re-runs the container. It renews automatically when +fewer than 30 days of validity remain. + +--- + +## Certificate Lifecycle (DNS-01) + +``` + vm-alb-certbot-letsencrypt STACKIT DNS API Let's Encrypt STACKIT Cert Mgr ALB + │ │ │ │ │ + │── certbot DNS-01 ─►│ │ │ │ + │ _acme-challenge │ │ │ │ + │ TXT record │ │ │ │ + │ │◄── verify TXT ────│ │ │ + │◄────────────── certificate issued ─────│ │ │ + │── delete TXT ─────►│ │ │ │ + │── upload cert ─────────────────────────────────────────►│ │ + │── PATCH listener ──────────────────────────────────────────────────────►│ +``` + +--- + +## Component Responsibility + +| Component | Provisioned by | Purpose | +| ------------------------- | ---------------------- | ----------------------------------------------- | +| STACKIT Folder + Project | Terraform | Resource boundary | +| Network + Security Group | Terraform | Private network, SSH + HTTP rules | +| VM (Debian 12) | Terraform + cloud-init | Docker host | +| DNS Zone + A-record | Terraform | Zone apex → ALB IP | +| Application Load Balancer | Terraform | HTTP listener initially, HTTPS added by certbot | +| Let's Encrypt certificate | certbot (Docker on VM) | First issuance + monthly renewal | diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example new file mode 100644 index 0000000..90ece14 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/backend.conf.example @@ -0,0 +1,15 @@ +bucket = "" +key = "" +region = "eu01" + +endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" +} + +access_key = "" +secret_key = "" + +skip_credentials_validation = true +skip_region_validation = true +skip_requesting_account_id = true +skip_s3_checksum = true diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example new file mode 100644 index 0000000..3d159b9 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/examples/terraform.tfvars.example @@ -0,0 +1,51 @@ +# Copy this file to terraform.tfvars and fill in your values. +# terraform.tfvars is gitignored — never commit real values. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +stackit_region = "eu01" +stackit_service_account_key_path = "keys/sa-key.json" + +# ─── Resource Hierarchy ─────────────────────────────────────────────────────── + +# Find your Organisation ID in the STACKIT Portal → Organisation → Settings +organization_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +owner_email = "your.name@example.com" +folder_name = "alb-certbot-workshop" +project_name = "alb-certbot-dev" + +# ─── Network ────────────────────────────────────────────────────────────────── + +network_name = "alb-certbot-net" +network_cidr = "10.10.0.0/24" + +# Restrict SSH to your IP — never use 0.0.0.0/0 in production +# Find your IP: curl -s https://ifconfig.schwarz +admin_cidr = "203.0.113.10/32" + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +vm_name = "alb-certbot-docker-host" +availability_zone = "eu01-1" + +# List available machine types: +# stackit server machine-type list --project-id +machine_type = "g1.1" + +# List available images (Debian 12 recommended): +# stackit image list --all --project-id +image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +boot_volume_size_gb = 32 + +keypair_name = "alb-certbot-workshop-key" +ssh_public_key = "ssh-ed25519 AAAA... your-key-comment" + +start_nginx_test_container = true + +# ─── DNS ────────────────────────────────────────────────────────────────────── + +dns_zone_name = "alb-certbot-workshop-zone" +dns_name = "workshop.example.com" +dns_contact_email = "your.name@example.com" diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default new file mode 100644 index 0000000..949e21b --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.env.default @@ -0,0 +1,20 @@ +PROJECT_ID= +REGION_ID=eu01 +ALB_NAME= +DOMAIN_WHITELIST= +DAYS_WARNING=30 + +# Path where Certbot saves the certificates (Default: /etc/letsencrypt/live) +CERTBOT_LIVE_PATH=/etc/letsencrypt/live + +# Mode Switch: set to 'true' for Delegation Mode (Mode 2), 'false' for Direct Mode (Mode 1) +USE_CHALLENGE_DELEGATION=true +VERIFY_ZONE_FQDN= + +# Service Account Keys (Base64 encoded) +ALB_SA_KEY_B64= +DNS_SA_KEY_B64= + +# Default: Let's Encrypt staging (safe for testing — no rate limits). +# Switch to production before deploying for real: https://acme-v02.api.letsencrypt.org/directory +ACME_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore new file mode 100644 index 0000000..3f4e08a --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/.gitignore @@ -0,0 +1,64 @@ +### macOS +# Finder metadata +.DS_Store + +# Thumbnails +._* + +# Custom folder icons +Icon + +# Volume root files +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +### Windows +# Windows thumbnail cache files +Thumbs.db + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows shortcuts +*.lnk + +### Linux +# Backup files +*~ + +# Temporary files from deleted open files +.fuse_hidden* + +# KDE directory preferences +.directory + +# Linux trash folder +.Trash-* + +# NFS temporary files +.nfs* + +### VS Code +# VSCode settings (keep shared configuration) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +# Custom +.env diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile new file mode 100644 index 0000000..7d246a0 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/Dockerfile @@ -0,0 +1,71 @@ +# ----------------------------------------------------------------------------- +# STACKIT ALB Certificate Auto-Updater +# Description: Containerized environment to automate Let's Encrypt DNS-01 +# challenges and update STACKIT Application Load Balancers. +# +# DISCLAIMER: This implementation is provided as a baseline/Proof of Concept. +# Depending on your organization's specific requirements, security +# policies, and special use cases, you may need to adapt and +# extend the underlying scripts (e.g., adding custom alerting, +# advanced error handling, or specific secret management integrations). +# ----------------------------------------------------------------------------- + +FROM mcr.microsoft.com/powershell:7.4-debian-12 + +# Standard OCI Metadata Labels +LABEL org.opencontainers.image.title="STACKIT ALB Cert Updater" +LABEL org.opencontainers.image.description="Automates ACME certificate renewal for STACKIT Application Load Balancers" +LABEL org.opencontainers.image.authors="Schwarz Digits Cloud GmbH & Co. KG" + +# Disable interactive prompts during apt package installation +ENV DEBIAN_FRONTEND=noninteractive + + +# ========================================== +# PHASE 1: SYSTEM BASE & SECURITY +# ========================================== +# Install core utilities and update existing packages to fix CVEs +RUN apt-get update && \ + apt-get upgrade -y && \ + apt-get install -y --no-install-recommends \ + curl \ + gnupg \ + python3 \ + python3-venv && \ + rm -rf /var/lib/apt/lists/* + + +# ========================================== +# PHASE 2: STACKIT CLI +# ========================================== +# Add official STACKIT repository and install the CLI +RUN curl -sL https://packages.stackit.cloud/keys/key.gpg | gpg --dearmor -o /usr/share/keyrings/stackit.gpg && \ + echo "deb [signed-by=/usr/share/keyrings/stackit.gpg] https://packages.stackit.cloud/apt/cli stackit main" > /etc/apt/sources.list.d/stackit.list && \ + apt-get update && \ + apt-get install -y --no-install-recommends stackit && \ + rm -rf /var/lib/apt/lists/* + + +# ========================================== +# PHASE 3: CERTBOT & ACME DNS PLUGIN +# ========================================== +# Set up a PEP-668 compliant Virtual Environment +ENV VIRTUAL_ENV=/opt/certbot-venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN python3 -m venv $VIRTUAL_ENV + +# Upgrade the venv's pip and install Certbot with the STACKIT plugin +RUN pip3 install --no-cache-dir --upgrade pip setuptools && \ + pip3 install --no-cache-dir certbot certbot-dns-stackit + + +# ========================================== +# PHASE 4: APPLICATION WORKSPACE +# ========================================== +WORKDIR /app + +# Copy the application scripts into the container +COPY app/ /app/ + +# Define the default execution command +ENTRYPOINT ["pwsh", "-File", "/app/Main_CertRenew_CLI.ps1"] diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md new file mode 100644 index 0000000..7ddc15c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/README.md @@ -0,0 +1,201 @@ +# STACKIT ALB Certificate Auto-Updater + +> **DISCLAIMER:** This repository provides a Proof of Concept (PoC) and baseline implementation. It was successfully developed and tested on a Windows 10 environment using Docker Desktop. Depending on your organization's specific requirements, security policies, and special use cases, you must adapt and extend the underlying scripts (e.g., adding custom alerting, advanced error handling, or specific secret management integrations) before deploying this to a production environment. + +## Overview + +Securing web traffic via TLS/SSL is a standard requirement for applications exposed through a STACKIT Application Load Balancer (ALB). When using Let's Encrypt certificates, an automated renewal process is highly recommended due to their short 90-day validity period. + +This solution automates the provisioning, uploading, and updating of ACME certificates without requiring inbound HTTP traffic to a validation endpoint. It leverages the **DNS-01 challenge** via the STACKIT DNS API, uploads the resulting certificates to the STACKIT Certificate Manager, and patches the Application Load Balancer to use the new certificates seamlessly. + +--- + +## How it Works + +This automation supports two different workflows for validating domain ownership via DNS, depending on where the target domain's DNS zone is hosted. + +### Mode 1: Direct DNS Update (Normal Challenge) + +**Use Case:** The target domain (e.g., `web.yourdomain.com`) is hosted directly within a STACKIT DNS Zone that the provided Service Account has access to. + +In this mode, the script uses the official STACKIT Certbot DNS plugin to create the `_acme-challenge` TXT record directly in the domain's primary DNS zone. + +**High-Level Architecture:**
+![Simple Normal DNS Challenge](./docs/images/mode1_high_level.png) + + + +**Detailed Technical Sequence:**
+![Detailed Normal DNS Challenge](./docs/images/mode1_detail_level.png) + +## + +### Mode 2: ACME Challenge Delegation via CNAME + +**Use Case:** Managing Let's Encrypt renewals for external domains (e.g., domains owned by end-customers). You do not have (and should not require) API credentials to directly modify their production DNS zones. + +To solve this, this automation leverages **ACME Challenge Delegation via CNAME**. This allows Let's Encrypt to validate domain ownership by redirecting the challenge request to a central, isolated "Verification Zone" hosted on STACKIT. The customer only needs to create a one-time CNAME record pointing to this central zone. + +**High-Level Architecture:**
+![Simple Delegated DNS Challenge](./docs/images/mode2_high_level.png) + + + +**Detailed Technical Sequence:**
+![Detailed Delegated DNS Challenge](./docs/images/mode2_detail_level.png) + +## + +## Prerequisites + +To run this automation, you need the following STACKIT resources and credentials: + +1. **A STACKIT Project** with an active **Application Load Balancer** (ALB). +2. **A Target DNS Zone OR a Verification DNS Zone:** Depending on the mode you choose. +3. **DNS Validator Service Account:** A STACKIT Service Account strictly scoped to manage TXT records in the relevant DNS Zone. +4. **ALB Manager Service Account:** A STACKIT Service Account with permissions for the STACKIT Certificate Manager (`POST`/`GET`/`DELETE`) and the Application Load Balancer API (`GET`/`PUT`). + +_Note: You can pass these credentials either as local JSON files or as Base64 encoded environment variables (recommended for Docker/CI)._ + +## Repository Structure + +```text +. +├── Dockerfile # Debian-based image definition with Certbot & STACKIT CLI +├── README.md # This documentation +├── keys/ # (Local execution only) Directory for Service Account JSON keys +└── app/ + ├── Main_CertRenew_CLI.ps1 # The main orchestration script + └── lib/ + ├── Get-StackitAlbCertStatus_CLI.ps1 # Helper: Evaluates ALB cert expiration + ├── StackitHelper_CLI.ps1 # Helper: Handles API uploads & ALB patching + ├── Stackit-DnsHook.ps1 # Hook: Creates TXT records in Verification Zone (Mode 2) + └── Stackit-CleanupHook.ps1 # Hook: Deletes TXT records after validation (Mode 2) +``` + +## Execution Methods + +The script is highly flexible. You can run it via Docker (using Base64 environment variables) or locally (using JSON files). + +### Option A: Running via Docker (Recommended for CI/CD) + +For Docker and CI/CD pipelines, passing JSON files via volume mounts is cumbersome and a security risk. Instead, this automation supports passing the Service Account credentials directly as **Base64 encoded strings** via environment variables. The script will safely decode them in memory during execution. + +**1. Convert your `.json` Service Account keys to Base64 (single line):** + +- _Linux/macOS:_ `base64 -w 0 alb-manager-sa.json` +- _PowerShell:_ `[convert]::ToBase64String([IO.File]::ReadAllBytes("alb-manager-sa.json"))` + +**2. Create an `.env` file** in the root of the repository: + +```env +PROJECT_ID=1b1a69ac-1482-4b44-9165-3470c0b8fb79 +REGION_ID=eu01 +ALB_NAME=your-alb-name +DOMAIN_WHITELIST=web.yourdomain.com,api.yourdomain.com +DAYS_WARNING=30 + +# Path where Certbot saves the certificates (Default: /etc/letsencrypt/live) +CERTBOT_LIVE_PATH=/etc/letsencrypt/live + +# Mode Switch: set to 'true' for Delegation Mode (Mode 2), 'false' for Direct Mode (Mode 1) +USE_CHALLENGE_DELEGATION=true +VERIFY_ZONE_FQDN=alb-verify-poc-ldb-acme.stackit.zone + +# Service Account Keys (Base64 encoded) +ALB_SA_KEY_B64= # gitleaks:allow +DNS_SA_KEY_B64= # gitleaks:allow + +# Use Staging for testing to avoid Let's Encrypt rate limits! +ACME_SERVER=https://acme-staging-v02.api.letsencrypt.org/directory +# For Production, comment out the line above and use: +# ACME_SERVER=https://acme-v02.api.letsencrypt.org/directory +``` + +**3. Build and Run the Docker Container:** +You only need to mount the volume for Certbot to persist the certificates. You do **not** need to mount the `keys` folder! + +Linux: + +```bash +docker build -t stackit-alb-cert-updater . + +docker run --rm -it \ + --env-file .env \ + -v "$(pwd)/letsencrypt_data:/etc/letsencrypt" \ + stackit-alb-cert-updater +``` + +Windows: + +```powershell +docker build -t stackit-alb-cert-updater . + +docker run --rm -it ` + --env-file .env ` + -v "${PWD}\letsencrypt_data:/etc/letsencrypt" ` + stackit-alb-cert-updater +``` + +--- + +### Option B: Running Locally (PowerShell) + +If you are running the script locally (e.g., for testing on your workstation), you can simply pass the direct file paths to your `.json` Service Account keys. + +Linux: + +```powershell +./app/Main_CertRenew_CLI.ps1 ` + -ProjectId "1b1a69ac-1482-4b44-9165-3470c0b8fb79" ` + -RegionId "eu01" ` + -AlbName "your-alb-name" ` + -DomainWhitelist "www.customerapplication.domain", "www2.customerapplication.domain" ` + -UseChallengeDelegation $true ` + -VerifyZoneFQDN "alb-verify-example.stackit.zone" ` + -CertbotLive "/etc/letsencrypt/live" ` + -DNS_SAKeyPath "./keys/dns-validator-sa.json" ` + -ALB_SAKeyPath "./keys/alb-manager-sa.json" ` + -AcmeServer "https://acme-staging-v02.api.letsencrypt.org/directory" +``` + +Windows: + +```powershell +.\app\Main_CertRenew_CLI.ps1 ` + -ProjectId "1b1a69ac-1482-4b44-9165-3470c0b8fb79" ` + -RegionId "eu01" ` + -AlbName "your-alb-name" ` + -DomainWhitelist "www.customerapplication.domain", "www2.customerapplication.domain" ` + -UseChallengeDelegation $true ` + -VerifyZoneFQDN "alb-verify-example.stackit.zone" ` + -CertbotLive "C:\Certbot\live" ` + -DNS_SAKeyPath "./keys/dns-validator-sa.json" ` + -ALB_SAKeyPath "./keys/alb-manager-sa.json" ` + -AcmeServer "https://acme-staging-v02.api.letsencrypt.org/directory" +``` + +## 💡 Important Note on Let's Encrypt Caching (For Testing) + +When testing the renewal process, please be aware of Let's Encrypt's **Authorization Caching**. Once a domain has been successfully validated via the DNS-01 challenge, Let's Encrypt caches this successful validation for **30 days**. +If you trigger a forced renewal for the exact same domain within this window, Let's Encrypt will skip the DNS challenge entirely. Consequently, Certbot will **not** execute the DNS plugin or hook scripts (creation/deletion of the TXT record). To forcefully test the entire workflow including the DNS hooks, you must either test with a new, unvalidated subdomain or ensure you clear your local Certbot account data while using the Staging Environment. + +## Limitations & Out of Scope (PoC Boundaries) + +As this is a baseline Proof of Concept intended for integration into existing CI/CD environments, the following features are deliberately **not implemented** and should be managed by the surrounding automation platform: + +- **CI/CD Pipeline Configurations:** This repository provides the core automation logic via Docker and PowerShell but does _not_ include ready-to-use pipeline definitions (e.g., `.gitlab-ci.yml`, GitHub Actions workflows, or Kubernetes `CronJob` manifests). The consumer must wrap this container into their own scheduling execution engine. +- **Enterprise Secret Management:** While the script supports Base64 encoded environment variables for secure pipeline injection, native API integration with external secret management solutions (e.g., HashiCorp Vault, Azure Key Vault, AWS Secrets Manager) is out of scope. +- **Orphaned Certificate Cleanup:** The script uploads new certificates but does _not_ auto-delete expired or detached certificates from the STACKIT Certificate Manager. This design choice prevents the accidental deletion of certificates that might still be bound to other resources. Housekeeping of orphaned certificates remains a separate operational process. +- **Multi-Domain (SAN) & Wildcard Certificates:** This workflow is heavily optimized for a 1:1 certificate-to-domain mapping. Grouping multiple domains under a single Subject Alternative Name (SAN) certificate or handling `*.domain.com` wildcards requires manual extensions to the underlying Certbot arguments and ALB patching logic. +- **Advanced Alerting & Notifications:** The script outputs clear status and error messages to standard `stdout`/`stderr` streams. It does not natively implement webhooks for chat platforms (e.g., MS Teams, Slack) or Email. It relies on the overarching CI/CD platform to scrape logs and trigger alerts based on the exit code (0 for success, >0 for errors). +- **Automated Rollbacks:** If the patching of the Application Load Balancer fails _after_ a new certificate has been successfully uploaded to STACKIT, the script will throw an error but will not automatically remove the newly uploaded certificate. +- **Concurrency & State Locking:** There is no native distributed state-locking mechanism. Running multiple instances of this script simultaneously against the same ALB configuration could lead to race conditions during the patching phase. + +## Next Steps / Automation + +For continuous operation, you should schedule this script/container to run regularly (e.g., daily). You can achieve this via: + +- A standard Linux `cron` job or in windows (using the task scheduler) on a dedicated server. +- A CI/CD Pipeline schedule (e.g., GitLab CI, GitHub Actions). +- A Kubernetes `CronJob` within STACKIT Kubernetes Engine (SKE), securely mounting the service account keys as Kubernetes Secrets. diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 new file mode 100644 index 0000000..fe0f95d --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/Main_CertRenew_CLI.ps1 @@ -0,0 +1,263 @@ +<# +.SYNOPSIS + STACKIT ALB Certificate Auto-Renewal Orchestrator (PoC) + +.DESCRIPTION + This script automates the renewal of Let's Encrypt certificates for a STACKIT Application Load Balancer. + It identifies expiring certificates, triggers Certbot to perform a DNS-01 challenge, uploads the + renewed certificates to the STACKIT Certificate Manager, and patches the ALB configuration. + + This is intended as a Proof of Concept (PoC) baseline for CI/CD integration. + It supports passing Service Account keys as Base64 environment variables to avoid persisting secrets on disk. +#> + +param ( + # Core Infrastructure + [string] $ProjectId = $env:PROJECT_ID, + [string] $RegionId = $(if ($env:REGION_ID) { $env:REGION_ID } else { "eu01" }), + [string] $AlbName = $env:ALB_NAME, + + # Optional filtering: Only renew domains listed here + [string[]] $DomainWhitelist = @(), + + # Paths & Credentials + [string] $CertbotLive = $(if ($env:CERTBOT_LIVE_PATH) { $env:CERTBOT_LIVE_PATH } else { "/etc/letsencrypt/live" }), + [string] $DNS_SAKeyPath = $(if ($env:DNS_SA_KEY_PATH) { $env:DNS_SA_KEY_PATH } else { "$PSScriptRoot/keys/dns-validator-sa.json" }), + [string] $ALB_SAKeyPath = $(if ($env:ALB_SA_KEY_PATH) { $env:ALB_SA_KEY_PATH } else { "$PSScriptRoot/keys/alb-manager-sa.json" }), + + # Execution Modifiers + [bool] $SkipCertbot = $(if ($env:SKIP_CERTBOT -match '^(true|1|yes)$') { $true } else { $false }), + [int] $DaysWarning = $(if ($env:DAYS_WARNING) { [int]$env:DAYS_WARNING } else { 30 }), + + # API Endpoints + [string] $AlbBaseUrl = $(if ($env:ALB_BASE_URL) { $env:ALB_BASE_URL } else { "https://alb.api.stackit.cloud/v2" }), + [string] $CertBaseUrl = $(if ($env:CERT_BASE_URL) { $env:CERT_BASE_URL } else { "https://certificates.api.stackit.cloud/v2" }), + [string] $AcmeServer = $(if ($env:ACME_SERVER) { $env:ACME_SERVER } else { "https://acme-staging-v02.api.letsencrypt.org/directory" }), + + # Advanced: CNAME Delegation Mode configuration + [bool] $UseChallengeDelegation = $(if ($env:USE_CHALLENGE_DELEGATION -match '^(true|1|yes)$') { $true } else { $false }), + [string] $VerifyZoneFQDN = $env:VERIFY_ZONE_FQDN +) + +$ErrorActionPreference = "Stop" +Write-Output "=== STACKIT ALB Certificate Auto-Renewal Pipeline ===" + +# Parse whitelist from environment if not provided via parameter +if ($DomainWhitelist.Count -eq 0 -and $env:DOMAIN_WHITELIST) { + $DomainWhitelist = $env:DOMAIN_WHITELIST -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" } +} + +$Config = @{ + ProjectId = $ProjectId + RegionId = $RegionId + AlbName = $AlbName + DomainWhitelist = $DomainWhitelist + CertbotLive = $CertbotLive + DNS_SAKeyPath = $DNS_SAKeyPath + ALB_SAKeyPath = $ALB_SAKeyPath + SkipCertbot = $SkipCertbot + DaysWarning = $DaysWarning + AlbBaseUrl = $AlbBaseUrl + CertBaseUrl = $CertBaseUrl + AcmeServer = $AcmeServer + UseChallengeDelegation = $UseChallengeDelegation + VerifyZoneFQDN = $VerifyZoneFQDN +} + +# --- PRE-FLIGHT CHECKS --- +Write-Output "`n>>> [INIT] Starting pre-flight checks..." + +# CI/CD Integration: Decode Base64 encoded Service Account keys injected via Environment Variables. +# This avoids storing physical JSON files in the repository or mounting them insecurely. +if ($env:ALB_SA_KEY_B64) { + Write-Output " [INFO] Decoding ALB Service Account from Base64 variable..." + $albBytes = [System.Convert]::FromBase64String($env:ALB_SA_KEY_B64.Trim()) + $Config.ALB_SAKeyPath = Join-Path ([System.IO.Path]::GetTempPath()) "alb-sa.json" + [System.IO.File]::WriteAllBytes($Config.ALB_SAKeyPath, $albBytes) +} + +if ($env:DNS_SA_KEY_B64) { + Write-Output " [INFO] Decoding DNS Service Account from Base64 variable..." + $dnsBytes = [System.Convert]::FromBase64String($env:DNS_SA_KEY_B64.Trim()) + $Config.DNS_SAKeyPath = Join-Path ([System.IO.Path]::GetTempPath()) "dns-sa.json" + [System.IO.File]::WriteAllBytes($Config.DNS_SAKeyPath, $dnsBytes) +} + +# Verify that key files exist (either provided directly or decoded above) +if (-not (Test-Path $Config.DNS_SAKeyPath)) { + Write-Error "[FATAL] DNS Service Account Key missing at $($Config.DNS_SAKeyPath)" + exit 1 +} +if (-not (Test-Path $Config.ALB_SAKeyPath)) { + Write-Error "[FATAL] ALB Service Account Key missing at $($Config.ALB_SAKeyPath)" + exit 1 +} + +if ($Config.UseChallengeDelegation -and [string]::IsNullOrWhiteSpace($Config.VerifyZoneFQDN)) { + Write-Error "[FATAL] Challenge Delegation is enabled, but VERIFY_ZONE_FQDN is empty!" + exit 1 +} + +# Import helper functions +. "$PSScriptRoot/lib/Get-StackitAlbCertStatus_CLI.ps1" +. "$PSScriptRoot/lib/StackitHelper_CLI.ps1" +Write-Output "[OK] Pre-flight checks passed." + +# --- STEP 1: AUTHENTICATION --- +Write-Output "`n>>> [STEP 1] Activating STACKIT CLI Service Account (ALB Scope)..." +& stackit auth activate-service-account --service-account-key-path $Config.ALB_SAKeyPath +if ($LASTEXITCODE -ne 0) { throw "Failed to activate STACKIT CLI Service Account for ALB operations." } +Write-Output "[OK] STACKIT CLI successfully authenticated." + +# --- STEP 2: STATUS CHECK --- +# Query the ALB configuration to map domains to active certificates and calculate expiration +Write-Output "`n>>> [STEP 2] Fetching ALB configuration and certificate status..." +$status = Get-StackitAlbCertStatus -ProjectId $Config.ProjectId -RegionId $Config.RegionId -AlbName $Config.AlbName -Whitelist $Config.DomainWhitelist -DaysWarning $Config.DaysWarning -AlbBaseUrl $Config.AlbBaseUrl -CertBaseUrl $Config.CertBaseUrl + +$allCerts = @($status.certificates) +$toRenew = @($allCerts | Where-Object { $_.shouldReplace -eq $true }) +$healthyCerts = @($allCerts | Where-Object { $_.shouldReplace -eq $false }) + +Write-Output "[INFO] Evaluated a total of $($allCerts.Count) active certificate(s) on ALB '$($Config.AlbName)'." + +# Sub-Check 2a: Warn if a whitelisted domain is missing from the ALB configuration +if ($Config.DomainWhitelist.Count -gt 0) { + $albDomains = $allCerts.domain + foreach ($wlDomain in $Config.DomainWhitelist) { + if ($albDomains -notcontains $wlDomain) { + Write-Warning " [SKIP] Domain '$wlDomain' is on the whitelist, but is NOT configured on the ALB. Skipping..." + } + } +} + +# Sub-Check 2b: Log healthy certificates that do not require renewal yet +if ($healthyCerts.Count -gt 0) { + foreach ($hc in $healthyCerts) { + Write-Output " [SKIP] Certificate for '$($hc.domain)' is healthy (Expires in $($hc.daysUntilExpiry) days). No action required." + } +} + +if ($toRenew.Count -eq 0) { + Write-Output "`n[RESULT] All targeted certificates are healthy and up to date. No renewals required." + Write-Output "=== Workflow Completed SUCCESSFULLY ===" + exit 0 +} + +# --- STEP 3 & 4: RENEWAL LOOP --- +Write-Output "`n>>> [STEP 3] Starting renewal process for $($toRenew.Count) certificate(s)..." + +$WorkflowHasErrors = $false + +foreach ($cert in $toRenew) { + Write-Output "`n--------------------------------------------------------" + Write-Output "[*] Target Domain : $($cert.domain)" + Write-Output "[*] Expiring Cert : $($cert.certificateId)" + Write-Output "--------------------------------------------------------" + + $certbotSuccess = $false + $domainPath = Join-Path $Config.CertbotLive $cert.domain + $fullchainPath = Join-Path $domainPath "fullchain.pem" + $privkeyPath = Join-Path $domainPath "privkey.pem" + + if (-not $Config.SkipCertbot) { + Write-Output " -> [Action] Triggering Certbot ACME DNS-01 challenge..." + + # --- CERTBOT EXTERNAL LOG BOUNDARY --- + Write-Output "" + Write-Output " v===================== CERTBOT OUTPUT =====================v" + + if ($Config.UseChallengeDelegation) { + Write-Output " -> [INFO] Mode: CNAME Delegation Hooks (Verify Zone: $($Config.VerifyZoneFQDN))" + + # Export variables required by the child processes (the PowerShell Hook scripts) + $env:STACKIT_PROJECT_ID = $Config.ProjectId + $env:DNS_SA_KEY_PATH = $Config.DNS_SAKeyPath + $env:VERIFY_ZONE_FQDN = $Config.VerifyZoneFQDN + + $PSExe = if ($PSVersionTable.PSVersion.Major -ge 6) { "pwsh" } else { "powershell" } + + $AuthHookPath = Join-Path $PSScriptRoot "lib\Stackit-DnsHook.ps1" + $CleanupHookPath = Join-Path $PSScriptRoot "lib\Stackit-CleanupHook.ps1" + + $AuthCmd = "$PSExe -NoProfile -ExecutionPolicy Bypass -File `"$AuthHookPath`"" + $CleanupCmd = "$PSExe -NoProfile -ExecutionPolicy Bypass -File `"$CleanupHookPath`"" + + $CertbotArgs = @( + "certonly", + "--manual", + "--manual-auth-hook", $AuthCmd, + "--manual-cleanup-hook", $CleanupCmd, + "--preferred-challenges", "dns", + "--server", $Config.AcmeServer, + "--agree-tos", + "-d", $cert.domain, + "--non-interactive", + "--force-renewal", + "--disable-hook-validation" + ) + & certbot $CertbotArgs + + } else { + Write-Output " -> [INFO] Mode: STACKIT DNS Plugin (Direct Zone Update)" + # Execute Certbot using the official STACKIT DNS plugin + & certbot certonly --authenticator dns-stackit ` + --dns-stackit-project-id $Config.ProjectId ` + --dns-stackit-service-account $Config.DNS_SAKeyPath ` + --dns-stackit-propagation-seconds 120 ` + --server $Config.AcmeServer ` + --agree-tos -d $cert.domain --non-interactive --force-renewal + } + $certbotSuccess = ($LASTEXITCODE -eq 0) + + # --- END OF CERTBOT LOG BOUNDARY --- + Write-Output " ^==========================================================^" + Write-Output "" + + if (-not $certbotSuccess) { + Write-Error " [FAIL] Certbot process failed for $($cert.domain)." + $WorkflowHasErrors = $true + continue + } + } else { + # SkipCertbot mode: Assume files were generated out-of-band and just deploy them + Write-Output " -> [INFO] SkipCertbot enabled. Checking for existing local files..." + $certbotSuccess = (Test-Path $fullchainPath) -and (Test-Path $privkeyPath) + if (-not $certbotSuccess) { + Write-Error " [FAIL] Local certificate files not found for $($cert.domain)!" + $WorkflowHasErrors = $true + continue + } + } + + Write-Output " [+] Certificate generated successfully. Local files are ready." + + # Generate a unique name for the new certificate in STACKIT Certificate Manager + $safeDomain = $cert.domain -replace '\.', '-' + $newName = "auto-$($safeDomain)-$(Get-Date -Format 'yyyyMMdd-HHmm')" + + try { + # Reactivate ALB scope auth (in case Certbot/Hooks altered the active CLI session) + Write-Output " -> [Action] Ensuring STACKIT CLI Auth is active before upload..." + & stackit auth activate-service-account --service-account-key-path $Config.ALB_SAKeyPath + + Write-Output " -> [Action] Uploading new certificate '$newName' to STACKIT Certificate Manager in project '$($Config.ProjectId)'" + $newCert = New-StackitCertificate -ProjectId $Config.ProjectId -RegionId $Config.RegionId -CertName $newName -FullchainPath $fullchainPath -PrivkeyPath $privkeyPath -CertBaseUrl $Config.CertBaseUrl + Write-Output " [SUCCESS] Certificate successfully uploaded!" + + Write-Output " -> [Action] Patching ALB to use new Certificate ID: $($newCert.id)..." + $patch = Update-AlbListenerCert -ProjectId $Config.ProjectId -RegionId $Config.RegionId -AlbName $Config.AlbName -OldCertId $cert.certificateId -NewCertId $newCert.id -AlbBaseUrl $Config.AlbBaseUrl + Write-Output " [SUCCESS] ALB successfully updated with the new certificate!" + } catch { + Write-Error " [ERROR] STACKIT platform update failed for $($cert.domain): $($_.Exception.Message)" + $WorkflowHasErrors = $true + } +} + +if ($WorkflowHasErrors) { + Write-Error "`n=== Workflow Completed with ERRORS ===" + Write-Error "At least one certificate failed to renew or deploy. Please check the logs above." + exit 1 +} else { + Write-Output "`n=== Workflow Completed SUCCESSFULLY ===" + exit 0 +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 new file mode 100644 index 0000000..d7b3b26 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Get-StackitAlbCertStatus_CLI.ps1 @@ -0,0 +1,94 @@ +<# +.SYNOPSIS + Evaluates STACKIT ALB Certificate Expiration + +.DESCRIPTION + This helper script retrieves the current configuration of the STACKIT Application Load Balancer. + It extracts the IDs of all bound certificates, retrieves their public keys from the Certificate Manager, + and calculates the exact expiration dates using native X509 .NET classes. +#> +function Get-StackitAlbCertStatus { + param ( + [Parameter(Mandatory=$true)] [string]$ProjectId, + [Parameter(Mandatory=$true)] [string]$RegionId, + [Parameter(Mandatory=$true)] [string]$AlbName, + [Parameter(Mandatory=$false)] [string[]]$Whitelist = @(), + [Parameter(Mandatory=$false)] [int]$DaysWarning = 30, + [Parameter(Mandatory=$false)] [string]$AlbBaseUrl = "https://alb.api.stackit.cloud/v2", + [Parameter(Mandatory=$false)] [string]$CertBaseUrl = "https://certificates.api.stackit.cloud/v2", + [Parameter(Mandatory=$false)] [switch]$ForceRenew + ) + + $CertResults = New-Object System.Collections.Generic.List[PSCustomObject] + + try { + # 1. Fetch current ALB state + $AlbUrl = "$AlbBaseUrl/projects/$ProjectId/regions/$RegionId/load-balancers/$AlbName" + $Alb = stackit curl -X GET $AlbUrl | ConvertFrom-Json + + # 2. Iterate through listeners and extract bound certificate IDs + foreach ($Listener in $Alb.listeners) { + if ($Listener.protocol -eq "PROTOCOL_HTTPS" -and $Listener.https.certificateConfig.certificateIds) { + foreach ($HostRule in $Listener.http.hosts) { + + # Apply whitelist filtering if specified + if ($Whitelist.Count -eq 0 -or $Whitelist -contains $HostRule.host) { + foreach ($CertId in $Listener.https.certificateConfig.certificateIds) { + + # Bypass calculation if forced renewal is triggered + if ($ForceRenew) { + $CertResults.Add([PSCustomObject]@{ + domain = $HostRule.host + certificateId = $CertId + name = "Forced-Check" + expiryDate = "FORCED" + daysLeft = 0 + shouldReplace = $true + }) + continue + } + + # 3. Retrieve actual certificate details from STACKIT Certificate Manager + $CertUrl = "$CertBaseUrl/projects/$ProjectId/regions/$RegionId/certificates/$CertId" + try { + $CertJson = stackit curl -X GET $CertUrl | ConvertFrom-Json + + # Note: The STACKIT API returns the public key inside a JSON string. + # We must extract the raw PEM, strip headers, and sanitize it for Base64 conversion. + $rawCert = $CertJson.publicKey + $firstCertPart = ($rawCert -split "-----END CERTIFICATE-----")[0] + $cleanB64 = ($firstCertPart -replace "-----BEGIN CERTIFICATE-----", "") -replace '[^a-zA-Z0-9\+\/=]', '' + + # Pad Base64 string if necessary to avoid format exceptions + while ($cleanB64.Length % 4 -ne 0) { $cleanB64 += "=" } + + # Load the byte array into a .NET X509 object to reliably read the expiration date (NotAfter) + $certBytes = [System.Convert]::FromBase64String($cleanB64) + $x509 = [System.Security.Cryptography.X509Certificates.X509Certificate2]::new($certBytes) + + $CertResults.Add([PSCustomObject]@{ + domain = $HostRule.host + certificateId = $CertId + name = $CertJson.name + expiryDate = $x509.NotAfter.ToString("yyyy-MM-dd") + daysLeft = ($x509.NotAfter - (Get-Date)).Days + shouldReplace = (($x509.NotAfter - (Get-Date)).Days -lt $DaysWarning) + }) + } catch { + $CertResults.Add([PSCustomObject]@{ domain = $HostRule.host; error = "CLI Error processing $CertId" }) + } + } + } + } + } + } + + # Return unique certificates (a cert might be bound to multiple listeners) + return [PSCustomObject]@{ + projectId = $ProjectId; + albName = $AlbName; + certificates = $CertResults | Select-Object -Unique * } + } catch { + throw "ALB Request via STACKIT CLI failed: $($_.Exception.Message)" + } +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 new file mode 100644 index 0000000..3b08d71 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-CleanupHook.ps1 @@ -0,0 +1,70 @@ +<# +.SYNOPSIS + Certbot Manual Cleanup Hook for STACKIT DNS Validation + +.DESCRIPTION + This script is invoked automatically by Certbot after the Let's Encrypt validation + attempt (whether successful or not). It ensures the temporary TXT challenge + records are removed from the STACKIT Verification Zone to maintain a clean DNS state. +#> + +$Domain = $env:CERTBOT_DOMAIN +$ProjectId = $env:STACKIT_PROJECT_ID +$SaKeyPath = $env:DNS_SA_KEY_PATH +$VerifyZoneFQDN = $env:VERIFY_ZONE_FQDN + +$ErrorActionPreference = "Stop" + +$MissingVars = @() +if ([string]::IsNullOrWhiteSpace($Domain)) { $MissingVars += "CERTBOT_DOMAIN" } +if ([string]::IsNullOrWhiteSpace($ProjectId)) { $MissingVars += "STACKIT_PROJECT_ID" } +if ([string]::IsNullOrWhiteSpace($VerifyZoneFQDN)) { $MissingVars += "VERIFY_ZONE_FQDN" } + +if ($MissingVars.Count -gt 0) { + [Console]::Error.WriteLine(">>> [HOOK FATAL] Missing required environment variables from Certbot/Main Script: $($MissingVars -join ', ')") + exit 1 +} + +try { + Write-Output ">>> [CLEANUP] Starting DNS Cleanup Hook for: $Domain" + $RecordName = $Domain + + # Suppress STACKIT CLI loading spinners from stderr to prevent Certbot logging errors + $authOutput = & stackit auth activate-service-account --service-account-key-path "$SaKeyPath" 2>&1 + if ($LASTEXITCODE -ne 0) { throw "STACKIT CLI Auth failed in Cleanup Hook." } + + $ZonesJson = & stackit dns zone list --project-id "$ProjectId" --output-format json 2>$null | ConvertFrom-Json + $Zone = $ZonesJson | Where-Object { $_.dnsName -eq "$VerifyZoneFQDN" -or $_.dnsName -eq "$VerifyZoneFQDN." } + if (-not $Zone) { throw "Verify Zone '$VerifyZoneFQDN' not found in STACKIT Project!" } + $ZoneId = $Zone.id + + Write-Output ">>> [CLEANUP] Fetching record sets for zone $ZoneId..." + $RecordsJson = & stackit dns record-set list --zone-id "$ZoneId" --project-id "$ProjectId" --output-format json 2>$null | ConvertFrom-Json + + # Account for FQDNs returned with or without a trailing dot + $ExpectedNameWithDot = "$RecordName.$VerifyZoneFQDN." + $ExpectedNameNoDot = "$RecordName.$VerifyZoneFQDN" + + $TargetRecord = $RecordsJson | Where-Object { + ($_.name -eq $ExpectedNameWithDot -or $_.name -eq $ExpectedNameNoDot) -and $_.type -eq "TXT" + } | Select-Object -First 1 + + if ($TargetRecord) { + $RecordId = $TargetRecord.id + Write-Output ">>> [CLEANUP] Found TXT record with ID '$RecordId'. Deleting..." + + # Suppress CLI auto-confirm warnings from stderr + $deleteOutput = & stackit dns record-set delete "$RecordId" --zone-id "$ZoneId" --project-id "$ProjectId" --assume-yes 2>&1 + + if ($LASTEXITCODE -ne 0) { + throw "Failed to delete record: $deleteOutput" + } + Write-Output ">>> [CLEANUP] Done." + } else { + Write-Output ">>> [CLEANUP] WARNING: TXT record for '$RecordName' not found. It may have been already deleted." + } + +} catch { + [Console]::Error.WriteLine(">>> [CLEANUP ERROR] $($_.Exception.Message)") + exit 1 +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 new file mode 100644 index 0000000..1707f32 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/Stackit-DnsHook.ps1 @@ -0,0 +1,75 @@ +<# +.SYNOPSIS + Certbot Manual Auth Hook for STACKIT DNS Validation + +.DESCRIPTION + This script is invoked automatically by Certbot during the DNS-01 challenge. + It takes the validation token provided by Let's Encrypt and creates a temporary + TXT record in a designated STACKIT Verification Zone (Delegation Mode). +#> + +# Certbot automatically injects these environment variables +$Domain = $env:CERTBOT_DOMAIN +$ValidationToken = $env:CERTBOT_VALIDATION + +# Variables passed down from the Main Orchestrator script +$ProjectId = $env:STACKIT_PROJECT_ID +$SaKeyPath = $env:DNS_SA_KEY_PATH +$VerifyZoneFQDN = $env:VERIFY_ZONE_FQDN + +$ErrorActionPreference = "Stop" + +# Ensure all required inputs are present +$MissingVars = @() +if ([string]::IsNullOrWhiteSpace($Domain)) { $MissingVars += "CERTBOT_DOMAIN" } +if ([string]::IsNullOrWhiteSpace($ProjectId)) { $MissingVars += "STACKIT_PROJECT_ID" } +if ([string]::IsNullOrWhiteSpace($VerifyZoneFQDN)) { $MissingVars += "VERIFY_ZONE_FQDN" } + +if ($MyInvocation.MyCommand.Name -match "DnsHook" -and [string]::IsNullOrWhiteSpace($ValidationToken)) { + $MissingVars += "CERTBOT_VALIDATION" +} + +if ($MissingVars.Count -gt 0) { + # We use [Console]::Error.WriteLine to ensure Certbot directly catches fatal hook errors + [Console]::Error.WriteLine(">>> [HOOK FATAL] Missing required environment variables from Certbot/Main Script: $($MissingVars -join ', ')") + exit 1 +} + +try { + Write-Output ">>> [HOOK] Starting DNS Auth Hook for: $Domain" + $RecordName = $Domain + + # Authenticate via STACKIT CLI using the dedicated DNS Service Account. + # CRITICAL: We redirect stream 2 (`2>&1`) to a variable. The CLI may output loading spinners + # to stderr, which Certbot falsely interprets as a critical script failure. + $authOutput = & stackit auth activate-service-account --service-account-key-path "$SaKeyPath" 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "STACKIT CLI Auth failed in Hook: $authOutput" + } + + # Fetch the ID of the Verification Zone. + # `2>$null` suppresses CLI warnings to ensure ConvertFrom-Json receives a clean JSON payload. + $ZonesJson = & stackit dns zone list --project-id "$ProjectId" --output-format json 2>$null | ConvertFrom-Json + $Zone = $ZonesJson | Where-Object { $_.dnsName -eq "$VerifyZoneFQDN" -or $_.dnsName -eq "$VerifyZoneFQDN." } + if (-not $Zone) { + throw "Verify Zone '$VerifyZoneFQDN' not found in STACKIT Project!" + } + $ZoneId = $Zone.id + + Write-Output ">>> [HOOK] Creating TXT Record: '$RecordName' in Zone: '$VerifyZoneFQDN'" + $QuotedToken = "`"$ValidationToken`"" + + # Create the TXT record. Stream 2 is captured again to suppress CLI auto-confirm warnings. + $createOutput = & stackit dns record-set create --zone-id "$ZoneId" --name "$RecordName" --type "TXT" --record "$QuotedToken" --project-id "$ProjectId" --assume-yes 2>&1 + if ($LASTEXITCODE -ne 0) { + throw "Failed to create TXT record: $createOutput" + } + + # Let's Encrypt requires time for the DNS entry to propagate across global nameservers + Write-Output ">>> [HOOK] Sleeping 30 seconds to allow DNS propagation..." + Start-Sleep -Seconds 30 + +} catch { + [Console]::Error.WriteLine(">>> [HOOK ERROR] $($_.Exception.Message)") + exit 1 +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 new file mode 100644 index 0000000..bc69f39 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/app/lib/StackitHelper_CLI.ps1 @@ -0,0 +1,127 @@ +<# +.SYNOPSIS + STACKIT API Interaction Helpers + +.DESCRIPTION + Provides functions to interact with the STACKIT Certificate Manager and the + Application Load Balancer API to deploy newly generated certificates. +#> + +function New-StackitCertificate { + <# + .SYNOPSIS + Uploads a local PEM certificate and private key to STACKIT Certificate Manager. + #> + param ($ProjectId, $RegionId, $CertName, $FullchainPath, $PrivkeyPath, $CertBaseUrl) + + # Read the PEM files and normalize line endings (CRLF to LF) for API compatibility + $cert = (Get-Content -Raw $FullchainPath) -replace "`r`n", "`n" + $key = (Get-Content -Raw $PrivkeyPath) -replace "`r`n", "`n" + + # Construct the JSON payload expected by STACKIT + $body = @{ + name = $CertName + publicKey = $cert.Trim() + privateKey = $key.Trim() + } | ConvertTo-Json + + $url = "$CertBaseUrl/projects/$ProjectId/regions/$RegionId/certificates" + + # Store payload in a temporary file to safely pass it to the curl wrapper + $tempFile = New-TemporaryFile + + try { + # Ensure UTF8 encoding without Byte Order Mark (BOM) to prevent parsing errors + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($tempFile.FullName, $body, $utf8NoBom) + + $response = stackit curl -X POST $url --data "@$($tempFile.FullName)" + + if ($LASTEXITCODE -ne 0) { + throw "Failed to upload new certificate '$CertName' to STACKIT Certificate Manager. API Output: $response" + } + + return $response | ConvertFrom-Json + } + catch { + Write-Error "[ERROR] New-StackitCertificate: $($_.Exception.Message)" + throw + } + finally { + if (Test-Path $tempFile.FullName) { + Remove-Item -Path $tempFile.FullName -Force + } + } +} + +function Update-AlbListenerCert { + <# + .SYNOPSIS + Patches an existing Application Load Balancer to use a newly uploaded certificate ID. + #> + param ($ProjectId, $RegionId, $AlbName, $OldCertId, $NewCertId, $AlbBaseUrl) + + $url = "$AlbBaseUrl/projects/$ProjectId/regions/$RegionId/load-balancers/$AlbName" + + # 1. Retrieve the complete, current state of the ALB + $alb = stackit curl -X GET $url | ConvertFrom-Json + if ($LASTEXITCODE -ne 0) { + throw "Failed to retrieve current configuration for ALB '$AlbName' from STACKIT API." + } + + # 2. Iterate through listeners and replace the old Certificate ID with the new one + $changed = $false + foreach ($l in $alb.listeners) { + if ($l.protocol -eq "PROTOCOL_HTTPS" -and $l.https.certificateConfig) { + $ids = $l.https.certificateConfig.certificateIds + for ($i=0; $i -lt $ids.Count; $i++) { + if ($ids[$i] -eq $OldCertId) { + $ids[$i] = $NewCertId + $changed = $true + } + } + } + } + + # 3. If modifications were made, we must push the entire state back via PUT + if ($changed) { + + # CRITICAL: The GET request returns read-only state variables. + # Sending these back in a PUT request will cause the STACKIT API to reject the payload. + # We must explicitly strip them from the object. + $alb.PSObject.Properties.Remove("status") + $alb.PSObject.Properties.Remove("loadBalancerSecurityGroup") + $alb.PSObject.Properties.Remove("targetSecurityGroup") + + if ($alb.options -and $alb.options.ephemeralAddress -eq $true) { + $alb.PSObject.Properties.Remove("externalAddress") + } + + # Convert back to JSON, ensuring nested arrays (like listeners) are not truncated + $body = $alb | ConvertTo-Json -Depth 20 + $tempFile = New-TemporaryFile + + try { + $utf8NoBom = New-Object System.Text.UTF8Encoding($false) + [System.IO.File]::WriteAllText($tempFile.FullName, $body, $utf8NoBom) + + $response = stackit curl -X PUT $url --data "@$($tempFile.FullName)" + + if ($LASTEXITCODE -ne 0) { + throw "Failed to update listener configuration for ALB '$AlbName' with new Certificate ID '$NewCertId'. API Output: $response" + } + return $true + } + catch { + Write-Error "[ERROR] Update-AlbListenerCert: $($_.Exception.Message)" + throw + } + finally { + if (Test-Path $tempFile.FullName) { + Remove-Item -Path $tempFile.FullName -Force + } + } + } + + return $false +} diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_detail_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_detail_level.png new file mode 100644 index 0000000000000000000000000000000000000000..e2b50617f5efa9b6d00ceeb038fe1eaffd6befc5 GIT binary patch literal 34827 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 m%t$Mv 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 literal 0 HcmV?d00001 diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_high_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode1_high_level.png new file mode 100644 index 0000000000000000000000000000000000000000..6acbfa47aa0bf6bb1e935a1d5c6584425c532ba2 GIT binary patch literal 14067 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 literal 0 HcmV?d00001 diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_detail_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_detail_level.png new file mode 100644 index 0000000000000000000000000000000000000000..de227cd6b7280af24674bd57b78798193cf30a21 GIT binary patch literal 58038 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 Ef}2gdMF0Q* literal 0 HcmV?d00001 diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_high_level.png b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/stackit-acme-alb/docs/images/mode2_high_level.png new file mode 100644 index 0000000000000000000000000000000000000000..987592f15b9ca077e6886d338f98837d43f1be3d GIT binary patch literal 25101 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

;rSow_c zR#Z)<2GO-Lt$I<9Nwd7JG6?cDc&b@p?|A8;l7?2}6yxFQ zV{a$^jB2^(wPTN`h!&|OPP$3ZgkjQ|$P^*Ex8&}MkNI}*4jOh{s_RhszzR67iT_MP zHIO-#R!Kv#7}hbroKJa5r&kUQgM~4C)QY+#aIK8^(;ZzG91a!}IGFaX*RBRZgH3+i zPjA`8=%6_z=!N(Faiim=~+Q12C%^_{y}|JpwyYdchL6pK|WFZDLT;6BdS)}>jUUVM+~ z`jRed333(S+#-n59c@SIYm&E?Rr{Ip6QTaWE}oK9nc?aHyBq3F$ut=d*#0R_KQwY< zQ8s%R?yXCLh_YVBtJ1g-e}MA)Tpz!4_o2@eNgpfxwm&uW5j$7U!D4NgnR-M@2{qXs zOf%~b+oqa8;K<})(yQ6l+F5>Nzp13ZH&OxK5i+&X?UPRjP`|YcwcRZ38f9P_o6(HG zz=dmPbnzAZr?!joYf-?Ql@{X5dQ-`MXWNTZLY(!Vx%Q8m^%=SLhmw1@o?D+z zL&T_rbz8f!mjnIOGNB+C7;iz4EjsPfGOwB()$5DM(5!zD_xoWl3YDmUQTkg_?z>8Q zg$kBk_8a=x!MiHpU6s_e1S;!Yg^T8-^!r@9B=R2v8G`Lf*`9@Q^(P{CeSUarlL zCQ!z=q3fL=Y|FWK8s{D$Zv*@_GIN-U84`-qG8(By+8Sub&tgA|g8CDe$@2jaTljy5 zJU^ehl71KVAcWXtlZY2aXmkzY-z_^X&LdQOrUk=nh^jZN%AC2*4;1SmBVvbVN!GwzJ4ny`FhI zDSEw4qELz=roYYlyea;;$pc*b)6V)^C8(AIKfxXKmo$V9s|4ceLOFO94mVYY6tCWa z`CGxk(9C1B%4s`a3^hr+TxM$Byi{}yE62=sEOg!xCY|GQ=DshI7u{C;+Q^y0kOaV> zLD+7{{ayd;kJ)DniBtjyytC)M&oaN39>3&`+~loGI_V`-D>b(*Kz`Kfs|sO~n^SlB!U+N$0S_bh`~ z-OrGEBkO|GCiJ!(qfuU)XDU1`U*6+`D0A{OfBrF>6?M>|{CmYsB!^P&@ruNqNwHHmIb`)jt5p3$buG}@4h6o~6XV_c z`}q=hRxm__Les-7?7z=kh+OHU+ zkrz{ZS96)*#sIdM8BTkn$57rfFG7#u0YlG7t2j+hc6s~Q-IqOD@On~KnDDeJq%SM&lXg?CHA_fp{9XCH-9j)lZ> z-;+#d!cRxK58b&A(&blacUJgSaXCh4XzAWkh6!7rj)~b~tO}1G)s9EtvKMY8PYqsM zRhwO<>GukNkh9Et)(lFy7tg{Gw~C&#fPLSTW#^$`^>EGw!d)lupD-1z(Qv3Ob7 zu~6{%J1)0(X@;rU(`3!qdpUd$fQe67hr!`E$f!>_J*AlKn}VX6diZRIo3ZZ{jSgS- z5jikFk*PqyD%{(39RshmUUlX|Iu~#(G=2zNsjgYMhQ{DuhS&Zl{Yi4>f*e_h%~$7^ zKY{?vOp$2vrS9o_@J1ENMS*&`4IE&=RRD707&E2P*QTE@dESZGZVX~JnV9;}eP*&K zA(T--ZKF#VnNcT*={MQ`>KguA)=x;&#YCh zRTl*W0sp9?#UKV_8Q??x|I-VYXGs|@c_klM<8wj(hsH3#l#lYEE$AQo@0))RtJoH4 zDW(4@U;ML&>oFYXbg@_{)E5JGCUdvVXF<(A)tP-4#qKSUlEt)1jI&59k^R=8N?_k( z)YQ=}>#63g-95`scVR!bUdJu$w2Z=S(qLt?sCG(8M;VR%o&}4DpmF(nVCh}zL}pGF zM66eM_$Q;rzCamztX_D7gm0la3n!3rk_M-IQV-`2&K%QlZB+xT}|9@HKic+(bo zCay`i?X;6Kz>wD}Tb4-r>CLx^;UPsYf7OON8Z zCE>a5eO4}EE_Qi_hA(0^h?(uH?}e2$iWG8cTl{el5O>&i8k(>K#=3XXD4f!tm#$dN z`|$ZxIq|2v*7%vMU9BUnmM=D|O;D&^7PAXx;T#1>=RX#tz1?{<}n7G?BYiu?*2jT)omugcL7 zVyRkGPm2orB4aO-vjDn*P2dfKg%GWXKDwg zhpTZQy{-bqs%*bypS()R{I9_dG`BGswGk3e*NmVRL}S>UvUYmlRSQ1nNn*@ z#|CA18zEY@#2iJF!Hlo|W!lLj_*nwVgPLDfHyHNL$o>yLzkav0mk5UeDx9n0>NW+B z^%qCJ%kn|8sC7@1`osjZgE8IBl~# zL2@?@@@K&g5>a=e7(zj?^O`hj$K16EOOY)@m!2j94{`vr)3xEa%SWU}tkM`__B7M4`e(#9ZrF=35+ZYwYC6JY;U6F<*~eZ<09ok{6N4{qYYx>^aaFh}Opr@V?sp5kUq_zZ96Hy% z@aKuaZ+=2Ql;FLC@g^oVo9Xx8F8lV|bd}m`LWXe~Ti|}mL7y9B`IR%ZmV`D3TfG-UXLFBPzPN+a?cV$R z^a>XBwsr;5jVraQ85S-vs7U-qDRfW3Rdc{3w9@C=@*mObh-U%DpYqWNVq-wg6MDx? zKvsnj4#lS9s!(M5KYO*pnzSUleSiH_fxufNwKKyB|!@x3GUXD=$4uLK)*TG7sMn|>R zB2p#%uT(pps{?K|=@w9O5%hY4$$Qsl;thvYqnEg2ah?D5wlQ7<{^BCaqR$me80o&s z#HRY58AvghSo5YLYqVwju;%HTL505f35?9Q_!`JqyLBgO!OdIb^mxJ6eFNk8y?GR3 z@5@TMRFnVEuO@EwCAY9p0XR{-3`?s9CXf7F%cFWfn$MG z-Ttlb1KoWm7Z@V(xx_2h!W8~|pysNsW+$y(W7X0e26|~OXNIdFSsdy+BINW94$1@> zXx7%k0{C4O!o2+l7Jl*A-8c{;a*;0*ceRDKXvW1}T@MmxA(kGyK>iX9(Poly5z#%z zgMOYtDy0}S!TzV55h@EFx&&tC$A_G*vwOQ1QsC9aNj=!|AAsZMa6~NQ9&$e(9-4#q z`LDQd?21TDPFc4rssuAbN8YQG3v~JjApaxKqx=Rcx*HJx1zF8!FgT!M585~Sq2~Wb zzyDCdAR<8o+MqDdod2uYqg7f9TY8M(pKTVDE>m9z*hh`0V`X0h!STo8-oDyz z%SVNPMXDIC10`11pOl__IvaQrgM3(8kl9I+cRi1}%`RI;m;kx*YUyn**@#qJ`z<$9vLOMFa+exBEaTAEE1#=h)da zqTP?^Rm)_8-+`%iLcy*US7v98lXc@JkQrrP*&Gh<(JGshrC?P^Q~&dyn1vGZ!1IQ> z$vjs$coN4?>aE3z$xCSn%Iy9N+b9}<#SnVp-6cWz;sH(yrj*a)--uymIJ|yj!09(2 z+rx|n%2I&$NVQvZdN(6JNK6Dj`@wE`?x&4XVF}-!u={gL@9Iq~Wgc{d?D@>U2H||) z0#)=kw9e!uTvz~bu3}Z*gC!3zAy;7*(JeN<^j2V>C8Hp=D>WQG|B#0Vy8@cAToQiq z%_f)!z6_&=#S1W-y5~6PGKj3t;if(}$`t?^R9AmmT^j?g{=P+Q0N8rDJ_m*3J%&;a@ zf-w`GGRfrm{)wtBg6WoX95nrSKb-`6INdoB<Ttw;mz- zfX9%|-Nvd`1*~~MxhecRvSFBhM3gf1T&J76k>N(fqI8vEOesi&+<)@ZMn;gm+9-O* zE(x3FHksyjk477OJe0oMl~>u;VE8G)kBHF$?`=J&IRhW5J&-snYPz!D;fp$9jlOs( z!VL8o3~)xbqQbKDm~itg(v+UwB?`bTN#`|~QPXkb{Ho?g^jm@^`~svb(B>-rDz3yE z6luc(I&Mo5yW+ThiDDyoWw0p*12+t$mkT2G=Cq^Rwg`WxD3Tv^X>EE*-rs!sYoaM_W)IYQ z`ow~y7RzvZg@VRU-Rt}2KdHCnE(p{6*_uA@P(AK=VCK@9#@${K31$%+?s}o;ifw^i zp1?RE#pC}bxrDr+T9?$$-HlU51ZgfLFvbRfj2U0h?t;8KLX1Pj*>#4C<|7NDISMQq z|9cI8@{WLOpl7t1zZv+dc-|t=^9x8sxWWyOi%>1{I8Ys#A|8y+h zEW0JTU8SV{OD7`R|FRD>`f&c!I z)Ma+;3G(Oacxd*zOtr4Gr|n2pB1=DC3FQ6rI^wrx;`2|Id~9p>#Pkhl$fW;i-!>k0 z2k;&#n!I)7Ms20p29Blf?;k9uo}bg_XR-fFLr&-SzN!`JA);{W&d<~tX~7t;XqVWk zcL;X=LryAi0>;DN0FCz-Ujjxj>1A77mTz?=K(n~JgbM=nZsPLb+EA> zks$wX23N5b1|1`NzwcCp?40gMj;``a>q}eHXPZN^dp9&X8}`&>HNLqda2JS71&;wG zm#t2MM5cP#N4YwTu;y;JRd`JQl>`wfInmvZqky-{NF3ZHxCb5Or>ESuJF=@CAT_~% z(XC1ojha%}rB7uY|GUz0W$%V9DO8yIG#M*lM{z6-i(|g{X+nV?(3qyweHrNEOKZfT zj!aX~?~kpdzJNdh`M_wpv?9#bN3(a)c?VLRS{SHp1Z1!&xy}HvHw@c6APsRqcBAU# zzxbBy9rh1@scYeEUT#_H4&`%WwGPxkB0mDx|Dv}8Zb zM?w~I%Bz&ZhyO-l)?GVI_pR!eScX@+`;&bcIB4#tDP9=HoUbJf_iSj~_Y+SuSFMt( zuhog&aVS$e5lS_b1O`pbYbrzi{IF(X5s9f+%B=N|B_BQh7Z^mV zBf{VCb+=Rl3%KGGEmNFd!3uY^RAXUkE}shJA3slLtb}*9+h2IB`)-*O5!a4Mta`UN z>E}vA%0CF>{G2GQ!hj9RVA}?z%P!syx zJfEWHcEK6#{Qdb7JK!(jP=M0_$7~7!Ld>=WC<01i><9f=TV)T|b@k@?^66_(NKt23 zuZ99k#U17vPX3?Bl1YEoI5pWTufs&)&s3|kH^jZFyy(;B>`d#Yx({M^OLTN)+;Q%B zKKE5|E!YpWV^$}O-C%csy)Wt=)c3Vq1#Mbwkrm|>mMBU_1wcLs`CoYSGp!IrcAf|u zafPgqbPS-6I1qQ*=|3H_Kb|tnss=r-hjOU?ssfkWb zu>PEQZW!gruKBnurbD@p1133nUA>%Hn!1;wo3Fq*6rboCtf*ebttFrKEnO?M_NQ-p zLugrtGnU!;)>$j@1uIedwpopdXH&|}kPR>To$8yxcLfIN-~c%IW?K|Glz)92jTYw_ z2Oy<9DsDn073f$p`jvOB2+8ax;NZ%*WmNCAv2HN^_!!W|=n%1IIcg?|?GK6Nj|Lb* z=)~jQN6P|`GSb<_^$IP)_X*pU(B=pAsy6$?JF8IYV}-vNKve9#7Dr^x3k}t0C{^wK z#a#}(l7bhIgNVBnC%c*q6xJ5QFsPDKMnI9(Q?7c`WNJUxgGUYHG@{uIaCs=k2Xwn? z=Sv9(e5QcjU-Jkz0q;GKTh1yABL5>eXPDi;kLbpWAtId*7+YGmlHN%@bZH>`AW#`P z$E$nbWm`?7!A^eDou1cN9PO@mSZnX8!*oXNHp3v2N^~6PZ9CINz7SJc1ng9Q#}gyh z+GBRj=LhES6h*LQwc5>8!lBbmtl#D)9hEn3V(%Zbf4v5BpYlR_4a=h>hk}xy@IBfx zr?3BIUN%6FJiI(;ub&fm73bLbxy9S?jfm>pb0_qTZ18@SiLsS+FpFDJCO7Azyvy;k>X)idWy1vB2qxrl z$h=1My0_c1u_Z=V!eJ9VGl4pN^6|^Q&T)@ zi&MBZ>|e<>ax4ewTb-UEme}Z}m&5Wl2kjw5Npy<=dBwVJchPPc`s|0H#(Yl_P>SJu z7DyC(@&ZCOxcZwikPsq*TYhgkh!(lnwOu<}yl^N3b7Z8%`3{qk7OZuFBcth};CPBWQz2Wj&eAloj~j0-_-xlu2yTHTJ@cSOB~wrsO~LB=X zx{zOJ6LI&rnp?{{Sa%KXYN<|+xcA=hdPL4!b>{WQ!;NdyCkHm3>Fr#Lo3fJxe0oY; z`iqt1$g}=0#XoX29YhM%q%2?`-W?^|AGX}8wS@Ebm|R)hzE4#yOKR^+yB&4lhm!<< z>>f2FKHsv!=l$5_Qq*@gi+^fRiG)0z@A+X1t` zLBaeRu*0c<2#&^eZ({lEQ0iI}&+S!Hs$QTFP3 zTo1@r;X(PYRd}C$49h&!cj>wmKF;RFz(qcdD4fw=oLq!`s5#3ZAHh7vzOPPtjE70n zBgsVqYrY_CM=*&z*KKiGUU+L_<$APA7{wT5)tXf44Al4NK=rr1MxBq+oq z_)+9Vs{;teS)$u&B~Y9ymZ}_AAHyzh7%{2gW?mx=56`tzoqEtD*35wD%tlCIqE-?T z+?{qzsNJUT9&9S&4KfU35wCoU)+E=LMTb=PktA?EEi&<|ql9@SY_pmO_kwP5zD{8L z(bhWFBlidhwJ{Wwyql<~5A54m*<@9kkz#cO%}^ED9R$|{qsU_^$Lmen`y@8d=6VL> zJ+exJ3hXQkj__+ic+ z>Q1UXxz4wOCWUyCz{E-Va)PbhmM^G5ne9INI5;oE)xzh>f#fmDatACRi8*9Qt}v0u zvW(?~v4|j9Y|C4`Rx)Z%N)s`q?$r2mroQ{|w;;pfsNuoUL!0o*6@6ahcD>L_u;a&c zCx3ug7c$=8EUWxdb(y%)Y?(f+EqM03w%#W<%T!x}74w+w7~&Yc>~Snn26VDytxG^F z24(*29~lwM<@-u*{Ab34ZOqA_Q+kgDmX-x?oVMCM>M zO|S445ot;p%>I}}NQo$QA$ZT`Ku7ZhvP&=pmmr)i{3l3m1;1fMx!M391|&18m}8}Q z!N5!=kBt5EH`)kRLK5uV6u4_JHEL)|F6OU~4az;andif%M}%DtMSl(wQ?kt+1JUNv zi+0hzf_8Y2U6IXNeOvn^EiP(>{}UHQ)suLfzjWPh4VF?WuQz>7$y~zL?X~-RF!d}+ zgWNVY)PkEVP7<%DzP*qx8O{Na!7WD_upv67YQ zSmpG7K4Ma9*BBU&=>G!)A6TV0`ZVSm%2cYeSB&rOm9W--o3$~VL*}{4eIoNWeO~6$ z%<#nF#x7G0%E#sKPZUz)@i7^eat$-k2F~_KzuPsvB@?eh=N*u(_c8|6X553chsJ|x za0>KnVB5;aGX{~(hU_c@4YDCH{!g3!D7J+0HGu}g6li1?5<1|;Diw>@z057JNoOrz zE75r9cQE@^&<$cl<28dGl<|c;WE7fig*y;{Z8s@|FM0^{xXGV&5Q{&&aq&w99pu>1 zCF+nsNFG1{Lhq1Bxmh2_KmNX^f9$>1e;oxTH2<<2PElGJBOHJ`qgrGdbSx@;$c!ESy?<&8A@-O@;)6TU~wv9;K+lt@AZ(+B0jf zXt@HXI>IG!Sx+#BK~|`wBuL;@A_LPF$~O}91>cp69uU;&UHn_lN9_|VK0yX0_fSP< zX68|-1hPgmyN#x~)cgWmJf`W+SIjQRgP9I-qnRfUf#3}%EHAlNJCIsaMqzR7 zha`)7Y7i?4@{&HFoFIPiXnd1&#*2g{Kr5qP4m=29NN;ixK{;z4i5foz~dJpbXBEp1?G4@*Y?E zjs~hZIKpvt zZ$1g=?j8g^0AnpaYYf0D!W7}Z{t10`TMh!~+9P45eQ__Rl(L$XyVLtsNAWDIB#Q^b z7fPSI_z6YbDpBT+;)hmgn?sVZsSVIU4oqGjiZQLZ?X_z&*%m4Qyaq+FJ-eP`+*RbS zR0`|kY7F`><^F}bky3rEVV<`Wn=^*CzO*4S0I9yt)JDs9Da$i#KkVmKYjRf)w>=^9 zo(hMpaSzfpc3BNgk<$`ky!O#7L|eDy!wLV^fKvfO?d`6x)deki5H3M(1yhtsTp{o_ zAL+_b=R^P8#}ozQWPEpjt~bNuBPw_>vQ#9z1k1n8LD2x;rL1SDxY9J}ltCql?CR_^=s>x@ zym?U$Wu6jPLZGL#CK=sN^quI<2$jImDU?$z3jIj7&#p0(h)3!32Eb0NZN6PeJj?Lz z4a^)0Wef%Vi|Sh+Vr>Q5aYH4l{<$uiuxD0n#OaNSHU1&O2DP2QhULY2<4)pdm9_4u zVdH#;7~vKhI<+QXSFl&E)}_&T8+qBJwrYwLlf?z~MZ=tPeNdx!Bbi@K<%@01U()?0 zHY$r~F@--haGJmL?_d%vgTx5XuX|;3uOf>M9~W#Wa$zm>bAI9E(Br_Dp=kzDi++}k;bi$EL@#rXs$Gvv;{~lw%kO|UbEYsCUtre#= zE+d7q=z*lH#bN$2sylSWE-%r{@yB=Aomm2R`cL^wkDK0b)fCN(wmJkX>PMp=rr!4K zTkg8bR!C@};`NmvpGTP{MG&~cxxD=iTJ7MU#q9Fk7Q}vl6UwDDINiW)>D4AdDbr|{ zC?PjQ0%G#%s7Z6;N8pf#C-UbXIA3gO)tk`OXjRo|(DjO1sx>UqaL4F4m-0`u_>FZF z&pi_zJ*kC;O5gDs1ZqU4T-f}~&JY^odLTIPhX~`sW)W!$jN_>DRI5TP4!JIdkfxf9 zsO;!qe7mcBF~q;rHZm?pS14(5&E_8U*Io8iR3-7xZQ>1gXTZ@iA~iAZ8GdVxlt&nY&Hon7a6dk?gma6k0y&+XrGs>A2vCGH zp3lLtwZbmf9j?9LwG&e3Ys#WZG1s3+F#qUZ+eX`ZN$Xojs1g61vmRF(N zj>M05XU2bRvN7afc6A8z0$fthw@8F=Gbt|!Rp3$oUKIEOObw9v=^jdT-|_`B85lh% zLL0FS^8ejma5bQzE~u>hwxOQfoD%U;XpadrXxH)E$N#5|vyO@?VAD9=sB}tqsdP6; z3?L;E(nt>7IkX_%h;#`^cZYxjC>_$B64DKO0p0c6-GAmB<{a+Ky?5>#&-;6y!M(1nf zCG*(#-^5x$`+u|W16=-Rv*)g(1jP3(;QFBUrp?k%BUuESyezcP@>gKp^PtpoC3jPe z?*yk@l*8z3MitA6=3|3$ux4yLJq9$AJ9N9;)D!V!;d?|RI&e)is4HPI5EI~|?hbf(4WcjjEgX-tm zv%@8^^N|xq$w(OF(IY{{eC1+}2=^V7*BkHWCHU>F4FR>9o9%ePm+6(`GCrCDTZS7b zZ6ApgC1d>Auf`|?`8;+}-4LnQsC<9_Jr*35-WvP_UTn!FuZp(jxO1mRIkuxRaLXUOvLxcA7FZeo?G|^ z0KMwx)!z08oCXE!W0RKlh?*Ur+M7k$(@BD_z*4|Mnom1hB8v?G>#Gg#0UENfb3=?+ zl-;lPBmuhz$>1JE&IP;f&IrHWmt?;eeIb9witpHnN{fH0pwQyH`^uF4=}s4RK$PK; zd6O)KR4j-a$>6u-qy#%E1P<048Q!v{6u}XuAR6dLeuWmN+#BzYK7cL4l+|106T~>r z@NvPe6&e+zwi2@%p7qKq5;p*^0Sq)`1Se|{@ z*&4;BG{pB^n)Mc^Qeu!P2pF>SMv-IfN||g};IrqkONTUXQsIJ~r#qMvS-@(6q=rlq zT9`F>PiRO7lj*3O>%w6tf|e1r2LA(Racwr?ozBPN4dP9Mm>$Ka3ssq@L{JV=v@^3x z?$HK{0ez({B$@M_Yx1uo+iZ2VSJDavkMmZ`J0^b-bo@MTOvs%P(UtE|AyKE~ix*}s zmaWo8EawFt+8ECLi9GbmXYS5QzJPq2_W54E^#k-+*j!2;J$A0!4@;(SIa@Tg!?90; z%gkZSF}@@^XKS1_Tbks$M8D1miy3}m6_p>Ah%7MI;19Hz6SAIVZz`H#rGQWR^co|Y zIK>S^pM(K; z5Y#Cd)%`!cmh7jnXG1m&|J%n=EMf>SgJ(ku5J-T*l&!uy{bb#gNCMtA`8 zRn>(iv}jDW;6aOZSIM}{q-ncPTRe9uh@fYm-yE^e)d0e~HHdLgg#THo0BK(1-~%Z0 z&0tUL?+3!=5AhNgvx*5tlP%!oD}Em%1|+igTz|W=&H-5*Ts`@+fvEE!Z)bjA`pO~~ z91~PY$&Y8?FGHk8l>EILSH?g^lG5MWb|`_P}T9TqDbUu^HuzD~`0P%+tp;zDAv&0LAkLlg^( zUrfFhzZWj&bRxEuuB$IwpfZ&acOoUglwbq^9a$ zMg1p5{ZU#WbFSvCYC1EXc_%`Wn(n&!$=p5DlNL*%%R4+_uh|ncs`FP##vRsG2qAbl z-SGEQ(ZHv%u7GNsmcg@|tZQvyaj}iETJxBEM-ZpS;eh5`8Hd280`s7ib?30JM)Y0Z zfxTD4>Sd48YyP&9i*IBEE}$h~nCtF0N0{7CeXIA>nh?)j$SR^?F<_Ohi@esSEY22| z$oM34TzgsR`$SNsO%)sMvd+8-m;bZ+=I$Pcnd6h>o`t)(AC&B84zyh|tAZ>M<8Kcm#EA!O^b-F?i$B#;y2S?;y@>aJxxjm&A#Wgp z^V(R4?DPZ1?_Oz>2bi(fYQc>BWUZxrIkz!~VAKtH4C9Vn%nd;_)K|7^^5a*(KKq(7r0PZ*WXmzEW z%wSpnRKCAz8AEY1IidDGgo>l^-F>Pouu4I<}e^M9Z$ttzB zUN66eP!BgVF0qb`Xp9mL@OO{9Kc==>cu=<59t5(*@SDQim37l=i-ajiIzw0eL$TdI zVfvtX4e}9`P?bx$)wXsYxLNbfRlS#NCsu-elKdBA6FTE7@X&Z`V7uBB)on)dTaM&T z_R{7HvZ+wk*!ZN^J0ynWS1Kj5YME(M;QR?*>`9KmQ5?tdj?g&TAZ zz14dZIb|N|m|AgW4y`d@pToKXoKkA!;saPTBKu+ z?RiaFFpJrQQpj`+a?G!--yE3}oK0vOmsBB&3GzgZ5GHvX$&oy?ecZ|Yq> z;{VN$XIod8yZHOC=&UVA;Zp&E_ar{lN>kSw=z-F&jB~%E#d08#JaNdX8)Udf362z36@dPb|BWVYZKe(iP%jVtEli zSyEkH@S~>dgw?PqCXfPA=D=xJ4K;>C{7xXM(S4g0jEz3E+Xrt621p5J#9zlG(;1BcS%Y$E5fd}F8ZA@=EE)$ya&w%XPWs?e_emb z=`AGMo{Y!;}1#3Ma}s>mFN2s<_pcIwDaw8f-iF61?%nc`MqFtCpRC4DJVfK7F4pr=)Pk1 z(h-!)Z+3(s4&0)$JseeUoD%Vv`b%+VLcCG@XyAm_KwdYLo%PQX!}=Y0ewDS0Pv(!s z-fFx=_jt7M#ta5Imi)>WPQ2XWf!qDp0w;@9( zS~EnKJaRvZ5YfnHViQvr*tmU(j4x`K%U-+H>9;U<^{6N<>gSb=PdMp6c}YoNxJ@f; z0nuZ1Yy!DR&TT=yulk6}>(>!&$dI5Zb;OW|c*KMW7M$8S)sC@3`U+vB=0$;Yojc^a7GHE9h(TDp%MO+PIExviV=-w6@_Xfe&FpNLkS=SfFi`@7Q)k=C`CFM$N zkMaQX`aSCXU_veZ)M1HL2AF8rn`4=WB?!BQSE2NHvPM9t%$z6|40RrBVbu)d%W@|0e#&cjZ?VW@z75)T4mW6aO{bK8+ z@iI4%wcurCM^%uaGp$> zQ|h5b?CmO_8dWX_!#dUMZl>N@zW@@DR3kRX&UR>=B1m@YkD<&0%Zfa#N`W<=O|kr> zL%j=eAQRSIRsX-4#XsIG|8W%{c22{VTwf|ZV&wdN%2rDED>P?uKREh*OEE(*V{KAx~{ zp%vX@S+6EOkE%ob!Nb5UyPnnvmT4AAkwdZVXUqMvY3eE>dxWk2tu;-o5#C{%>K$O? z_c?*a4I>Y-g&UtHJ*u$v>-o9;_YF{_%{w1O^3qnj*XzBZA#7fFEEl6|mIcDrS1cn| zGD+KJH>O^YH^tKMEAXMi>27QrV(#%8 zUFM|{*{4hVe0m}QIrO>KK%T9%xP1xwme0ByH0LAqt)t)fkP(^kLz;BXfoOj!R-{>r zShcL+>6{3!$p&E1n1cMq@ z4$>PSgJWeW6Q=fmX|ZE;n5pu@r`O{LS=`IvqK*IL}u!@ zr@jSR?^&i=)PBFnG8^OB0K2a*C;Mq<;=q9z_~^qNMC3>;7-W89=MTRl5&v=TWR)bY zY4{uwE;TD-H)y8>LdR9bpm90qn=Rg=z_%PCnKD1!ghXcFwkzk;T?L!dlu|b$8`}*Q zh!s5M<~<(5vz;60xHQbFXvtx$a=gKj&CaRWcZiiKCn9=fR~SXQ+j6l`FQiN7HIAj} z+?8264Anzuw#w;)9y3%2J)uyqG%(Ak26ZjsYwu!Yj*q;R>qB|@+`W*cQ)T|R6M;G$ zR$yc;=7o88t1zMidR)@B%CQIB+TU8=S{cVC(rHjS{z&~YGm@_PExfl-1>-}1MIsI zD~nYDj;h`WFuCfQ!RF@*1aGg0!-t__KF)ab)7l{wYfX!9ol=BgD=04~f(-0`dh6Zi z(yBWR>Yn#09T;F@weD5&nMNOrV2s_GfRhQ?VuMTF-Ol;cJgiYv6PG>aZLq#pXkj?j zE_}mN+UkP#E7oLhj(@|0=+^*|G4)%i0>I-7+0hy4+ccL!IYiL*`1rblgU)Uv~} zj$B{cdGmTc#-xWRJ~lY^i&70YivpK|A!Q-uINm`)Xz)3u3sHLMBvrC_Q>RQY_z2~X zv7R}-J$-lJJHS|vAzz01P+4xl%l0d@%HlJ)BN!Qdba^Ju+j!-di2XN~x*he!$1i&$ z^=I}zdZQWXmV9KH`9XN@;Sv#t;<&ku9<$IPYfc=q5<}eV&zy_QVYdrUhd)!eQSzm)Bv8JL zq^tKtrY}+m4d|jp*{}!PB6nUK^(Y;qmOC*FBPL?hPYQGvgvO=OF8hp}NIc&RqULgI z=dHz0^4O$*g*23Ljx?3m-JMLWuKHoso3vo^CEPb+cT@M>-mm&32j;$%%0`!FOAE-Js7uG9D!ml#vwBE-X&-KE z7VIN58+>z?hP87SX6P*xQ>Wb)vCd9jM_d~-Sju0Q&KOXeyk)x}nXf;=T<3sFH=w9T zvJ*LS`5UpFm-|n#6U5s=VHR-{wUP9n@bwBUeiJh{mI<=#rg&RvCKM!mUQk1-?`li? zA*E)mjzVll6WfMI!~!|inhQN5YO~xi9@oTKT!(y>qEBN~*vQFYV$j&V(uf};Txoks~?=`=E1sHJsB*@%S zme%;u#sFrj1(85yyO>$)O<+op+gC3Jlcj?&K64;#ew6xkcgw0x$R&2&d#+b7&VTC~ z6dkd=ust=cOm)w+H$qM$f&IzOZf(vhsRYf(!Hfe;ASrEflYs=^_KKDZ@|3>&cPWXB%&{R>(7PF+|m)_AEas$yW&4dymKE(Wlgu zpFhs4reQ+;Tx9v_$L{ubV^at72?gRC5wjF4rT*B+T@UxSKU5^32KC_R#?6=+I zj@n&=t5u+It}Diw4jr6wZj={8`WJ?YN`<>wvsVk7#`$ZrH5^N!-D`Tbv=aNFk_@5j zL49f$Fcu(@x(~zW!Y{MPIqDZ}js)HLQ-1^skmdKpj=nz6m51{7JW7r{kbXD7v~whH zmrA3{J+$24zh^U!!_JjJUEdl$-bck8EP@r&hSpiR=HeH#^l%nsat9iL8iKMdt#37k;7Y7@ ztQ4R2c#_bUG=9~&Rad*-grgG;pj?g-jUi8yxeIsC>s@(M@trEFy}CJ@BPUWaK@zH2 zL_NKsx}8aMBmYUyIp|DI6}3ny31xN{%XTzg$c|&@ESc6-wpZlN<;EE74-{}=Ep4m+ zvUb>w1>fpX_E>RK3Sv=|?cq$c?m(_m$;z&k1Gc>cg|X z>yP+Qq7X3EX2hOp>PVo(q~ol_S&L_x63$!t^9sFW=G8%TUB5y-m+Hs*#?0K%fub*a zkcPFiu;>(Bh6@D~ZIZX#lWGoi`mw``O21C*GJPgr^c6BvHJDqe=pEEWn)9Y%>@d$P zZN02dd22k3bz|Mr3-ZynsmNUD9ZJ=O3tjFH6y-K?s?28KW0mW|9ST6uNC@jISc&ZG z#q#DZ#;aOi)e0?O5i~5f^c`s5AL@-=(qmw9{g5{D9F1n|O9LCW^Iqn#Vx^aY!uKPl zFCA&VesZ?QMh^Roi&$*t^c+Vzg;NHjBrL2ESu0haW7YELI3w|%=_tydJKXA77DJm< zj0`%jPZzGxb9sIHiAgz~#S6Jm`@S2_fMDS=$t=Y(x*WNSa3K`>$(t#$1Tek1WR|ew z9EdP5~dNG6+&F)6e)4Fl&fwrQSxGi)+?Jyyd_P1kV*i4v0+0w zovFh+vST$50*fRkoc1b?h8|Uo+DUnwf>`sT?=G>a>Ab19*=vaW7HMUd`_U*T<>$- + . /etc/os-release && + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] + https://download.docker.com/linux/debian $${VERSION_CODENAME} stable" + | tee /etc/apt/sources.list.d/docker.list > /dev/null + - apt-get update -y + # Install Docker Engine + Compose plugin + - >- + DEBIAN_FRONTEND=noninteractive apt-get install -y + docker-ce docker-ce-cli containerd.io + docker-buildx-plugin docker-compose-plugin + - systemctl enable --now docker + # Add default Debian cloud user to docker group + - usermod -aG docker debian +%{ if start_nginx_test_container ~} + # Start nginx test container — verify with: curl http:// + - docker run -d --name nginx-test --restart unless-stopped -p 80:80 nginx:alpine +%{ endif ~} + +final_message: "Cloud-init complete after $UPTIME seconds. Docker host is ready." diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore b/examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore new file mode 100644 index 0000000..5d20f84 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/.gitignore @@ -0,0 +1,20 @@ +**/.terraform/* +*.tfstate +*.tfstate.* +*.tfvars +*.tfvars.json +.terraform.tfstate.lock.info +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json +.terraformrc +terraform.rc +keys/* +backend.conf +certs/ +.DS_Store +*.bkp +.idea diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version new file mode 100644 index 0000000..22708fe --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform-version @@ -0,0 +1 @@ +v1.5.7 diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl new file mode 100644 index 0000000..adbccc5 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/.terraform.lock.hcl @@ -0,0 +1,45 @@ +# This file is maintained automatically by "terraform init". +# Manual edits may be lost in future updates. + +provider "registry.terraform.io/hashicorp/tls" { + version = "4.3.0" + constraints = "~> 4.0" + hashes = [ + "h1:5bCU/c+2HUh7GhclzNSH6gAuoCS4inW3obEtRAwu6WQ=", + "zh:0ab58d6f8991d436c7d2dbd89ed814709b949b07ac5a54ee53b0aec1fa772a8b", + "zh:60b347abcb56f45d97c56f14d895069cd15a83993f199777f571b79fea3642ee", + "zh:6889be32640349230de3f23856e6f04e0e9ced4a84a27d3f552fa54684448218", + "zh:73f8e1ecf7135033165fb14b7e8bf4d656f3ce13065ec35762ea0481975328c7", + "zh:94ce25ee253eca0b42cae9c856b36bca8103b6453012d1b279c3623c805f2d42", + "zh:96bc6de9fd67bc446fd11257872e1ffb1029a996ed1d65a3f6b43f6d408ad9ab", + "zh:97c609a310a51bfd504d704e036d72064a84bf0bdb36cc08cd4cc66098212b41", + "zh:a12c16e94533c5bd123f75032576b9dc91dd5d5ccd5f7cf331d0f2e1adc55cf8", + "zh:c4f014f876adf7af57188795050bda5b0029d8c7d7773031102b6c36dcf1fc21", + "zh:d9b0a21583aaa3df3a95394fb949a3c515ff71c2ff5a1fc4a73d364aa90bfca5", + "zh:da510d22f0c6d71ad19a76406f106b782448f512375787ecfabb338ed1e311a7", + "zh:f0e9447a9ce3a24cdaa113089e65663c836d8b9bfdb915a1c0284e0112cab5c0", + "zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c", + ] +} + +provider "registry.terraform.io/stackitcloud/stackit" { + version = "0.98.0" + constraints = ">= 0.98.0" + hashes = [ + "h1:/FB0wBnvmjumjykX+j90kSck6LMScDaYo1STO5Vp/kw=", + "zh:031028340fbaeeb5c4c6b1d5c6d6287a70cf253cfb89f04d462a1c0ab6237ffc", + "zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f", + "zh:0eee18f9a262fa58966c960f1f0863eed92cd953d0f0306ecc456b58cc2911f8", + "zh:1646966ebac0eb5d6c78ac5aa1528921d7a635f14d81300463a402c55e33cfd3", + "zh:5374ab9e5e6d837787b4f18bcf0125a1bf3ee2da40c022cc7695d6879fed111b", + "zh:6a5b9e1307055f8d358373da625ffcb4d77ec44f260d14473b10e5777380765e", + "zh:6c90090504474695ab7290d64386dd988f4fb65c90c74c9cf3a6da6226ae8a70", + "zh:8317218828f29be95ce712863646dc8968e146ec14e5ab258cb1e8f8b649245b", + "zh:9eef08e4fb7a75760f9dc8a422446f19a210ebf8177dd5aeb97444295f0120cf", + "zh:9f2147eee63feae75b96f17f3b3ebab8a29cd7164cdd08eb2bb871e5c425a77f", + "zh:b63ea754eea233292fb73d87a9810104da2bd347abf2ca0da44ac76591dcdddb", + "zh:de60bd928828a836e446f9f89e7a3bfc4e6dd73bac6827914087b34e4ad0c978", + "zh:f22d295b2e4e94ae1566e20fd752825e008a62250cf7243f1161c0bf4e986518", + "zh:f7e57bc7be2cc016983ff3ad50d2733b85e90bfaa7aa9e2192563dc9d422fb07", + ] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf new file mode 100644 index 0000000..dfc2f09 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-backend.tf @@ -0,0 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + backend "s3" {} +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf new file mode 100644 index 0000000..44c0dc7 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/00-provider.tf @@ -0,0 +1,34 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +terraform { + required_version = ">= 1.3.0" + required_providers { + stackit = { + source = "stackitcloud/stackit" + version = ">= 0.98.0" + } + tls = { + source = "hashicorp/tls" + version = "~> 4.0" + } + } +} + +provider "stackit" { + default_region = var.stackit_region + service_account_key_path = var.stackit_service_account_key_path + # required for stackit_application_load_balancer (beta resource) + enable_beta_resources = true +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf new file mode 100644 index 0000000..6af7b73 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/01-variables.tf @@ -0,0 +1,176 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +variable "stackit_region" { + description = "STACKIT region, e.g. eu01" + type = string + default = "eu01" +} + +variable "stackit_service_account_key_path" { + description = "Path to the STACKIT service account key JSON file" + type = string + default = "keys/sa-key.json" +} + +# ─── Resource Hierarchy ─────────────────────────────────────────────────────── + +variable "organization_id" { + description = "STACKIT organisation container ID — find it in the Portal under Organisation → Settings" + type = string +} + +variable "owner_email" { + description = "Email of the resource owner; must be an existing STACKIT user in the organisation" + type = string +} + +variable "folder_name" { + description = "Display name of the folder (must match the existing folder name when importing)" + type = string + default = "alb-showcase" +} + +variable "project_name" { + description = "Name of the new project to create inside the folder" + type = string + default = "vm-alb-self-signed-cert" +} + +# ─── Naming ─────────────────────────────────────────────────────────────────── + +variable "name_prefix" { + description = "Short prefix applied to all resource names (network, VMs, ALB, certificate)" + type = string + default = "vm-alb-tls" +} + +# ─── Network ────────────────────────────────────────────────────────────────── + +variable "network_cidr" { + description = "IPv4 CIDR block for the private network, e.g. 10.10.0.0/24" + type = string + default = "10.10.0.0/24" + + validation { + condition = can(cidrnetmask(var.network_cidr)) + error_message = "network_cidr must be a valid CIDR, e.g. 10.10.0.0/24." + } +} + +variable "admin_cidr" { + description = "Source CIDR for SSH access (port 22). Use your own egress IP, e.g. 203.0.113.10/32. Avoid 0.0.0.0/0." + type = string + + validation { + condition = can(cidrnetmask(var.admin_cidr)) + error_message = "admin_cidr must be a valid CIDR, e.g. 203.0.113.10/32." + } +} + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +variable "machine_type" { + description = "STACKIT machine type for backend VMs — list available: stackit server machine-type list --project-id " + type = string + default = "g1.1" +} + +variable "availability_zone" { + description = "Availability zone for the VMs, e.g. eu01-1, eu01-2, eu01-3" + type = string + default = "eu01-1" +} + +variable "image_id" { + description = "UUID of the boot image (Debian 12 recommended) — list: stackit image list --all --project-id " + type = string +} + +variable "boot_volume_size_gb" { + description = "Root disk size in GB for each backend VM" + type = number + default = 20 + + validation { + condition = var.boot_volume_size_gb >= 10 + error_message = "boot_volume_size_gb must be at least 10 GB." + } +} + +variable "keypair_name" { + description = "Name of the SSH key pair to register in STACKIT" + type = string + default = "vm-alb-tls-key" +} + +variable "ssh_public_key" { + description = "SSH public key string (ssh-ed25519 AAAA... or ssh-rsa AAAA...) — never commit the private key" + type = string + sensitive = true +} + +# ─── TLS / Certificate ──────────────────────────────────────────────────────── + +variable "tls_common_name" { + description = "Common Name (CN) for the self-signed certificate, e.g. alb.example.com or the ALB IP address" + type = string + default = "alb.example.internal" +} + +variable "tls_organization" { + description = "Organisation name embedded in the certificate subject" + type = string + default = "STACKIT ALB Showcase" +} + +variable "tls_validity_hours" { + description = "Certificate validity in hours (8760 = 1 year)" + type = number + default = 8760 +} + +# ─── ALB ────────────────────────────────────────────────────────────────────── + +variable "alb_plan_id" { + description = "ALB service plan — p10 is the smallest available plan" + type = string + default = "p10" +} + +variable "alb_acl_ranges" { + description = "Source CIDRs allowed to reach the ALB. Use [\"0.0.0.0/0\"] for public access." + type = list(string) + default = ["0.0.0.0/0"] +} + +# ─── DNS ────────────────────────────────────────────────────────────────────── + +variable "dns_zone_name" { + description = "Human-readable label for the DNS zone resource" + type = string + default = "vm-alb-tls-zone" +} + +variable "dns_name" { + description = "DNS zone apex FQDN, e.g. vm-alb-tls.stackit.gg" + type = string +} + +variable "dns_contact_email" { + description = "SOA contact email for the DNS zone" + type = string +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf new file mode 100644 index 0000000..935f925 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/02-resource-hierarchy.tf @@ -0,0 +1,43 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +locals { + common_labels = { + environment = "showcase" + managed-by = "terraform" + use-case = "vm-alb-self-signed-tls" + } +} + +resource "stackit_resourcemanager_folder" "showcase" { + name = var.folder_name + owner_email = var.owner_email + parent_container_id = var.organization_id + + labels = { + managed-by = "terraform" + } + + lifecycle { + ignore_changes = [labels] + } +} + +resource "stackit_resourcemanager_project" "showcase" { + name = var.project_name + owner_email = var.owner_email + parent_container_id = stackit_resourcemanager_folder.showcase.id + + labels = local.common_labels +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf new file mode 100644 index 0000000..c17ddcd --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/03-network.tf @@ -0,0 +1,84 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_network" "main" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.name_prefix}-network" + ipv4_prefix = var.network_cidr + ipv4_nameservers = ["8.8.8.8", "1.1.1.1"] + routed = true + + labels = local.common_labels +} + +resource "stackit_security_group" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.name_prefix}-vm-sg" + description = "Security group for the showcase VM" + stateful = true + + labels = local.common_labels +} + +resource "stackit_security_group_rule" "ssh_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "SSH from admin CIDR only" + + protocol = { name = "tcp" } + port_range = { min = 22, max = 22 } + ip_range = var.admin_cidr +} + +resource "stackit_security_group_rule" "http_ingress" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "ingress" + description = "HTTP port 80 — ALB forwards traffic here after TLS termination" + + protocol = { name = "tcp" } + port_range = { min = 80, max = 80 } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_tcp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound TCP (Docker image pulls, apt)" + + protocol = { name = "tcp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_udp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow all outbound UDP (DNS)" + + protocol = { name = "udp" } + ip_range = "0.0.0.0/0" +} + +resource "stackit_security_group_rule" "egress_icmp" { + project_id = stackit_resourcemanager_project.showcase.project_id + security_group_id = stackit_security_group.vm.security_group_id + direction = "egress" + description = "Allow outbound ICMP" + + protocol = { name = "icmp" } + ip_range = "0.0.0.0/0" +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf new file mode 100644 index 0000000..4170e38 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/04-compute.tf @@ -0,0 +1,67 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_key_pair" "showcase" { + name = var.keypair_name + public_key = chomp(var.ssh_public_key) + + labels = local.common_labels +} + +resource "stackit_network_interface" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_id = stackit_network.main.network_id + name = "${var.name_prefix}-vm-nic" + security_group_ids = [stackit_security_group.vm.security_group_id] + + # The ALB injects its own target security group into this NIC after creation. + lifecycle { + ignore_changes = [security_group_ids] + } +} + +resource "stackit_public_ip" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + network_interface_id = stackit_network_interface.vm.network_interface_id + + labels = local.common_labels +} + +resource "stackit_server" "vm" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = "${var.name_prefix}-vm" + machine_type = var.machine_type + availability_zone = var.availability_zone + keypair_name = stackit_key_pair.showcase.name + user_data = file("${path.root}/templates/cloud-init.yaml.tpl") + + boot_volume = { + source_type = "image" + source_id = var.image_id + size = var.boot_volume_size_gb + } + + network_interfaces = [stackit_network_interface.vm.network_interface_id] + + agent = { + provisioning_policy = "ALWAYS" + } + + labels = local.common_labels + + depends_on = [ + stackit_network_interface.vm, + stackit_key_pair.showcase, + ] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf new file mode 100644 index 0000000..9d0e162 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-certificate.tf @@ -0,0 +1,44 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "tls_private_key" "self_signed" { + algorithm = "RSA" + rsa_bits = 2048 +} + +resource "tls_self_signed_cert" "self_signed" { + private_key_pem = tls_private_key.self_signed.private_key_pem + + subject { + common_name = var.tls_common_name + organization = var.tls_organization + } + + dns_names = [var.tls_common_name] + validity_period_hours = var.tls_validity_hours + + allowed_uses = [ + "key_encipherment", + "digital_signature", + "server_auth", + ] +} + +resource "stackit_alb_certificate" "self_signed" { + project_id = stackit_resourcemanager_project.showcase.project_id + region = var.stackit_region + name = "${var.name_prefix}-selfsigned-cert" + public_key = tls_self_signed_cert.self_signed.cert_pem + private_key = tls_private_key.self_signed.private_key_pem +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf new file mode 100644 index 0000000..9d24069 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/05-dns.tf @@ -0,0 +1,31 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_dns_zone" "showcase" { + project_id = stackit_resourcemanager_project.showcase.project_id + name = var.dns_zone_name + dns_name = var.dns_name + contact_email = var.dns_contact_email + type = "primary" + default_ttl = 300 +} + +resource "stackit_dns_record_set" "alb_a" { + project_id = stackit_resourcemanager_project.showcase.project_id + zone_id = stackit_dns_zone.showcase.zone_id + name = var.dns_name + type = "A" + ttl = 300 + records = [stackit_public_ip.alb.ip] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf new file mode 100644 index 0000000..14cb37f --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/06-alb.tf @@ -0,0 +1,96 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +resource "stackit_public_ip" "alb" { + project_id = stackit_resourcemanager_project.showcase.project_id + + labels = local.common_labels + + lifecycle { + ignore_changes = [network_interface_id] + } +} + +resource "stackit_application_load_balancer" "main" { + project_id = stackit_resourcemanager_project.showcase.project_id + region = var.stackit_region + name = "${var.name_prefix}-alb" + plan_id = var.alb_plan_id + external_address = stackit_public_ip.alb.ip + + networks = [ + { + network_id = stackit_network.main.network_id + role = "ROLE_LISTENERS_AND_TARGETS" + } + ] + + listeners = [ + { + name = "https" + port = 443 + protocol = "PROTOCOL_HTTPS" + http = { + hosts = [ + { + host = "*" + rules = [{ target_pool = "${var.name_prefix}-pool" }] + } + ] + } + https = { + certificate_config = { + certificate_ids = [stackit_alb_certificate.self_signed.cert_id] + } + } + }, + { + name = "http" + port = 80 + protocol = "PROTOCOL_HTTP" + http = { + hosts = [ + { + host = "*" + rules = [{ target_pool = "${var.name_prefix}-pool" }] + } + ] + } + } + ] + + target_pools = [ + { + name = "${var.name_prefix}-pool" + target_port = 80 + targets = [ + { + display_name = "${var.name_prefix}-vm" + ip = stackit_network_interface.vm.ipv4 + } + ] + } + ] + + options = { + private_network_only = false + access_control = { + allowed_source_ranges = var.alb_acl_ranges + } + } + + labels = local.common_labels + + depends_on = [stackit_network_interface.vm] +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf b/examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf new file mode 100644 index 0000000..61f8672 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/07-outputs.tf @@ -0,0 +1,73 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +output "project_id" { + description = "UUID of the created project" + value = stackit_resourcemanager_project.showcase.project_id +} + +output "alb_public_ip" { + description = "Public IPv4 address of the Application Load Balancer" + value = stackit_public_ip.alb.ip +} + +output "alb_https_url" { + description = "HTTPS URL — certificate is self-signed, use curl -k or accept the browser warning" + value = "https://${stackit_public_ip.alb.ip}" +} + +output "alb_http_url" { + description = "Plain HTTP URL for baseline connectivity tests" + value = "http://${stackit_public_ip.alb.ip}" +} + +output "vm_public_ip" { + description = "Public IPv4 address of the VM (for SSH access)" + value = stackit_public_ip.vm.ip +} + +output "vm_private_ip" { + description = "Private IPv4 address of the VM" + value = stackit_network_interface.vm.ipv4 +} + +output "ssh_command" { + description = "SSH command to connect to the VM (Debian 12 default user)" + value = "ssh debian@${stackit_public_ip.vm.ip}" +} + +output "certificate_id" { + description = "STACKIT ALB certificate ID referenced by the HTTPS listener" + value = stackit_alb_certificate.self_signed.cert_id +} + +output "certificate_expiry" { + description = "Certificate expiry timestamp (UTC)" + value = tls_self_signed_cert.self_signed.validity_end_time +} + +output "dns_name" { + description = "DNS name pointing to the ALB" + value = var.dns_name +} + +output "dns_nameservers" { + description = "Nameservers for the DNS zone — set these at your registrar to delegate the zone" + value = stackit_dns_zone.showcase.primary_name_server +} + +output "curl_test" { + description = "curl command to test the HTTPS endpoint by DNS name" + value = "curl -k https://${var.dns_name}" +} diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/README.md b/examples/alb-tls-examples/vm-alb-self-signed-cert/README.md new file mode 100644 index 0000000..1b4d705 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/README.md @@ -0,0 +1,259 @@ +# vm-alb-self-signed-cert + +Introductory showcase: a STACKIT VM with Docker nginx, an Application Load Balancer (ALB) in front of it, and a self-signed TLS certificate — fully managed by Terraform, no external tools, no ACME, no Kubernetes. + +--- + +## Architecture + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant TLS as hashicorp/tls + + Note over User,TLS: terraform apply + + User->>TF: create Folder + Project + TF-->>User: project_id + + User->>TF: create Network + Security Group + TF-->>User: network_id + + User->>TLS: generate RSA Private Key + TLS-->>User: private_key_pem + + User->>TLS: sign Self-Signed Certificate (private_key_pem) + TLS-->>User: cert_pem + + User->>TF: upload ALB Certificate (cert_pem + private_key_pem) + TF-->>User: cert_id + + User->>TF: create VM (cloud-init → Docker · nginx) + TF-->>User: private_ip · vm_public_ip + + User->>TF: create Public IP + DNS Zone + A-Record + TF-->>User: alb_public_ip · zone ready + + User->>TF: create ALB (cert_id · private_ip · alb_public_ip) + TF-->>User: ✅ https://your-domain ready +``` + +``` +STACKIT Organisation +└── Folder: alb-showcase + └── Project: vm-alb-self-signed-cert + ├── Network: vm-alb-tls-network (10.10.0.0/24, routed) + │ └── Security Group: vm-alb-tls-vm-sg + │ ├── Ingress TCP 22 ← admin_cidr (SSH) + │ ├── Ingress TCP 80 ← 0.0.0.0/0 (ALB backend traffic) + │ └── Egress all → 0.0.0.0/0 + ├── VM: vm-alb-tls-vm (Debian 12) + │ └── Docker → nginx:alpine :80 + ├── DNS Zone: vm-alb-tls.stackit.gg + │ └── A-Record → ALB Public IP + └── ALB: vm-alb-tls-alb + ├── Public IP (static) + ├── Certificate: vm-alb-tls-selfsigned-cert + ├── Listener HTTP :80 → vm-alb-tls-pool + └── Listener HTTPS :443 → vm-alb-tls-pool (TLS terminated) +``` + +--- + +## Overview + +| Component | Description | +| ------------------ | ---------------------------------------------------------------------------------- | +| Resource hierarchy | Folder + Project under an existing STACKIT organisation | +| Network | Private routed network (`10.10.0.0/24`) with security groups | +| Compute | Debian 12 VM with Docker Engine + nginx:alpine | +| Certificate | RSA key + self-signed X.509 cert generated by `hashicorp/tls`, uploaded to STACKIT | +| DNS | Primary zone + A-record pointing to the ALB public IP | +| Load Balancer | ALB with HTTPS :443 (TLS terminates here) and HTTP :80 | + +| In this showcase | Not in this showcase | +| ----------------------------- | --------------------------- | +| Self-signed TLS via Terraform | Let's Encrypt / ACME | +| One VM + Docker nginx | Multiple VMs / auto-scaling | +| STACKIT DNS Zone + A-Record | Certificate renewal | +| Fully Terraform-managed | Kubernetes / cert-manager | + +→ For Let's Encrypt: [`vm-alb-certbot-letsencrypt/`](../vm-alb-certbot-letsencrypt/README.md) +→ For Kubernetes: [`alb-k8s/`](../alb-k8s/README.md) + +--- + +## Prerequisites + +| Tool | Version | +| ----------- | -------- | +| Terraform | >= 1.5.7 | +| STACKIT CLI | latest | +| SSH client | — | + +### Required STACKIT permissions + +| Service | Role | +| --------------------------------- | -------- | +| Resource Manager (Folder/Project) | `editor` | +| Compute | `editor` | +| Networking | `editor` | +| DNS | `editor` | +| Application Load Balancer | `editor` | + +### Required variables + +| Variable | Description | Example | +| ------------------- | ---------------------------------- | -------------------------------- | +| `organization_id` | STACKIT organisation container ID | Portal → Organisation → Settings | +| `owner_email` | Owner email for folder and project | `name@example.com` | +| `folder_name` | Name of the existing folder | `alb-showcase` | +| `image_id` | UUID of the Debian 12 boot image | `stackit image list --all` | +| `ssh_public_key` | SSH public key string | `ssh-ed25519 AAAA...` | +| `admin_cidr` | Your IP for SSH access | `203.0.113.10/32` | +| `dns_name` | DNS name for the ALB | `vm-alb-tls.stackit.gg` | +| `dns_contact_email` | SOA contact for the DNS zone | `name@example.com` | + +All other variables have sensible defaults — see [`01-variables.tf`](01-variables.tf). + +--- + +## Deployment + +### 1. Configure variables + +```bash +cp examples/terraform.tfvars.example terraform.tfvars +# Fill in: organization_id, owner_email, image_id, ssh_public_key, admin_cidr, dns_name +``` + +Find your egress IP for `admin_cidr`: + +```bash +curl -s https://ifconfig.schwarz +``` + +Find available Debian 12 image UUIDs: + +```bash +stackit image list --all --project-id +``` + +### 2. Configure remote state + +```bash +cp examples/backend.conf.example backend.conf +# Fill in: bucket, key, access_key, secret_key +``` + +### 3. Deploy + +```bash +terraform init -backend-config=backend.conf +terraform plan +terraform apply +``` + +Duration: ~5–8 minutes. Expected resources: ~15. + +### 4. Outputs + +```bash +terraform output +``` + +--- + +## Validation + +```bash +# HTTPS — -k skips the self-signed cert warning +curl -k https://vm-alb-tls.stackit.gg + +# HTTP +curl http://vm-alb-tls.stackit.gg + +# Inspect TLS certificate +openssl s_client -connect vm-alb-tls.stackit.gg:443 /dev/null \ + | openssl x509 -noout -subject -issuer -dates + +# SSH to the VM +ssh debian@$(terraform output -raw vm_public_ip) +docker ps +docker logs nginx +``` + +--- + +## File Structure + +``` +vm-alb-self-signed-cert/ +├── .terraform-version # Terraform version pin (v1.5.7) +├── .gitignore +├── 00-backend.tf # S3 remote state (STACKIT Object Storage) +├── 00-provider.tf # stackitcloud/stackit + hashicorp/tls +├── 01-variables.tf # All variables with descriptions and defaults +├── 02-resource-hierarchy.tf # locals, folder resource, project +├── 03-network.tf # Network, security group, rules +├── 04-compute.tf # SSH key pair, NIC, VM (Docker nginx) +├── 05-certificate.tf # tls_private_key, tls_self_signed_cert, stackit_alb_certificate +├── 05-dns.tf # DNS zone + A-record +├── 06-alb.tf # Public IP, Application Load Balancer +├── 07-outputs.tf # All outputs (IP, URLs, SSH, cert_id, curl) +├── templates/ +│ └── cloud-init.yaml.tpl # Docker Engine install + nginx:alpine container +├── examples/ +│ ├── terraform.tfvars.example +│ └── backend.conf.example +├── docs/ +│ └── architecture.md # Deployment sequence diagram + component overview +└── keys/ # SA key JSON — gitignored +``` + +--- + +## Security + +| File | Git status | Contains | +| ------------------ | ---------- | -------------------------- | +| `terraform.tfvars` | gitignored | SSH key, sensitive config | +| `backend.conf` | gitignored | Object Storage access keys | +| `keys/` | gitignored | Service account JSON key | + +- SSH access is restricted to `admin_cidr` — never use `0.0.0.0/0` +- The TLS private key is stored in Terraform state — acceptable for a showcase, use KMS/Vault for production + +--- + +## Cleanup + +```bash +terraform destroy +``` + +Removes all resources: ALB, VM, network, certificate, DNS zone, public IP, project. +The parent folder is not destroyed (it pre-exists and was imported into state). + +--- + +## Troubleshooting + +| Problem | Cause | Fix | +| ------------------------------- | ------------------------------- | ------------------------------------------------ | +| `curl: (7) Failed to connect` | ALB not ready yet | Wait 1–2 min | +| `curl: (35) SSL error` | Self-signed cert | Use `-k` flag | +| nginx not responding | Docker image pull still running | Wait 3–5 min, check `docker ps` on VM | +| DNS not resolving | Zone not yet propagated | Run `dig vm-alb-tls.stackit.gg` and wait | +| Provider error on folder labels | Existing labels after import | `lifecycle { ignore_changes = [labels] }` is set | + +--- + +## References + +- [Full architecture details](docs/architecture.md) +- [STACKIT Terraform Provider](https://registry.terraform.io/providers/stackitcloud/stackit/latest/docs) +- [STACKIT CLI](https://github.com/stackitcloud/stackit-cli) +- [STACKIT Developer Documentation](https://docs.stackit.cloud) +- [hashicorp/tls Provider](https://registry.terraform.io/providers/hashicorp/tls/latest/docs) diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md b/examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md new file mode 100644 index 0000000..1ab2fb1 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/docs/architecture.md @@ -0,0 +1,128 @@ +# Architecture: vm-alb-self-signed-cert + +## Deployment Sequence + +```mermaid +sequenceDiagram + participant User + participant TF as Terraform / STACKIT API + participant TLS as hashicorp/tls + + Note over User,TLS: terraform apply + + User->>TF: create Folder + Project + TF-->>User: project_id + + User->>TF: create Network + Security Group + TF-->>User: network_id + + User->>TLS: generate RSA Private Key + TLS-->>User: private_key_pem + + User->>TLS: sign Self-Signed Certificate (private_key_pem) + TLS-->>User: cert_pem + + User->>TF: upload ALB Certificate (cert_pem + private_key_pem) + TF-->>User: cert_id + + User->>TF: create VM (cloud-init → Docker · nginx) + TF-->>User: private_ip · vm_public_ip + + User->>TF: create Public IP + DNS Zone + A-Record + TF-->>User: alb_public_ip · zone ready + + User->>TF: create ALB (cert_id · private_ip · alb_public_ip) + TF-->>User: ✅ https://your-domain ready +``` + +--- + +# VM + ALB + Self-Signed TLS + +## Traffic Flow + +``` + Client + │ + │ DNS lookup: vm-alb-tls.stackit.gg + ▼ + STACKIT DNS + │ resolves to ALB public IP (Terraform A-record) + │ + │ HTTPS :443 + ▼ + STACKIT ALB (L7, TLS termination) + │ certificate: self-signed (hashicorp/tls, managed by Terraform) + │ HTTP routing to target pool + │ + │ HTTP :80 + ▼ + STACKIT VM (Debian 12, Docker Engine) + │ + ▼ + Container: nginx:alpine (port 80) +``` + +The ALB terminates TLS. The VM only receives plain HTTP on port 80 — no +certificate management on the backend. + +--- + +## Component Responsibility + +| Component | File | Purpose | +| ------------------------- | -------------------------- | --------------------------------- | +| STACKIT Folder + Project | `02-resource-hierarchy.tf` | Resource boundary | +| Network + Security Group | `03-network.tf` | Private network, SSH + HTTP rules | +| VM (Debian 12) | `04-compute.tf` | Docker host | +| TLS Private Key | `05-certificate.tf` | RSA key pair (hashicorp/tls) | +| Self-Signed Certificate | `05-certificate.tf` | X.509 cert (hashicorp/tls) | +| ALB Certificate resource | `05-certificate.tf` | Uploads cert to STACKIT | +| DNS Zone + A-record | `05-dns.tf` | Zone apex → ALB public IP | +| Application Load Balancer | `06-alb.tf` | L7 TLS termination + HTTP routing | + +--- + +## Certificate Flow + +``` + hashicorp/tls provider STACKIT + ───────────────────── ─────── + tls_private_key ─ PEM ─► stackit_alb_certificate ─ cert_id ─► stackit_application_load_balancer + tls_self_signed_cert ─ PEM ─► (certificate store) (HTTPS listener) +``` + +All steps happen in a single `terraform apply`. No scripts or manual API calls required. + +--- + +## Resource Hierarchy + +``` +STACKIT Organisation +└── Folder: alb-showcase (imported via terraform import) + └── Project: vm-alb-self-signed-cert (created by Terraform) + ├── Network: vm-alb-tls-net (10.10.0.0/24, routed) + │ └── Security Group: vm-alb-tls-vm-sg + │ ├── Ingress TCP 22 ← admin_cidr only + │ ├── Ingress TCP 80 ← 0.0.0.0/0 (ALB backend traffic) + │ └── Egress all → 0.0.0.0/0 + ├── VM: vm-alb-tls-vm (Debian 12, Docker Engine) + ├── Certificate: vm-alb-tls-selfsigned-cert (self-signed) + ├── DNS Zone: vm-alb-tls.stackit.gg (primary) + └── ALB: vm-alb-tls-alb + ├── Public IP (dedicated, wired to DNS A-record by Terraform) + ├── Listener HTTP :80 → VM:80 + └── Listener HTTPS :443 → VM:80 (TLS terminated at ALB) +``` + +--- + +## Security Notes + +| Concern | Status | +| ------------------------------ | ----------------------------------------------------------- | +| Private key in Terraform state | Yes — acceptable for showcase/dev | +| Certificate trust | Self-signed — browsers will warn; use `curl -k` for testing | +| SSH access | Restricted to `admin_cidr` — never use `0.0.0.0/0` | +| Production recommendation | Use `stackit-acme-alb` for Let's Encrypt certs | diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example new file mode 100644 index 0000000..c19f34e --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/backend.conf.example @@ -0,0 +1,15 @@ +bucket = "" +key = "" +region = "eu01" + +endpoints = { + s3 = "https://object.storage.eu01.onstackit.cloud" +} + +access_key = "" +secret_key = "" + +skip_credentials_validation = true +skip_region_validation = true +skip_requesting_account_id = true +skip_s3_checksum = true diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example new file mode 100644 index 0000000..d2ad220 --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/examples/terraform.tfvars.example @@ -0,0 +1,52 @@ +# Copy this file to terraform.tfvars and fill in the required values. +# terraform.tfvars is gitignored — never commit real credentials or IDs. + +# ─── Provider ───────────────────────────────────────────────────────────────── + +stackit_region = "eu01" +stackit_service_account_key_path = "keys/sa-key.json" + +# ─── Project ────────────────────────────────────────────────────────────────── + +# Find your project ID in the STACKIT Portal or via: stackit project list +project_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +# ─── Naming ─────────────────────────────────────────────────────────────────── + +name_prefix = "vm-alb-tls" + +# ─── Network ────────────────────────────────────────────────────────────────── + +network_cidr = "10.10.0.0/24" + +# Restrict SSH to your IP — never use 0.0.0.0/0 in production +# Find your IP: curl -s https://ifconfig.schwarz +admin_cidr = "203.0.113.10/32" + +# ─── Compute / VM ───────────────────────────────────────────────────────────── + +machine_type = "g1.1" +availability_zone = "eu01-1" + +# List available Debian 12 images: +# stackit image list --all --project-id +image_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" + +boot_volume_size_gb = 20 +keypair_name = "vm-alb-tls-key" + +# Paste your SSH public key here (never the private key) +ssh_public_key = "ssh-ed25519 AAAA... your-key-comment" + +# ─── TLS / Certificate ──────────────────────────────────────────────────────── + +# Use the ALB public IP or a hostname you control. +# A self-signed cert on an IP address is fully valid for testing. +tls_common_name = "alb.example.internal" +tls_organization = "STACKIT ALB Showcase" +tls_validity_hours = 8760 # 1 year + +# ─── ALB ────────────────────────────────────────────────────────────────────── + +alb_plan_id = "p10" +alb_acl_ranges = ["0.0.0.0/0"] diff --git a/examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl b/examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl new file mode 100644 index 0000000..d0a5b0c --- /dev/null +++ b/examples/alb-tls-examples/vm-alb-self-signed-cert/templates/cloud-init.yaml.tpl @@ -0,0 +1,37 @@ +#cloud-config + +package_update: true +package_upgrade: false +packages: + - ca-certificates + - curl + - gnupg + +write_files: + - path: /opt/install-docker.sh + permissions: "0755" + content: | + #!/bin/bash + set -euo pipefail + + install -m 0755 -d /etc/apt/keyrings + curl -fsSL https://download.docker.com/linux/debian/gpg -o /etc/apt/keyrings/docker.asc + chmod a+r /etc/apt/keyrings/docker.asc + + . /etc/os-release + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] \ + https://download.docker.com/linux/debian ${VERSION_CODENAME} stable" \ + > /etc/apt/sources.list.d/docker.list + + apt-get update -y + DEBIAN_FRONTEND=noninteractive apt-get install -y \ + docker-ce docker-ce-cli containerd.io docker-compose-plugin + + systemctl enable --now docker + usermod -aG docker debian + docker run -d --name nginx --restart unless-stopped -p 80:80 nginx:alpine + +runcmd: + - /opt/install-docker.sh + +final_message: "Cloud-init complete after $UPTIME seconds." -- 2.49.1 From 85c3ef80708350dddb7added008812e15ec0674c Mon Sep 17 00:00:00 2001 From: Sven Schmidt Date: Tue, 16 Jun 2026 09:40:47 +0200 Subject: [PATCH 2/4] examples: add license header --- .pre-commit-config.yaml | 16 +++++++++++++++- examples/alb-tls-examples/MAINTAINERS.md | 9 +++++++++ .../cert-manager/00-stackit-sa-secret.yaml | 14 ++++++++++++++ .../cert-manager/01-cluster-issuer.yaml | 14 ++++++++++++++ .../cert-manager/02-certificate.yaml | 14 ++++++++++++++ .../alb-k8s/kubernetes/nginx/00-namespace.yaml | 14 ++++++++++++++ .../kubernetes/nginx/01-deployment.yaml | 14 ++++++++++++++ .../alb-k8s/kubernetes/nginx/02-service.yaml | 14 ++++++++++++++ .../alb-k8s/kubernetes/nginx/03-ingress.yaml | 14 ++++++++++++++ .../alb-k8s/terraform/00-backend.tf | 18 +++++++++++++----- .../alb-k8s/terraform/backend.conf.example | 4 ++-- .../vm-alb-certbot-letsencrypt/README.md | 16 ++++++++-------- .../cloud-init/user-init-linux.yml | 14 ++++++++++++++ .../cloud-init/user-init-windows.yml | 14 ++++++++++++++ 14 files changed, 173 insertions(+), 16 deletions(-) create mode 100644 examples/alb-tls-examples/MAINTAINERS.md diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b00eab..6c7483b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # .pre-commit-config.yaml # To use this file, install pre-commit (https://pre-commit.com): # pip install pre-commit @@ -52,5 +66,5 @@ repos: # Requires `addlicense` to be installed locally (go install github.com/google/addlicense@latest) entry: addlicense -c "Schwarz Digits Cloud GmbH & Co. KG" -l apache language: system - types_or: [terraform, python, go, javascript] + types_or: [terraform, python, go, javascript, yaml, json] pass_filenames: true diff --git a/examples/alb-tls-examples/MAINTAINERS.md b/examples/alb-tls-examples/MAINTAINERS.md new file mode 100644 index 0000000..345a653 --- /dev/null +++ b/examples/alb-tls-examples/MAINTAINERS.md @@ -0,0 +1,9 @@ +# Maintainers + +General maintainers: + +- Sven Schmidt (sven.schmidt@digits.schwarz) + +This example is actively maintained. The owner is responsible for reviewing and updating dependencies and functionalities on a monthly basis. +For questions, issues, or feature requests, please email general maintainers. +Please include the BP name and version in your request. We will track your request as an issue. diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml index 1baaeeb..9a38fbb 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/00-stackit-sa-secret.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + # Reference template only. Do NOT commit with real credentials. # deploy.sh creates this secret automatically from terraform/keys/sa-key.json. # To create manually: kubectl create secret generic stackit-sa-authentication \ diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml index 21b74ba..f028e89 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/01-cluster-issuer.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: cert-manager.io/v1 kind: ClusterIssuer metadata: diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml index a1cb5cf..3b27a70 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/cert-manager/02-certificate.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: cert-manager.io/v1 kind: Certificate metadata: diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml index 0455b17..13357ed 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/00-namespace.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: v1 kind: Namespace metadata: diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml index a0d7767..15e71a6 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/01-deployment.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: apps/v1 kind: Deployment metadata: diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml index 62ee40e..ec5069f 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/02-service.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: v1 kind: Service metadata: diff --git a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml index 9587e52..0cf3411 100644 --- a/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml +++ b/examples/alb-tls-examples/alb-k8s/kubernetes/nginx/03-ingress.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: networking.k8s.io/v1 kind: Ingress metadata: diff --git a/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf b/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf index 58dd5c9..dfc2f09 100644 --- a/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf +++ b/examples/alb-tls-examples/alb-k8s/terraform/00-backend.tf @@ -1,8 +1,16 @@ -/*Copyright 2025 STACKIT GmbH & Co. KG - -Use of this source code is governed by an MIT-style -license that can be found in the LICENSE file or at -https://opensource.org/licenses/MIT.*/ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. terraform { backend "s3" {} diff --git a/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example b/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example index de0f400..c19f34e 100644 --- a/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example +++ b/examples/alb-tls-examples/alb-k8s/terraform/backend.conf.example @@ -1,5 +1,5 @@ -bucket = "tfstatealbworkshop" -key = "alb-k8s/terraform.tfstate" +bucket = "" +key = "" region = "eu01" endpoints = { diff --git a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md index c070b8c..1e633d2 100644 --- a/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md +++ b/examples/alb-tls-examples/vm-alb-certbot-letsencrypt/README.md @@ -8,14 +8,14 @@ Infrastructure-as-Code workshop environment demonstrating automated TLS certific This repository provisions a fully reproducible demo environment on STACKIT using Terraform. It covers the complete path from infrastructure provisioning to automated certificate management. -| Component | Description | -| ---------------------- | ------------------------------------------------------------------------------------------------------------------- | -| Resource hierarchy | Folder and project under an existing STACKIT organisation | -| Network | Private routed network (`10.10.0.0/24`) with security groups | -| Compute | Debian 12 VM with Docker Engine (provisioned via cloud-init) | -| DNS | Primary zone for ACME DNS-01 challenge validation | -| Load Balancer | Application Load Balancer with HTTP and HTTPS listeners | -| Certificate automation | [vm-alb-certbot-letsencrypt](vm-alb-certbot-letsencrypt/readme.md) — PowerShell/certbot container running on the VM | +| Component | Description | +| ---------------------- | --------------------------------------------------------------------------------------------------------- | +| Resource hierarchy | Folder and project under an existing STACKIT organisation | +| Network | Private routed network (`10.10.0.0/24`) with security groups | +| Compute | Debian 12 VM with Docker Engine (provisioned via cloud-init) | +| DNS | Primary zone for ACME DNS-01 challenge validation | +| Load Balancer | Application Load Balancer with HTTP and HTTPS listeners | +| Certificate automation | [vm-alb-certbot-letsencrypt](stackit-acme-alb/README.md) — PowerShell/certbot container running on the VM | --- diff --git a/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml b/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml index be6e579..fb07b83 100644 --- a/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml +++ b/examples/opnsense-hub-and-spoke/cloud-init/user-init-linux.yml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config # --------------------------------------------------------------------------- # Example cloud-init for Linux instances (RHEL / Debian). diff --git a/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml b/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml index 89103d4..e7bf80d 100644 --- a/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml +++ b/examples/opnsense-hub-and-spoke/cloud-init/user-init-windows.yml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config # --------------------------------------------------------------------------- # Example cloud-init for Windows Server instances. -- 2.49.1 From 6799e646edffaefb516479f4f3cf25c4524d2e09 Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Tue, 16 Jun 2026 09:41:08 +0200 Subject: [PATCH 3/4] chore: fix pre-commit run --- .github/dependabot.yaml | 14 ++++++++++++++ .github/workflows/default-ci.yaml | 14 ++++++++++++++ .github/workflows/github-mirror-ci.yaml | 14 ++++++++++++++ .../apache-debug-user.yaml | 14 ++++++++++++++ .../apache-debug-user.yaml | 14 ++++++++++++++ examples/iaas-ha-vrrp/cloud-init.yaml | 14 ++++++++++++++ examples/iaas-volume-encryption/cloud-init.yaml | 14 ++++++++++++++ .../PersistentVolumeClaim.yaml | 14 ++++++++++++++ .../example-rwx-deployment.yaml | 14 ++++++++++++++ .../stackit-sna-with-debug-machine/debug-user.yml | 14 ++++++++++++++ 10 files changed, 140 insertions(+) diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 0b4d3e3..abda7a2 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + version: 2 updates: - package-ecosystem: "github-actions" diff --git a/.github/workflows/default-ci.yaml b/.github/workflows/default-ci.yaml index 7f9f84d..e2dacc0 100644 --- a/.github/workflows/default-ci.yaml +++ b/.github/workflows/default-ci.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: "Default CI" on: diff --git a/.github/workflows/github-mirror-ci.yaml b/.github/workflows/github-mirror-ci.yaml index de61ea9..01ef428 100644 --- a/.github/workflows/github-mirror-ci.yaml +++ b/.github/workflows/github-mirror-ci.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + name: "Mirror to Public GitHub" on: diff --git a/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml b/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml index c44cda9..74c4065 100644 --- a/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml +++ b/examples/iaas-cross-az-layer4-loadbalancer/apache-debug-user.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config users: - name: debug diff --git a/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml b/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml index c44cda9..74c4065 100644 --- a/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml +++ b/examples/iaas-cross-az-layer7-loadbalancer-waf/apache-debug-user.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config users: - name: debug diff --git a/examples/iaas-ha-vrrp/cloud-init.yaml b/examples/iaas-ha-vrrp/cloud-init.yaml index 596b229..8224582 100644 --- a/examples/iaas-ha-vrrp/cloud-init.yaml +++ b/examples/iaas-ha-vrrp/cloud-init.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config # Update apt database on first boot (run 'apt-get update'). # Note, if packages are given, or package_upgrade is true, then diff --git a/examples/iaas-volume-encryption/cloud-init.yaml b/examples/iaas-volume-encryption/cloud-init.yaml index 653b5c6..8b26cf3 100644 --- a/examples/iaas-volume-encryption/cloud-init.yaml +++ b/examples/iaas-volume-encryption/cloud-init.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config users: - name: Administrator diff --git a/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml b/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml index 9247fb1..28104d8 100644 --- a/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml +++ b/examples/ske-stackit-sfs-integration/PersistentVolumeClaim.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + kind: PersistentVolumeClaim apiVersion: v1 metadata: diff --git a/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml b/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml index 08fd14d..9163dd1 100644 --- a/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml +++ b/examples/ske-stackit-sfs-integration/example-rwx-deployment.yaml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + apiVersion: v1 kind: Service metadata: diff --git a/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml b/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml index 865ffb5..90b0227 100644 --- a/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml +++ b/examples/vpn-usecases/module/stackit-sna-with-debug-machine/debug-user.yml @@ -1,3 +1,17 @@ +# Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #cloud-config # --------------------------------------------------------------------------- # Example cloud-init for Linux instances (RHEL / Debian). -- 2.49.1 From 8b2578086f716b20630c71ed8c0d73e1599a6d5a Mon Sep 17 00:00:00 2001 From: Mauritz Uphoff Date: Tue, 16 Jun 2026 09:42:05 +0200 Subject: [PATCH 4/4] chore: remove license for the example --- examples/alb-tls-examples/LICENSE | 73 ------------------------------- 1 file changed, 73 deletions(-) delete mode 100644 examples/alb-tls-examples/LICENSE diff --git a/examples/alb-tls-examples/LICENSE b/examples/alb-tls-examples/LICENSE deleted file mode 100644 index 23e2ace..0000000 --- a/examples/alb-tls-examples/LICENSE +++ /dev/null @@ -1,73 +0,0 @@ -Apache License -Version 2.0, January 2004 -http://www.apache.org/licenses/ - -TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - -1. Definitions. - -"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. - -"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. - -"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. - -"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. - -"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. - -"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. - -"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). - -"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. - -"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." - -"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. - -2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. - -3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. - -4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: - - (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and - - (b) You must cause any modified files to carry prominent notices stating that You changed the files; and - - (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and - - (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. - - You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. - -5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. - -6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. - -7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. - -8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. - -9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - -END OF TERMS AND CONDITIONS - -APPENDIX: How to apply the Apache License to your work. - -To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. - -Copyright 2026 Schwarz Digits Cloud GmbH & Co. KG - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - -http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -- 2.49.1