Initial commit
All checks were successful
CI / TruffleHog Secrets Scan (push) Successful in 4s
CI / Terraform Format & Validate (push) Successful in 5s

This commit is contained in:
Mauritz Uphoff 2025-05-15 11:02:49 +02:00
commit ea19003e9c
14 changed files with 423 additions and 0 deletions

View file

@ -0,0 +1,28 @@
name: CI
on: [push]
jobs:
secrets-scan:
name: TruffleHog Secrets Scan
runs-on: docker
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: trufflehog-actions-scan
uses: https://github.com/edplato/trufflehog-actions-scan@master
terraform:
name: Terraform Format & Validate
runs-on: docker
steps:
- name: Checkout code
uses: actions/checkout@v4
- uses: https://github.com/hashicorp/setup-terraform@v3
with:
terraform_version: "1.5.7"
- name: Format Terraform Code
run: terraform fmt -recursive -check

7
.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
.idea
.terraform
*.tfstate
terraform.tfstate
terraform.tfstate.backup
.DS_Store
keys/stackit-sa.json

83
.terraform.lock.hcl Normal file
View file

@ -0,0 +1,83 @@
# This file is maintained automatically by "terraform init".
# Manual edits may be lost in future updates.
provider "registry.terraform.io/hashicorp/helm" {
version = "2.17.0"
hashes = [
"h1:kQMkcPVvHOguOqnxoEU2sm1ND9vCHiT8TvZ2x6v/Rsw=",
"zh:06fb4e9932f0afc1904d2279e6e99353c2ddac0d765305ce90519af410706bd4",
"zh:104eccfc781fc868da3c7fec4385ad14ed183eb985c96331a1a937ac79c2d1a7",
"zh:129345c82359837bb3f0070ce4891ec232697052f7d5ccf61d43d818912cf5f3",
"zh:3956187ec239f4045975b35e8c30741f701aa494c386aaa04ebabffe7749f81c",
"zh:66a9686d92a6b3ec43de3ca3fde60ef3d89fb76259ed3313ca4eb9bb8c13b7dd",
"zh:88644260090aa621e7e8083585c468c8dd5e09a3c01a432fb05da5c4623af940",
"zh:a248f650d174a883b32c5b94f9e725f4057e623b00f171936dcdcc840fad0b3e",
"zh:aa498c1f1ab93be5c8fbf6d48af51dc6ef0f10b2ea88d67bcb9f02d1d80d3930",
"zh:bf01e0f2ec2468c53596e027d376532a2d30feb72b0b5b810334d043109ae32f",
"zh:c46fa84cc8388e5ca87eb575a534ebcf68819c5a5724142998b487cb11246654",
"zh:d0c0f15ffc115c0965cbfe5c81f18c2e114113e7a1e6829f6bfd879ce5744fbb",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
]
}
provider "registry.terraform.io/hashicorp/kubernetes" {
version = "2.36.0"
constraints = ">= 2.14.0"
hashes = [
"h1:94wlXkBzfXwyLVuJVhMdzK+VGjFnMjdmFkYhQ1RUFhI=",
"zh:07f38fcb7578984a3e2c8cf0397c880f6b3eb2a722a120a08a634a607ea495ca",
"zh:1adde61769c50dbb799d8bf8bfd5c8c504a37017dfd06c7820f82bcf44ca0d39",
"zh:39707f23ab58fd0e686967c0f973c0f5a39c14d6ccfc757f97c345fdd0cd4624",
"zh:4cc3dc2b5d06cc22d1c734f7162b0a8fdc61990ff9efb64e59412d65a7ccc92a",
"zh:8382dcb82ba7303715b5e67939e07dd1c8ecddbe01d12f39b82b2b7d7357e1d9",
"zh:88e8e4f90034186b8bfdea1b8d394621cbc46a064ff2418027e6dba6807d5227",
"zh:a6276a75ad170f76d88263fdb5f9558998bf3a3f7650d7bd3387b396410e59f3",
"zh:bc816c7e0606e5df98a0c7634b240bb0c8100c3107b8b17b554af702edc6a0c5",
"zh:cb2f31d58f37020e840af52755c18afd1f09a833c4903ac59270ab440fab57b7",
"zh:ee0d103b8d0089fb1918311683110b4492a9346f0471b136af46d3b019576b22",
"zh:f569b65999264a9416862bca5cd2a6177d94ccb0424f3a4ef424428912b9cb3c",
"zh:f688b9ec761721e401f6859c19c083e3be20a650426f4747cd359cdc079d212a",
]
}
provider "registry.terraform.io/hashicorp/random" {
version = "3.7.2"
hashes = [
"h1:KG4NuIBl1mRWU0KD/BGfCi1YN/j3F7H4YgeeM7iSdNs=",
"zh:14829603a32e4bc4d05062f059e545a91e27ff033756b48afbae6b3c835f508f",
"zh:1527fb07d9fea400d70e9e6eb4a2b918d5060d604749b6f1c361518e7da546dc",
"zh:1e86bcd7ebec85ba336b423ba1db046aeaa3c0e5f921039b3f1a6fc2f978feab",
"zh:24536dec8bde66753f4b4030b8f3ef43c196d69cccbea1c382d01b222478c7a3",
"zh:29f1786486759fad9b0ce4fdfbbfece9343ad47cd50119045075e05afe49d212",
"zh:4d701e978c2dd8604ba1ce962b047607701e65c078cb22e97171513e9e57491f",
"zh:78d5eefdd9e494defcb3c68d282b8f96630502cac21d1ea161f53cfe9bb483b3",
"zh:7b8434212eef0f8c83f5a90c6d76feaf850f6502b61b53c329e85b3b281cba34",
"zh:ac8a23c212258b7976e1621275e3af7099e7e4a3d4478cf8d5d2a27f3bc3e967",
"zh:b516ca74431f3df4c6cf90ddcdb4042c626e026317a33c53f0b445a3d93b720d",
"zh:dc76e4326aec2490c1600d6871a95e78f9050f9ce427c71707ea412a2f2f1a62",
"zh:eac7b63e86c749c7d48f527671c7aee5b4e26c10be6ad7232d6860167f99dbb0",
]
}
provider "registry.terraform.io/stackitcloud/stackit" {
version = "0.51.0"
constraints = "0.51.0"
hashes = [
"h1:xEq9g1RuX4WIA3KNHyYgnBfWwY7mNn3yQGlFn/WbX9o=",
"zh:0dde99e7b343fa01f8eefc378171fb8621bedb20f59157d6cc8e3d46c738105f",
"zh:2d920411cbf4976d0fdf0d0d3f0403929b0c1e21ca3c3aa09031651b8bd1f614",
"zh:4067b3b21b50d55485edda9da11fd0df4188a05e02c3bc9332ad24f6a5c977e8",
"zh:46e25c4058e5ff5b321e2f3c23521acba4722b626cb427ed17f970515e3efb8c",
"zh:4bbe1904333a5dc00584915b94271e8482ca06586de4f345d3b1f019057d5dff",
"zh:7480e6d7deb519fc15f28156fe09b201f73c0654944526ac3da3510c902feaa9",
"zh:7868dd37c44423a9bb5b5fbd883171d2c36fd4507f7545de355caffbf20c2b99",
"zh:a5b47602355cb1611673fcb26f972529f5d6fe0e11a47b1b916e610ecb10fc86",
"zh:aefa5b9a0eefecac09e10c8c4d0ff2109b9a9eabd163fa34b48116f7a205b66f",
"zh:c6a5b0737198add8bed3affc072b09ee87f928c4c25437fe1a5871b6d454c879",
"zh:d07dcbc92463ebe2f28f9125b74e362ccf32f4d5bc71ee339c6d28dce7229a8d",
"zh:d63dd003fbe2752694d70e1accce9bb8d118e6332ac7a08a844109b93c98e8f2",
"zh:f32ebcb26ae57d7c7480e07b5a42eec57ee410097072c216485ceca3254a98f2",
"zh:f3b78e090125cb02136677cc7faf5a3a317a620132c0211bfd6fe63e66de2807",
"zh:f5db68035985aba0b6a8b66da7b653dbb02b4dcc8f0f8efc6ff243b7bad16d99",
]
}

1
00-backend.tf Normal file
View file

@ -0,0 +1 @@
terraform {}

15
01-variables.tf Normal file
View file

@ -0,0 +1,15 @@
variable "stackit_project_id" {
type = string
/*default = "XXXXX-XXXX-XXXX-XXXX-XXXXXXX"*/
default = "d75e6aab-b616-4b42-ae3b-aaf161ad626d"
}
variable "stackit_region" {
type = string
default = "eu01"
}
variable "stackit_service_account_key_path" {
type = string
default = "./keys/stackit-sa.json"
}

33
02-provider.tf Normal file
View file

@ -0,0 +1,33 @@
terraform {
required_providers {
stackit = {
source = "stackitcloud/stackit"
version = "0.51.0"
}
kubernetes = {
source = "hashicorp/kubernetes"
version = ">=2.14.0"
}
}
}
provider "stackit" {
default_region = var.stackit_region
service_account_key_path = var.stackit_service_account_key_path
}
provider "kubernetes" {
host = yamldecode(stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.server
client_certificate = base64decode(yamldecode(stackit_ske_kubeconfig.example.kube_config).users.0.user.client-certificate-data)
client_key = base64decode(yamldecode(stackit_ske_kubeconfig.example.kube_config).users.0.user.client-key-data)
cluster_ca_certificate = base64decode(yamldecode(stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.certificate-authority-data)
}
provider "helm" {
kubernetes {
host = yamldecode(stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.server
client_certificate = base64decode(yamldecode(stackit_ske_kubeconfig.example.kube_config).users.0.user.client-certificate-data)
client_key = base64decode(yamldecode(stackit_ske_kubeconfig.example.kube_config).users.0.user.client-key-data)
cluster_ca_certificate = base64decode(yamldecode(stackit_ske_kubeconfig.example.kube_config).clusters.0.cluster.certificate-authority-data)
}
}

38
03-ske.tf Normal file
View file

@ -0,0 +1,38 @@
resource "random_string" "random" {
length = 4
lower = true
upper = false
special = false
}
resource "stackit_ske_cluster" "default" {
project_id = var.stackit_project_id
name = "ske-${random_string.random.result}"
kubernetes_version_min = "1.31"
node_pools = [
{
name = "standard"
machine_type = "c1.4"
minimum = "1"
maximum = "3"
max_surge = "3"
availability_zones = ["eu01-1", "eu01-2", "eu01-3"]
os_version_min = "4152.2.1"
os_name = "flatcar"
volume_size = 32
volume_type = "storage_premium_perf6"
}
]
maintenance = {
enable_kubernetes_version_updates = true
enable_machine_image_version_updates = true
start = "01:00:00Z"
end = "02:00:00Z"
}
}
resource "stackit_ske_kubeconfig" "example" {
project_id = var.stackit_project_id
cluster_name = stackit_ske_cluster.default.name
refresh = true
}

5
04-k8s-ns.tf Normal file
View file

@ -0,0 +1,5 @@
resource "kubernetes_namespace" "nginx" {
metadata {
name = "nginx"
}
}

18
05-dns.tf Normal file
View file

@ -0,0 +1,18 @@
resource "stackit_dns_zone" "svc_zone" {
project_id = var.stackit_project_id
name = random_string.random.result
dns_name = "${random_string.random.result}.runs.onstackit.cloud"
type = "primary"
default_ttl = 60
contact_email = "hostmaster@stackit.cloud"
}
resource "stackit_dns_record_set" "svc" {
project_id = var.stackit_project_id
zone_id = stackit_dns_zone.svc_zone.zone_id
name = "nginx"
type = "A"
ttl = 60
comment = "a record"
records = [stackit_public_ip.public_ip.ip]
}

7
06-public-ip.tf Normal file
View file

@ -0,0 +1,7 @@
resource "stackit_public_ip" "public_ip" {
project_id = var.stackit_project_id
lifecycle {
ignore_changes = [network_interface_id]
}
}

40
07-helm.tf Normal file
View file

@ -0,0 +1,40 @@
resource "helm_release" "ingress_nginx" {
name = "ingress-nginx"
repository = "https://kubernetes.github.io/ingress-nginx"
chart = "ingress-nginx"
version = "4.12.2"
namespace = kubernetes_namespace.nginx.metadata[0].name
values = [
<<EOF
controller:
config:
use-proxy-protocol: "true"
allow-snippet-annotations: "true"
compute-full-forwarded-for: "true"
use-proxy-protocol: "true"
use-forwarded-headers: "true"
enable-real-ip: "true"
forwarded-for-header: "proxy_protocol"
proxy-connect-timeout: "10"
proxy-next-upstream: "error timeout http_502 http_503 http_504"
proxy-next-upstream-timeout: "10"
proxy-next-upstream-tries: "5"
retry-non-idempotent: "true"
proxy-body-size: "5M"
client-body-buffer-size: "128K"
replicaCount: 1
service:
type: LoadBalancer
externalTrafficPolicy: Local
ipFamilyPolicy: SingleStack
ipFamilies:
- IPv4
annotations:
lb.stackit.cloud/external-address: ${stackit_public_ip.public_ip.ip}
lb.stackit.cloud/tcp-proxy-protocol: "true"
EOF
]
timeout = 600
}

77
08-rand-service.tf Normal file
View file

@ -0,0 +1,77 @@
resource "random_pet" "suffix" {}
resource "kubernetes_deployment_v1" "random_nginx" {
metadata {
name = "nginx-${random_pet.suffix.id}"
}
spec {
replicas = 1
selector {
match_labels = {
app = "nginx-${random_pet.suffix.id}"
}
}
template {
metadata {
labels = {
app = "nginx-${random_pet.suffix.id}"
}
}
spec {
container {
name = "nginx"
image = "nginx:alpine"
port {
container_port = 80
}
}
}
}
}
}
resource "kubernetes_service" "nginx" {
metadata {
name = "nginx-${random_pet.suffix.id}"
}
spec {
selector = {
app = "nginx-${random_pet.suffix.id}"
}
port {
port = 80
target_port = 80
}
type = "ClusterIP"
}
}
resource "kubernetes_ingress_v1" "nginx" {
metadata {
name = "nginx-${random_pet.suffix.id}"
annotations = {
"kubernetes.io/ingress.class" = "nginx"
"nginx.ingress.kubernetes.io/limit-rps" : "10"
}
}
spec {
rule {
host = "${stackit_dns_record_set.svc.name}.${stackit_dns_zone.svc_zone.dns_name}"
http {
path {
path = "/"
path_type = "Prefix"
backend {
service {
name = kubernetes_service.nginx.metadata[0].name
port {
number = 80
}
}
}
}
}
}
}
}

3
09-outputs.tf Normal file
View file

@ -0,0 +1,3 @@
output "url" {
value = "${stackit_dns_record_set.svc.name}.${stackit_dns_zone.svc_zone.dns_name}"
}

68
README.md Normal file
View file

@ -0,0 +1,68 @@
# How to Forward the Real Client IP to the Ingress Controller
When your application is accessed through a Load Balancer, the original client IP may not be visible to your pods unless the **TCP Proxy Protocol** is enabled and properly configured.
STACKIT supports Proxy Protocol version 2. Please follow the steps below to ensure your application and ingress-nginx controller can correctly receive and log the original client IP.
**Reference:**
Official STACKIT documentation: [Load Balancer SKE - TCP Proxy Protocol](https://docs.stackit.cloud/stackit/en/load-balancer-ske-28476594.html#LoadBalancerSKE-TCPProxyProtocol)
---
## 1. Configure the Service for Proxy Protocol
The Kubernetes `Service` of type `LoadBalancer` must be annotated to enable Proxy Protocol support from STACKIT.
**Example:**
```yaml
apiVersion: v1
kind: Service
metadata:
name: my-proxy-service
annotations:
lb.stackit.cloud/tcp-proxy-protocol: "true"
spec:
selector:
app: MyApp
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer
```
**Note:**
Make sure the application behind the Load Balancer is compatible with Proxy Protocol v2. If not, connections might fail or behave unexpectedly.
---
## 2. Enable Proxy Protocol on ingress-nginx
Your ingress controller must also be configured to accept Proxy Protocol headers.
This may require special Helm values when deploying ingress-nginx.
- See how this is done in [07-helm.tf](07-helm.tf) for this repository.
- In general, for ingress-nginx Helm charts, you will need:
- `controller.config.use-proxy-protocol: "true"`
- Any additional Service annotations as required by your cloud provider and use case.
---
## 3. Testing
To load test your endpoint and validate real client IP handling, you can use [`fortio`](https://fortio.org/):
```sh
fortio load --qps 50 -t 10s <url from outputs.tf>
```
Replace `<url from outputs.tf>` with the Load Balancer or Ingress endpoint output by your Terraform configuration.
---
## Important
- Proxy Protocol must be enabled on **both** the Load Balancer service **and** the ingress/nginx controller.
- If either is not configured, the original client IP will not be visible within your pods, and may result in errors.
- Always review the official documentation for your platform and ingress-nginx version.
---