diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b52e709 --- /dev/null +++ b/.gitignore @@ -0,0 +1,26 @@ +.next +.vscode +node_modules +**.DS_Store +bin/ +obj/ +db/ +database +.env.local +.idea/ +tsconfig.tsbuildinfo +.vs/ +.react-email +google_oauth2_client_secrets.env +dist +# Python development +.python-version +/test-results/ +/playwright-report/ +/blob-report/ +/playwright/.cache/ + + +.env +1password-credentials.json +hosts \ No newline at end of file diff --git a/base/artifactShipper/kustomization.yaml b/base/artifactShipper/kustomization.yaml index 826be91..925306a 100644 --- a/base/artifactShipper/kustomization.yaml +++ b/base/artifactShipper/kustomization.yaml @@ -2,5 +2,4 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - github-token-external-secret.yaml - job.yaml \ No newline at end of file diff --git a/base/authority/httproute.yml b/base/authority/httproute.yml new file mode 100644 index 0000000..d1d398b --- /dev/null +++ b/base/authority/httproute.yml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: authority +spec: + parentRefs: + - name: processcube-gateway + sectionName: websecure-authority + hostnames: + - authority.dev.5minds.cloud + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: authority + port: 11560 diff --git a/base/authority/ingress.yml b/base/authority/ingress.yml deleted file mode 100644 index 0f76e20..0000000 --- a/base/authority/ingress.yml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: authority - annotations: - cert-manager.io/cluster-issuer: letsencrypt-production -spec: - ingressClassName: nginx - tls: - - hosts: - - authority.dev.5minds.cloud - secretName: authority-ingress-tls - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: authority - port: - number: 11560 \ No newline at end of file diff --git a/base/authority/kustomization.yaml b/base/authority/kustomization.yaml index ce0da50..d229f00 100644 --- a/base/authority/kustomization.yaml +++ b/base/authority/kustomization.yaml @@ -2,7 +2,7 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - ingress.yml + - httproute.yml - service.yml - deployment.yml diff --git a/base/engine/httproute.yml b/base/engine/httproute.yml new file mode 100644 index 0000000..ce46fc9 --- /dev/null +++ b/base/engine/httproute.yml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: engine +spec: + parentRefs: + - name: processcube-gateway + sectionName: websecure-engine + hostnames: + - engine.dev.5minds.cloud + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: engine + port: 10560 diff --git a/base/engine/ingress.yml b/base/engine/ingress.yml deleted file mode 100644 index da43dae..0000000 --- a/base/engine/ingress.yml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: engine - annotations: - cert-manager.io/cluster-issuer: letsencrypt-production -spec: - ingressClassName: nginx - tls: - - hosts: - - engine.dev.5minds.cloud - secretName: engine-ingress-tls - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: engine - port: - number: 10560 \ No newline at end of file diff --git a/base/engine/kustomization.yaml b/base/engine/kustomization.yaml index d324083..d9a5f31 100644 --- a/base/engine/kustomization.yaml +++ b/base/engine/kustomization.yaml @@ -2,6 +2,6 @@ apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: - - ingress.yml + - httproute.yml - service.yml - deployment.yml \ No newline at end of file diff --git a/base/gateway.yml b/base/gateway.yml new file mode 100644 index 0000000..80fd286 --- /dev/null +++ b/base/gateway.yml @@ -0,0 +1,48 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: Gateway +metadata: + name: processcube-gateway + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production +spec: + gatewayClassName: traefik + listeners: + - name: web + port: 80 + protocol: HTTP + allowedRoutes: + namespaces: + from: Same + - name: websecure-engine + port: 443 + protocol: HTTPS + hostname: engine.dev.5minds.cloud + tls: + mode: Terminate + certificateRefs: + - name: engine-tls + allowedRoutes: + namespaces: + from: Same + - name: websecure-authority + port: 443 + protocol: HTTPS + hostname: authority.dev.5minds.cloud + tls: + mode: Terminate + certificateRefs: + - name: authority-tls + allowedRoutes: + namespaces: + from: Same + - name: websecure-nodered + port: 443 + protocol: HTTPS + hostname: nodered.dev.5minds.cloud + tls: + mode: Terminate + certificateRefs: + - name: nodered-tls + allowedRoutes: + namespaces: + from: Same diff --git a/base/http-redirect.yml b/base/http-redirect.yml new file mode 100644 index 0000000..40581e0 --- /dev/null +++ b/base/http-redirect.yml @@ -0,0 +1,14 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: http-to-https-redirect +spec: + parentRefs: + - name: processcube-gateway + sectionName: web + rules: + - filters: + - type: RequestRedirect + requestRedirect: + scheme: https + statusCode: 301 diff --git a/base/kustomization.yaml b/base/kustomization.yaml index 55bf731..66d15d0 100644 --- a/base/kustomization.yaml +++ b/base/kustomization.yaml @@ -6,4 +6,6 @@ resources: - authority/ - engine/ - lowcode/ - - postgres/ \ No newline at end of file + - postgres/ + - gateway.yml + - http-redirect.yml \ No newline at end of file diff --git a/base/lowcode/httproute.yaml b/base/lowcode/httproute.yaml new file mode 100644 index 0000000..423a1ac --- /dev/null +++ b/base/lowcode/httproute.yaml @@ -0,0 +1,18 @@ +apiVersion: gateway.networking.k8s.io/v1 +kind: HTTPRoute +metadata: + name: nodered +spec: + parentRefs: + - name: processcube-gateway + sectionName: websecure-nodered + hostnames: + - nodered.dev.5minds.cloud + rules: + - matches: + - path: + type: PathPrefix + value: / + backendRefs: + - name: nodered + port: 30000 diff --git a/base/lowcode/ingress.yaml b/base/lowcode/ingress.yaml deleted file mode 100644 index cdad3f4..0000000 --- a/base/lowcode/ingress.yaml +++ /dev/null @@ -1,22 +0,0 @@ -apiVersion: networking.k8s.io/v1 -kind: Ingress -metadata: - name: nodered - annotations: - cert-manager.io/cluster-issuer: letsencrypt-production -spec: - ingressClassName: nginx - tls: - - hosts: - - nodered.dev.5minds.cloud - secretName: nodered-ingress-tls - rules: - - http: - paths: - - path: / - pathType: Prefix - backend: - service: - name: nodered - port: - number: 30000 diff --git a/base/lowcode/kustomization.yaml b/base/lowcode/kustomization.yaml index eb1b58a..71d6e0b 100644 --- a/base/lowcode/kustomization.yaml +++ b/base/lowcode/kustomization.yaml @@ -1,6 +1,6 @@ resources: - deployment.yaml -- ingress.yaml +- httproute.yaml - service.yaml - pvc.yaml diff --git a/hetzner-setup/ProcessCube.Cloud/.gitignore b/hetzner-setup/ProcessCube.Cloud/.gitignore new file mode 100644 index 0000000..ec24f27 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/.gitignore @@ -0,0 +1,33 @@ +# Terraform files +*.tfstate +*.tfstate.* +.terraform/ +.terraform.lock.hcl +crash.log +crash.*.log +override.tf +override.tf.json +*_override.tf +*_override.tf.json + +# Sensitive files +terraform.tfvars +*.tfvars +.terraformrc +terraform.rc + +# Kubeconfig +kubeconfig.yaml +*.kubeconfig + +# SSH keys +*.pem +*.key +id_rsa* + +# OS files +.DS_Store +Thumbs.db + +# Generated files +ansible/inventory/hosts diff --git a/hetzner-setup/ProcessCube.Cloud/README.md b/hetzner-setup/ProcessCube.Cloud/README.md new file mode 100644 index 0000000..ff0f7b3 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/README.md @@ -0,0 +1,612 @@ +# ProcessCube K3s Cluster on Hetzner Cloud + +Terraform + Ansible configuration for deploying a production-ready K3s Kubernetes cluster on Hetzner Cloud. + +## Architecture + +- **1 Master Node**: K3s server with control plane +- **2 Worker Nodes**: K3s agents for workload execution (scalable) +- **Hetzner Cloud Controller Manager**: Native cloud integration for LoadBalancers and persistent volumes +- **Hetzner CSI Driver**: Dynamic volume provisioning +- **Nginx Ingress Controller**: DaemonSet configuration for high availability +- **cert-manager**: Automatic TLS certificate management with Let's Encrypt +- **Tailscale**: Secure mesh VPN for remote access to cluster nodes +- **Private Network**: Internal 10.0.0.0/16 network for cluster communication +- **Firewall**: Configured security rules for SSH, K8s API, HTTP/HTTPS + +## Prerequisites + +1. **Hetzner Cloud Account**: Sign up at https://www.hetzner.com/cloud +2. **Hetzner API Token**: Create one in the Hetzner Cloud Console under "Security" → "API Tokens" +3. **Terraform**: Install from https://www.terraform.io/downloads + ```bash + # macOS + brew install terraform + + # Linux + wget https://releases.hashicorp.com/terraform/1.6.0/terraform_1.6.0_linux_amd64.zip + unzip terraform_1.6.0_linux_amd64.zip + sudo mv terraform /usr/local/bin/ + ``` +4. **Ansible**: Install from https://docs.ansible.com/ansible/latest/installation_guide/intro_installation.html + ```bash + # macOS + brew install ansible + + # Linux (Ubuntu/Debian) + sudo apt update + sudo apt install ansible + + # Python pip (all platforms) + pip3 install ansible + # or use the requirements file: + cd ansible && pip3 install -r requirements.txt + ``` +5. **SSH Key**: Generate if you don't have one: + ```bash + ssh-keygen -t rsa -b 4096 -f ~/.ssh/id_rsa + ``` +6. **Tailscale Account** (optional): Sign up at https://tailscale.com for secure remote access + +## Configuration + +### 1. Create terraform.tfvars + +Create a `terraform.tfvars` file in this directory: + +```hcl +# Hetzner Cloud Configuration +hcloud_token = "YOUR_HETZNER_API_TOKEN" + +# Cluster Configuration +cluster_name = "processcube-k3s" +location = "fsn1" # Options: nbg1, fsn1, hel1 +server_type = "cx43" # Options: cx11, cx21, cx31, cx41, cx51 +worker_count = 2 + +# K3s Version +k3s_version = "v1.34.2+k3s1" + +# Hetzner Cloud Integrations +hcloud_csi_version = "v2.18.1" # CSI Driver for persistent volumes +hcloud_ccm_version = "v1.20.0" # Cloud Controller Manager + +# SSH Key Paths +ssh_public_key_path = "~/.ssh/id_rsa.pub" +ssh_private_key_path = "~/.ssh/id_rsa" + +# Let's Encrypt (for automatic HTTPS certificates) +letsencrypt_email = "your-email@example.com" + +# Tailscale (optional - for secure remote access) +tailscale_auth_key = "YOUR_TAILSCALE_AUTH_KEY" # See "Tailscale Setup" below +# tailscale_tags = "tag:k3s" # Optional: Uncomment to use tags +``` + +### 2. Tailscale Setup (Optional but Recommended) + +Tailscale provides secure remote access to your cluster nodes without exposing them to the public internet. + +**Create an Auth Key:** + +1. Go to https://login.tailscale.com/admin/settings/keys +2. Click **Generate auth key** +3. Configure the key: + - **Description**: `ProcessCube K3s Cluster` + - **Reusable**: ✅ Enable (allows multiple devices to use the same key) + - **Ephemeral**: ❌ Disable (nodes should persist in your network) + - **Pre-approved**: ✅ Enable (automatically approve devices) + - **Tags**: Add `tag:k3s` if you want to use ACL rules for this cluster +4. Click **Generate key** +5. Copy the key (starts with `tskey-auth-...`) +6. Add to your `terraform.tfvars`: + ```hcl + tailscale_auth_key = "tskey-auth-kXXXXXXXXXXXXXXXXXXXXXXXXX" + ``` + +**Benefits:** +- Secure SSH access from anywhere without VPN configuration +- Access cluster services via Tailscale IPs +- No need to expose SSH on public IPs +- Automatic encryption and authentication + +**Skip Tailscale:** If you don't want to use Tailscale, you can skip this step and access nodes via their public IPs. + +### 3. Server Types + +Choose your server type based on workload requirements: + +| Type | vCPUs | RAM | Price/month* | +|------|-------|-----|--------------| +| cx11 | 1 | 2GB | ~€4.15 | +| cx21 | 2 | 4GB | ~€6.40 | +| cx31 | 2 | 8GB | ~€12.40 | +| cx41 | 4 | 16GB | ~€23.40 | +| cx51 | 8 | 32GB | ~€44.40 | + +*Prices are approximate. Check current pricing at https://www.hetzner.com/cloud + +### 4. Locations + +- `nbg1` - Nuremberg, Germany +- `fsn1` - Falkenstein, Germany +- `hel1` - Helsinki, Finland + +## Deployment + +The deployment process uses Terraform to provision infrastructure and Ansible to configure K3s. + +### Initialize Terraform + +```bash +cd infrastructure/ProcessCube.Cloud +terraform init +``` + +### Plan Deployment + +```bash +terraform plan +``` + +### Deploy Cluster + +```bash +terraform apply +``` + +Type `yes` when prompted to confirm. + +**What happens during deployment:** +1. Terraform creates Hetzner Cloud resources (servers, network, firewall) +2. Terraform generates Ansible inventory with all configuration +3. Ansible installs Tailscale on all nodes (if configured) +4. Ansible installs and configures K3s master node +5. Ansible installs Hetzner Cloud Controller Manager (CCM) +6. Ansible joins worker nodes to the cluster +7. Ansible installs cluster addons: + - Hetzner CSI Driver (for persistent volumes) + - Nginx Ingress Controller (DaemonSet on all nodes) + - cert-manager (for automatic TLS certificates) + - Hetzner LoadBalancer (automatically created by CCM) +8. Ansible verifies all nodes are ready + +Deployment takes approximately 10-15 minutes. + +## Manual Ansible Execution + +If you need to re-run Ansible without recreating infrastructure: + +```bash +cd ansible +ansible-playbook -i inventory/hosts site.yml +``` + +Check connectivity first: +```bash +ansible all -i inventory/hosts -m ping +``` + +## Access the Cluster + +### Get Cluster Information + +```bash +terraform output +``` + +### Download kubeconfig + +```bash +terraform output -raw kubeconfig_command | bash +``` + +Or manually: + +```bash +ssh root@$(terraform output -raw master_ip) 'cat /etc/rancher/k3s/k3s.yaml' | \ + sed "s/127.0.0.1/$(terraform output -raw master_ip)/g" > kubeconfig.yaml +``` + +### Use kubectl + +```bash +export KUBECONFIG=./kubeconfig.yaml +kubectl get nodes +kubectl get pods -A +``` + +Expected output: +``` +NAME STATUS ROLES AGE VERSION +processcube-k3s-master Ready control-plane,master 5m v1.28.5+k3s1 +processcube-k3s-worker-1 Ready 4m v1.28.5+k3s1 +processcube-k3s-worker-2 Ready 4m v1.28.5+k3s1 +``` + +### SSH to Nodes + +```bash +# Master node +ssh root@$(terraform output -raw master_ip) + +# Worker nodes +ssh root@ +``` + +## Cluster Features + +### What's Included + +- ✅ **K3s v1.34.2+k3s1** - Lightweight Kubernetes distribution +- ✅ **Hetzner Cloud Controller Manager** - Native cloud integration +- ✅ **Hetzner CSI Driver** - Dynamic persistent volume provisioning +- ✅ **Nginx Ingress Controller** - DaemonSet configuration for HA +- ✅ **cert-manager** - Automatic TLS certificates with Let's Encrypt +- ✅ **Hetzner LoadBalancer** - Automatically provisioned for Ingress +- ✅ **Tailscale VPN** - Secure mesh networking (optional) +- ✅ **Private networking** - 10.0.0.0/16 internal network +- ✅ **Firewall configuration** - UFW rules on all nodes +- ✅ **Helm 3** - Installed on master node +- ✅ **Idempotent Ansible playbooks** - Safe to re-run + +### What's Disabled + +- ❌ **Traefik ingress controller** - Using Nginx instead +- ❌ **ServiceLB** - Using Hetzner LoadBalancer via CCM +- ❌ **K3s cloud provider** - Using external cloud-provider via CCM + +## Ansible Structure + +``` +ansible/ +├── ansible.cfg # Ansible configuration +├── site.yml # Main playbook orchestration +├── requirements.txt # Python dependencies +├── inventory/ +│ ├── hosts.tpl # Terraform template for inventory +│ └── hosts # Generated inventory (by Terraform) +└── roles/ + ├── tailscale/ # Tailscale VPN installation + │ └── tasks/main.yml + ├── k3s_master/ # K3s master node setup + │ └── tasks/main.yml + ├── k3s_ccm/ # Hetzner Cloud Controller Manager + │ └── tasks/main.yml + ├── k3s_worker/ # K3s worker node setup + │ └── tasks/main.yml + └── k3s_addons/ # Cluster addons (CSI, Ingress, cert-manager) + └── tasks/main.yml +``` + +## Customizing Ansible Playbooks + +### Modify K3s Installation + +Edit [ansible/roles/k3s_master/tasks/main.yml](ansible/roles/k3s_master/tasks/main.yml) or [ansible/roles/k3s_worker/tasks/main.yml](ansible/roles/k3s_worker/tasks/main.yml) + +### Add Additional Software + +Create new Ansible roles: + +```bash +cd ansible/roles +ansible-galaxy init my_custom_role +``` + +Then add it to `site.yml`: + +```yaml +- name: Install custom software + hosts: all + roles: + - my_custom_role +``` + +## Working with the Cluster + +### Using Tailscale for Remote Access + +If you configured Tailscale, your nodes are accessible via their Tailscale IPs: + +```bash +# View Tailscale machines +tailscale status + +# SSH via Tailscale +ssh root@ + +# You can also set up Tailscale on your local machine to access the cluster +``` + +### LoadBalancer and Ingress + +The Hetzner LoadBalancer is automatically created and configured: + +```bash +# Get LoadBalancer IP +kubectl get svc ingress-nginx-controller -n ingress-nginx + +# The LoadBalancer distributes traffic to all nodes (port 80/443) +``` + +### Deploy Your Applications with Ingress + +Example Ingress with automatic TLS: + +```yaml +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: my-app + annotations: + cert-manager.io/cluster-issuer: letsencrypt-production +spec: + ingressClassName: nginx + tls: + - hosts: + - myapp.example.com + secretName: myapp-tls + rules: + - host: myapp.example.com + http: + paths: + - path: / + pathType: Prefix + backend: + service: + name: my-app + port: + number: 80 +``` + +Apply it: +```bash +kubectl apply -f my-app-ingress.yaml +``` + +cert-manager will automatically request and configure TLS certificates from Let's Encrypt. + +## Monitoring + +### View Cluster Status + +```bash +kubectl get nodes +kubectl top nodes +kubectl get pods -A +``` + +### Check K3s Service + +```bash +# On master +ssh root@$(terraform output -raw master_ip) +systemctl status k3s +journalctl -u k3s -f + +# On workers +ssh root@ +systemctl status k3s-agent +journalctl -u k3s-agent -f +``` + +### Ansible Logs + +Check Ansible output during `terraform apply` for any errors. + +## Cost Estimation + +**Monthly costs** (approximate): +- 1x Master (cx21): €6.40 +- 2x Workers (cx21): €12.80 +- 1x Load Balancer (lb11): €5.39 +- **Total: ~€24.59/month** + +Additional costs: +- Traffic: 20TB included (€1.19/TB after) +- Volumes: €0.0476/GB/month (if used) + +## Scaling + +### Add More Workers + +Edit `terraform.tfvars`: + +```hcl +worker_count = 3 # or more +``` + +Then apply: + +```bash +terraform apply +``` + +Terraform will: +1. Create new worker servers +2. Update Ansible inventory +3. Run Ansible to join new workers to cluster + +### Upgrade Server Type + +Edit `terraform.tfvars`: + +```hcl +server_type = "cx31" # upgrade from cx21 +``` + +Note: This will recreate the servers. Backup your data first! + +## Backup & Disaster Recovery + +### Backup etcd + +```bash +ssh root@$(terraform output -raw master_ip) +k3s etcd-snapshot save +``` + +Snapshots are stored in `/var/lib/rancher/k3s/server/db/snapshots/` + +### Download Backups + +```bash +scp root@$(terraform output -raw master_ip):/var/lib/rancher/k3s/server/db/snapshots/* ./backups/ +``` + +### Restore from Snapshot + +```bash +k3s server \ + --cluster-reset \ + --cluster-reset-restore-path=/var/lib/rancher/k3s/server/db/snapshots/snapshot-name +``` + +## Troubleshooting + +### Terraform Issues + +**Problem**: `Error: Error creating server` +- Check Hetzner API token is valid +- Verify server type is available in chosen location +- Check account limits in Hetzner Console + +### Ansible Issues + +**Problem**: Ansible cannot connect to servers +```bash +# Test SSH connectivity +ssh root@ + +# Check Ansible inventory +cat ansible/inventory/hosts + +# Test with Ansible ping +cd ansible +ansible all -i inventory/hosts -m ping +``` + +**Problem**: K3s installation fails +```bash +# SSH to the server and check logs +ssh root@ +journalctl -xeu k3s +# or for workers: +journalctl -xeu k3s-agent +``` + +### Nodes Not Joining + +1. Check master node is running: + ```bash + ssh root@$(terraform output -raw master_ip) 'systemctl status k3s' + ``` + +2. Check worker logs: + ```bash + ssh root@ 'journalctl -u k3s-agent -f' + ``` + +3. Verify network connectivity: + ```bash + ssh root@ 'ping -c 3 10.0.1.2' + ``` + +4. Re-run Ansible: + ```bash + cd ansible + ansible-playbook -i inventory/hosts site.yml + ``` + +### Get K3s Token + +```bash +ssh root@$(terraform output -raw master_ip) 'cat /var/lib/rancher/k3s/server/node-token' +``` + +Or use terraform: + +```bash +terraform output -raw k3s_token +``` + +### Firewall Issues + +Check UFW status: + +```bash +ssh root@$(terraform output -raw master_ip) 'ufw status' +``` + +## Cleanup + +### Destroy Cluster + +```bash +terraform destroy +``` + +Type `yes` to confirm. + +This will delete: +- All servers +- Load balancer +- Network +- Firewall +- SSH keys +- Generated Ansible inventory + +**Warning**: This action is irreversible. Backup any important data first! + +### Clean Local Files + +```bash +rm -f ansible/inventory/hosts +rm -f kubeconfig.yaml +``` + +## Security Considerations + +1. **SSH Access**: Consider restricting SSH access to specific IPs in the firewall rules +2. **API Access**: The Kubernetes API is publicly accessible. Use RBAC and network policies. +3. **Secrets**: Never commit `terraform.tfvars` or `*.tfstate` files to git +4. **API Token**: Keep your Hetzner API token secure +5. **Updates**: Regularly update K3s version for security patches +6. **Ansible**: SSH private key is used by Ansible - ensure it's properly secured + +## Advanced Configuration + +### Custom K3s Flags + +Edit the Ansible role to add custom flags: + +```yaml +# In ansible/roles/k3s_master/tasks/main.yml +- name: Install K3s master + shell: | + INSTALL_K3S_VERSION="{{ k3s_version }}" /tmp/k3s_install.sh server \ + --cluster-init \ + --your-custom-flag \ + --another-flag=value +``` + +### Configure Node Labels + +Add to Ansible playbook: + +```yaml +- name: Label nodes + shell: kubectl label node {{ inventory_hostname }} custom-label=value +``` + +## Support + +- **Hetzner Cloud Docs**: https://docs.hetzner.com/cloud/ +- **K3s Documentation**: https://docs.k3s.io/ +- **Terraform Hetzner Provider**: https://registry.terraform.io/providers/hetznercloud/hcloud/ +- **Ansible Documentation**: https://docs.ansible.com/ + +## License + +This configuration is part of ProcessCube.UG project. diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/ansible.cfg b/hetzner-setup/ProcessCube.Cloud/ansible/ansible.cfg new file mode 100644 index 0000000..67fd705 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/ansible.cfg @@ -0,0 +1,13 @@ +[defaults] +inventory = inventory/hosts +host_key_checking = False +retry_files_enabled = False +roles_path = roles +interpreter_python = auto_silent +forks = 1 +timeout = 60 + +[ssh_connection] +ssh_args = -o ControlMaster=auto -o ControlPersist=300s -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ServerAliveInterval=30 -o ServerAliveCountMax=10 -o ConnectTimeout=60 +pipelining = True +control_path = /tmp/ansible-ssh-%%h-%%p-%%r diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/inventory/hosts.tpl b/hetzner-setup/ProcessCube.Cloud/ansible/inventory/hosts.tpl new file mode 100644 index 0000000..b9b4ec0 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/inventory/hosts.tpl @@ -0,0 +1,31 @@ +[k3s_master] +${master_public_ip} ansible_user=root ansible_ssh_private_key_file=${ssh_private_key_path} + +[k3s_workers] +%{ for ip in worker_public_ips ~} +${ip} ansible_user=root ansible_ssh_private_key_file=${ssh_private_key_path} +%{ endfor ~} + +[k3s_cluster:children] +k3s_master +k3s_workers + +[k3s_cluster:vars] +k3s_token=${k3s_token} +master_ip=${master_private_ip} +cluster_name=${cluster_name} +hcloud_token=${hcloud_token} +hcloud_csi_version=${hcloud_csi_version} +hcloud_ccm_version=${hcloud_ccm_version} +network_id=${network_id} +location=${location} +letsencrypt_email=${letsencrypt_email} +%{ if tailscale_auth_key != "" ~} +tailscale_auth_key=${tailscale_auth_key} +tailscale_tags=${tailscale_tags} +%{ endif ~} +%{ if onepassword_credentials_json != "" ~} +onepassword_credentials_json=${onepassword_credentials_json} +%{ endif ~} +processcube_api_key=${processcube_api_key} +ansible_python_interpreter=/usr/bin/python3 diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/requirements.txt b/hetzner-setup/ProcessCube.Cloud/ansible/requirements.txt new file mode 100644 index 0000000..415e63e --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/requirements.txt @@ -0,0 +1 @@ +ansible>=2.15.0,<3.0.0 diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/README.md b/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/README.md new file mode 100644 index 0000000..de5bc5f --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/README.md @@ -0,0 +1,117 @@ +# External Secrets Operator Role + +Diese Ansible-Rolle installiert den External Secrets Operator (Version 1.1.0) und konfiguriert die Integration mit 1Password Connect. + +## Voraussetzungen + +- Funktionierender K3s Cluster +- Helm installiert auf dem Master Node +- 1Password Connect Credentials JSON Datei + +## Konfiguration + +Die Konfiguration erfolgt über Terraform-Variablen, die automatisch an Ansible übergeben werden. + +### 1. 1Password Connect Credentials vorbereiten + +Lade die `1password-credentials.json` Datei von 1Password herunter und speichere sie lokal. + +### 2. Terraform-Variablen setzen + +Setze folgende Variablen in `terraform.tfvars`: + +```hcl +# 1Password Connect Configuration +onepassword_credentials_json = "/path/to/1password-credentials.json" +``` + +Diese Variable wird automatisch von Terraform an das Ansible-Inventory übergeben. + +**Wichtig:** Das `onepassword-connect-token` Secret wird NICHT global installiert, sondern muss pro Applikations-Namespace erstellt werden (siehe "Nach der Installation"). + +### 3. Optional: Versionen überschreiben + +Falls notwendig, kannst du die Versionen in `roles/external_secrets/defaults/main.yml` anpassen: + +```yaml +external_secrets_version: "1.1.0" +onepassword_connect_version: "2.0.5" +``` + +## Verwendung + +Die Rolle wird automatisch beim Ausführen von `site.yml` installiert: + +```bash +ansible-playbook -i inventory/hosts.yml site.yml +``` + +Um nur die External Secrets Operator Installation auszuführen: + +```bash +ansible-playbook -i inventory/hosts.yml site.yml --tags external_secrets +``` + +## Was wird installiert? + +1. **External Secrets Operator** (v1.1.0) + - Installiert via Helm Chart + - Namespace: `external-secrets` + +2. **1Password Connect** + - Installiert via Helm Chart (v2.0.5, App v1.8.1) + - Konfiguriert mit den bereitgestellten Credentials + - Verbindet sich mit dem External Secrets Operator + +## Nach der Installation + +### 1. onepassword-connect-token Secret pro Namespace erstellen + +Für jede Applikation/Namespace muss ein eigenes Token-Secret erstellt werden: + +```bash +kubectl create secret generic onepassword-connect-token \ + -n \ + --from-literal=token='' +``` + +### 2. SecretStore erstellen + +Nach dem Erstellen des Token-Secrets kannst du einen SecretStore erstellen: + +```yaml +apiVersion: external-secrets.io/v1 +kind: SecretStore +metadata: + name: processcube-ug +spec: + provider: + onepassword: + connectHost: http://onepassword-connect.external-secrets.svc.cluster.local:8080 + vaults: + "ProcessCube.UG": 1 + auth: + secretRef: + connectTokenSecretRef: + name: onepassword-connect-token + key: token +``` + +## Fehlerbehebung + +### Pods prüfen +```bash +kubectl get pods -n external-secrets +``` + +### Logs prüfen +```bash +kubectl logs -n external-secrets -l app.kubernetes.io/name=external-secrets +kubectl logs -n external-secrets -l app.kubernetes.io/name=connect +``` + +### SecretStore Status prüfen +```bash +kubectl get secretstore -A +kubectl describe secretstore processcube-ug -n +``` diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/defaults/main.yml b/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/defaults/main.yml new file mode 100644 index 0000000..03be839 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/defaults/main.yml @@ -0,0 +1,11 @@ +--- +# External Secrets Operator version +external_secrets_version: "1.1.0" + +# 1Password Connect settings +onepassword_connect_namespace: "external-secrets" +onepassword_connect_version: "2.0.5" + +# 1Password credentials (set via Terraform variables in inventory) +# These values are automatically passed from Terraform to Ansible +# Configure them in terraform.tfvars diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/tasks/main.yml b/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/tasks/main.yml new file mode 100644 index 0000000..8e942d0 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/external_secrets/tasks/main.yml @@ -0,0 +1,103 @@ +--- +- name: Create external-secrets namespace + shell: kubectl create namespace {{ onepassword_connect_namespace }} --dry-run=client -o yaml | kubectl apply -f - + changed_when: true + +- name: Install External Secrets Operator + shell: | + helm repo add external-secrets https://charts.external-secrets.io || true + helm repo update + helm upgrade --install external-secrets \ + external-secrets/external-secrets \ + -n {{ onepassword_connect_namespace }} \ + --version {{ external_secrets_version }} \ + --create-namespace \ + --wait + register: eso_install + changed_when: "'has been upgraded' in eso_install.stdout or 'has been installed' in eso_install.stdout" + +- name: Wait for External Secrets Operator to be ready + shell: | + ready_count=$(kubectl get pods -n {{ onepassword_connect_namespace }} --field-selector=status.phase=Running --no-headers | grep external-secrets | wc -l) + if [ "$ready_count" -ge 1 ]; then + exit 0 + else + exit 1 + fi + args: + executable: /bin/bash + register: eso_status + until: eso_status.rc == 0 + retries: 30 + delay: 10 + changed_when: false + +- name: Resolve 1Password credentials path + set_fact: + onepassword_credentials_full_path: "{{ onepassword_credentials_json | realpath }}" + when: onepassword_credentials_json != "" + delegate_to: localhost + +- name: Copy 1Password credentials to remote host + copy: + content: "{{ lookup('file', onepassword_credentials_full_path) }}" + dest: /tmp/1password-credentials.json + mode: '0600' + when: onepassword_credentials_json != "" + +- name: Install 1Password Connect + shell: | + helm repo add 1password https://1password.github.io/connect-helm-charts || true + helm repo update + helm upgrade --install onepassword-connect \ + 1password/connect \ + -n {{ onepassword_connect_namespace }} \ + --version {{ onepassword_connect_version }} \ + --set-file connect.credentials="/tmp/1password-credentials.json" \ + --set operator.create=false \ + --wait + when: onepassword_credentials_json != "" + register: op_connect_install + changed_when: "'has been upgraded' in op_connect_install.stdout or 'has been installed' in op_connect_install.stdout" + +- name: Wait for 1Password Connect to be ready + shell: | + ready_count=$(kubectl get pods -n {{ onepassword_connect_namespace }} --field-selector=status.phase=Running --no-headers | grep onepassword-connect | wc -l) + if [ "$ready_count" -ge 1 ]; then + exit 0 + else + exit 1 + fi + args: + executable: /bin/bash + register: op_connect_status + until: op_connect_status.rc == 0 + retries: 30 + delay: 10 + changed_when: false + when: onepassword_credentials_json != "" + +- name: Remove temporary 1Password credentials file + file: + path: /tmp/1password-credentials.json + state: absent + when: onepassword_credentials_json != "" + +- name: Display External Secrets installation info + debug: + msg: + - "External Secrets Operator {{ external_secrets_version }} installed successfully!" + - "" + - "1Password Connect installed and configured" + - "" + - "Check status with:" + - " kubectl get pods -n {{ onepassword_connect_namespace }}" + - "" + - "Next steps:" + - "1. Create onepassword-connect-token secret in each application namespace:" + - " kubectl create secret generic onepassword-connect-token \\" + - " -n \\" + - " --from-literal=token=''" + - "" + - "2. Create a SecretStore in your namespace:" + - " See deployment/base/secretstore/secretstore.yaml" diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_addons/tasks/main.yml b/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_addons/tasks/main.yml new file mode 100644 index 0000000..0754f48 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_addons/tasks/main.yml @@ -0,0 +1,230 @@ +--- +- name: Wait for all nodes to be initialized by CCM + shell: | + for node in $(kubectl get nodes -o jsonpath='{.items[*].metadata.name}'); do + providerid=$(kubectl get node $node -o jsonpath='{.spec.providerID}') + if [[ -z "$providerid" || ! "$providerid" =~ ^hcloud:// ]]; then + exit 1 + fi + done + exit 0 + args: + executable: /bin/bash + register: providerid_check + until: providerid_check.rc == 0 + retries: 60 + delay: 5 + changed_when: false + +- name: Download Hetzner CSI Driver manifest + get_url: + url: https://raw.githubusercontent.com/hetznercloud/csi-driver/{{ hcloud_csi_version }}/deploy/kubernetes/hcloud-csi.yml + dest: /tmp/hcloud-csi.yml + mode: '0644' + +- name: Install Hetzner CSI Driver + shell: kubectl apply -f /tmp/hcloud-csi.yml + register: csi_install + changed_when: "'created' in csi_install.stdout or 'configured' in csi_install.stdout" + +- name: Wait for CSI Driver to be ready + shell: kubectl get pods -n kube-system -l app.kubernetes.io/name=hcloud-csi -o jsonpath='{.items[*].status.phase}' + register: csi_status + until: "'Running' in csi_status.stdout" + retries: 30 + delay: 10 + changed_when: false + +- name: Label all nodes for LoadBalancer targeting + shell: kubectl label nodes --all loadbalancer-target=true --overwrite + changed_when: true + +- name: Wait for Traefik DaemonSet to be ready on all nodes + shell: | + node_count=$(kubectl get nodes --no-headers | wc -l) + ready_count=$(kubectl get pods -n kube-system -l app.kubernetes.io/name=traefik --field-selector=status.phase=Running --no-headers 2>/dev/null | wc -l) + if [ "$ready_count" -eq "$node_count" ]; then + exit 0 + else + exit 1 + fi + args: + executable: /bin/bash + register: traefik_status + until: traefik_status.rc == 0 + retries: 30 + delay: 10 + changed_when: false + +- name: Remove Gateway API CRDs (not needed for Ingress-only setup) + shell: | + kubectl delete crd \ + gateways.gateway.networking.k8s.io \ + gatewayclasses.gateway.networking.k8s.io \ + httproutes.gateway.networking.k8s.io \ + grpcroutes.gateway.networking.k8s.io \ + referencegrants.gateway.networking.k8s.io \ + --ignore-not-found + changed_when: true + +- name: Ensure Traefik Service annotations are present (overwrite in case HelmChart reset them) + shell: | + kubectl annotate service traefik \ + -n kube-system \ + load-balancer.hetzner.cloud/location="{{ location }}" \ + load-balancer.hetzner.cloud/name="{{ cluster_name }}-lb" \ + load-balancer.hetzner.cloud/network="{{ cluster_name }}-network" \ + load-balancer.hetzner.cloud/use-private-ip="true" \ + load-balancer.hetzner.cloud/uses-proxyprotocol="false" \ + load-balancer.hetzner.cloud/health-check-interval="10s" \ + load-balancer.hetzner.cloud/health-check-timeout="5s" \ + load-balancer.hetzner.cloud/health-check-retries="3" \ + --overwrite + changed_when: true + +- name: Wait a moment for LoadBalancer to start provisioning + pause: + seconds: 30 + +- name: Wait for LoadBalancer to be provisioned + shell: kubectl get svc traefik -n kube-system -o jsonpath='{.status.loadBalancer.ingress[0].ip}' + register: lb_ip + until: lb_ip.stdout != "" + retries: 60 + delay: 10 + changed_when: false + ignore_errors: true + +- name: Collect CCM logs on LoadBalancer provisioning failure + shell: kubectl logs -n kube-system -l app=hcloud-cloud-controller-manager --tail=100 + register: ccm_logs_on_failure + changed_when: false + ignore_errors: true + when: lb_ip.stdout == "" + +- name: Show CCM logs on failure + debug: + msg: "{{ ccm_logs_on_failure.stdout_lines }}" + when: lb_ip.stdout == "" + +- name: Show Traefik service events on failure + shell: kubectl describe svc traefik -n kube-system + register: traefik_svc_describe + changed_when: false + ignore_errors: true + when: lb_ip.stdout == "" + +- name: Display Traefik service description on failure + debug: + msg: "{{ traefik_svc_describe.stdout_lines }}" + when: lb_ip.stdout == "" + +- name: Fail if LoadBalancer was not provisioned + fail: + msg: "LoadBalancer IP was not assigned after 10 minutes. See CCM logs and service events above for details." + when: lb_ip.stdout == "" + +- name: Display LoadBalancer IP + debug: + msg: "LoadBalancer IP: {{ lb_ip.stdout }}" + +- name: Install cert-manager + shell: | + kubectl apply -f https://github.com/cert-manager/cert-manager/releases/download/v1.20.2/cert-manager.yaml + register: certmanager_install + changed_when: "'created' in certmanager_install.stdout or 'configured' in certmanager_install.stdout" + +- name: Wait for cert-manager to be ready + shell: kubectl get pods -n cert-manager -o jsonpath='{.items[*].status.phase}' + register: certmanager_status + until: certmanager_status.stdout.find('Running') != -1 and (certmanager_status.stdout | regex_findall('Running') | length) >= 3 + retries: 30 + delay: 10 + changed_when: false + +- name: Wait for cert-manager webhook to be ready + shell: kubectl get deployment -n cert-manager cert-manager-webhook -o jsonpath='{.status.availableReplicas}' + register: webhook_status + until: webhook_status.stdout | int > 0 + retries: 30 + delay: 10 + changed_when: false + +- name: Wait for cert-manager webhook endpoint to be ready + shell: kubectl get endpoints -n cert-manager cert-manager-webhook -o jsonpath='{.subsets[*].addresses[*].ip}' + register: webhook_endpoint + until: webhook_endpoint.stdout != "" + retries: 30 + delay: 10 + changed_when: false + +- name: Wait for webhook configuration to be registered + shell: kubectl get validatingwebhookconfigurations cert-manager-webhook + register: webhook_config + until: webhook_config.rc == 0 + retries: 30 + delay: 10 + changed_when: false + ignore_errors: true + +- name: Wait additional time for webhook to be fully operational + pause: + seconds: 60 + +- name: Create Let's Encrypt Staging ClusterIssuer + shell: | + kubectl apply -f - </dev/null || true + systemctl disable multipathd.service multipathd.socket 2>/dev/null || true + changed_when: false + +- name: Configure firewall rules + shell: | + ufw allow 22/tcp + ufw allow 80/tcp + ufw allow 443/tcp + ufw allow from 10.0.0.0/16 to any port 6443 proto tcp + ufw allow from 10.0.0.0/16 to any port 10250 proto tcp + ufw allow from 10.0.0.0/16 + ufw allow from 10.42.0.0/16 + ufw allow from 10.43.0.0/16 + ufw default deny incoming + ufw --force enable + +- name: Check if K3s is already installed + stat: + path: /usr/local/bin/k3s + register: k3s_binary + +- name: Download K3s installation script + get_url: + url: https://get.k3s.io + dest: /tmp/k3s_install.sh + mode: '0700' + when: not k3s_binary.stat.exists + +- name: Get private IP from private network interface + shell: ip -4 addr show enp7s0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}' + register: private_ip + changed_when: false + +- name: Get IP from tailscale network interface + shell: ip -4 addr show tailscale0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}' + register: tailscale_ip + changed_when: false + failed_when: false + when: tailscale_auth_key is defined and tailscale_auth_key != "" + +- name: Install K3s master with Tailscale + shell: | + INSTALL_K3S_CHANNEL=stable \ + /tmp/k3s_install.sh server \ + --cluster-init \ + --node-ip {{ private_ip.stdout }} \ + --advertise-address {{ private_ip.stdout }} \ + --tls-san {{ private_ip.stdout }} \ + --tls-san {{ tailscale_ip.stdout }} \ + --node-name {{ cluster_name }}-master \ + --flannel-backend=vxlan \ + --flannel-iface=enp7s0 \ + --disable servicelb \ + --disable-cloud-controller \ + --write-kubeconfig-mode 644 \ + --kubelet-arg cloud-provider=external + args: + creates: /usr/local/bin/k3s + when: + - not k3s_binary.stat.exists + - tailscale_auth_key is defined and tailscale_auth_key != "" + - tailscale_ip.rc == 0 + +- name: Install K3s master without Tailscale + shell: | + INSTALL_K3S_CHANNEL=stable \ + /tmp/k3s_install.sh server \ + --cluster-init \ + --node-ip {{ private_ip.stdout }} \ + --advertise-address {{ private_ip.stdout }} \ + --tls-san {{ private_ip.stdout }} \ + --node-name {{ cluster_name }}-master \ + --flannel-backend=vxlan \ + --flannel-iface=enp7s0 \ + --disable servicelb \ + --disable-cloud-controller \ + --write-kubeconfig-mode 644 \ + --kubelet-arg cloud-provider=external + args: + creates: /usr/local/bin/k3s + when: + - not k3s_binary.stat.exists + - tailscale_auth_key is not defined or tailscale_auth_key == "" or tailscale_ip.rc != 0 + +- name: Reload systemd daemon + systemd: + daemon_reload: yes + +- name: Enable and start K3s service + systemd: + name: k3s + enabled: yes + state: started + register: k3s_service_start + ignore_errors: true + +- name: Get K3s service status on failure + shell: systemctl status k3s.service --no-pager + register: k3s_status + when: k3s_service_start.failed | default(false) + changed_when: false + failed_when: false + +- name: Get K3s service logs on failure + shell: journalctl -xeu k3s.service --no-pager -n 100 + register: k3s_logs + when: k3s_service_start.failed | default(false) + changed_when: false + failed_when: false + +- name: Display K3s service status + debug: + msg: "{{ k3s_status.stdout_lines }}" + when: k3s_service_start.failed | default(false) + +- name: Display K3s service logs + debug: + msg: "{{ k3s_logs.stdout_lines }}" + when: k3s_service_start.failed | default(false) + +- name: Fail if K3s service could not be started + fail: + msg: "K3s service failed to start. Check the logs above for details." + when: k3s_service_start.failed | default(false) + +- name: Wait for K3s to be ready + wait_for: + path: /etc/rancher/k3s/k3s.yaml + timeout: 300 + +- name: Wait for node to be ready + shell: kubectl get nodes {{ cluster_name }}-master --no-headers | grep -q Ready + register: node_ready + until: node_ready.rc == 0 + retries: 30 + delay: 10 + changed_when: false + +- name: Configure Traefik as Ingress Controller + copy: + dest: /var/lib/rancher/k3s/server/manifests/traefik-config.yaml + mode: '0644' + content: | + apiVersion: helm.cattle.io/v1 + kind: HelmChartConfig + metadata: + name: traefik + namespace: kube-system + spec: + valuesContent: |- + deployment: + kind: DaemonSet + + ports: + web: + port: 8000 + exposedPort: 80 + websecure: + port: 8443 + exposedPort: 443 + + service: + type: LoadBalancer + annotations: + load-balancer.hetzner.cloud/location: "{{ location }}" + load-balancer.hetzner.cloud/name: "{{ cluster_name }}-lb" + load-balancer.hetzner.cloud/network: "{{ cluster_name }}-network" + load-balancer.hetzner.cloud/use-private-ip: "true" + load-balancer.hetzner.cloud/uses-proxyprotocol: "false" + load-balancer.hetzner.cloud/health-check-interval: "10s" + load-balancer.hetzner.cloud/health-check-timeout: "5s" + load-balancer.hetzner.cloud/health-check-retries: "3" + + providers: + kubernetesIngress: + enabled: true + kubernetesGateway: + enabled: false + + rbac: + enabled: true + changed_when: true + +- name: Get K3s token + slurp: + src: /var/lib/rancher/k3s/server/node-token + register: k3s_token_file + +- name: Save K3s token + set_fact: + k3s_node_token: "{{ k3s_token_file.content | b64decode | trim }}" + +- name: Download Helm installation script + get_url: + url: https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 + dest: /tmp/get_helm.sh + mode: '0700' + +- name: Install Helm + shell: /tmp/get_helm.sh + args: + creates: /usr/local/bin/helm + +- name: Set proper ownership for kubeconfig + file: + path: /etc/rancher/k3s/k3s.yaml + owner: root + group: root + mode: '0644' + +- name: Create kubeconfig directory for root user + file: + path: /root/.kube + state: directory + mode: '0755' + +- name: Copy kubeconfig for root user + copy: + src: /etc/rancher/k3s/k3s.yaml + dest: /root/.kube/config + remote_src: yes + mode: '0600' + +- name: Display K3s installation info + debug: + msg: "K3s master node installed successfully. Token saved for workers." diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_user/tasks/main.yml b/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_user/tasks/main.yml new file mode 100644 index 0000000..2194f88 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_user/tasks/main.yml @@ -0,0 +1,41 @@ +--- +- name: Create required directories + file: + path: "{{ item }}" + state: directory + owner: root + group: root + mode: '0755' + loop: + - /etc/rancher/k3s + - /var/lib/rancher/k3s + - /var/log/k3s + +- name: Load required kernel modules + shell: modprobe {{ item }} + loop: + - br_netfilter + - overlay + changed_when: false + +- name: Ensure kernel modules are loaded on boot + copy: + content: | + br_netfilter + overlay + dest: /etc/modules-load.d/k3s.conf + owner: root + group: root + mode: '0644' + +- name: Configure sysctl for k3s + copy: + content: | + net.ipv4.ip_forward = 1 + net.bridge.bridge-nf-call-iptables = 1 + net.bridge.bridge-nf-call-ip6tables = 1 + dest: /etc/sysctl.d/99-k3s.conf + +- name: Apply sysctl settings + shell: sysctl --system + changed_when: false diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_worker/tasks/main.yml b/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_worker/tasks/main.yml new file mode 100644 index 0000000..86f4b95 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/k3s_worker/tasks/main.yml @@ -0,0 +1,173 @@ +--- +- name: Install required packages + apt: + name: + - curl + - wget + - ufw + - netcat-openbsd + state: present + update_cache: yes + +- name: Disable swap + shell: | + swapoff -a + sed -i '/ swap / s/^/#/' /etc/fstab + changed_when: false + +- name: Disable multipathd to prevent interference with Hetzner CSI volumes + shell: | + systemctl stop multipathd.service multipathd.socket 2>/dev/null || true + systemctl disable multipathd.service multipathd.socket 2>/dev/null || true + changed_when: false + +- name: Configure firewall rules + shell: | + ufw allow 22/tcp + ufw allow 80/tcp + ufw allow 443/tcp + ufw allow from 10.0.0.0/16 to any port 10250 proto tcp + ufw allow from 10.0.0.0/16 + ufw allow from 10.42.0.0/16 + ufw allow from 10.43.0.0/16 + ufw default deny incoming + ufw --force enable + +- name: Ensure private network interface has an IP in the cluster subnet + shell: | + for i in $(seq 1 30); do + if ip -4 addr show enp7s0 2>/dev/null | grep -qoP '(?<=inet\s)10\.0\.'; then + exit 0 + fi + ip link set enp7s0 up 2>/dev/null || true + dhclient -1 enp7s0 2>/dev/null || true + sleep 5 + done + echo "enp7s0 never received a 10.0.x.x address" >&2 + exit 1 + register: private_iface_up + changed_when: false + +- name: Show private interface state on failure + shell: ip -4 addr show enp7s0; echo '--- routes ---'; ip route + register: iface_debug + changed_when: false + when: private_iface_up.rc != 0 + +- name: Display private interface diagnostics + debug: + msg: "{{ iface_debug.stdout_lines }}" + when: private_iface_up.rc != 0 + +- name: Wait for master node API to be reachable over the private network + wait_for: + host: "{{ master_ip }}" + port: 6443 + timeout: 120 + msg: >- + Cannot reach the master API at {{ master_ip }}:6443 over the private network. + Check that enp7s0 has a 10.0.x.x address (see diagnostics above) and that the + master's firewall allows traffic from 10.0.0.0/16. + +- name: Check if K3s is already installed + stat: + path: /usr/local/bin/k3s + register: k3s_binary + +- name: Download K3s installation script + get_url: + url: https://get.k3s.io + dest: /tmp/k3s_install.sh + mode: '0700' + when: not k3s_binary.stat.exists + retries: 5 + delay: 10 + register: k3s_script_download + until: k3s_script_download is success + +- name: Get private IP from private network interface + shell: ip -4 addr show enp7s0 | grep -oP '(?<=inet\s)\d+(\.\d+){3}' + register: worker_private_ip + changed_when: false + +- name: Get hostname for node name + shell: hostname + register: node_hostname + changed_when: false + +- name: Display installation parameters + debug: + msg: + - "Installing K3s worker with:" + - " Channel: stable (latest)" + - " Master IP: {{ master_ip }}" + - " Node IP: {{ worker_private_ip.stdout }}" + - " Node Name: {{ node_hostname.stdout }}" + +- name: Install K3s worker + shell: | + INSTALL_K3S_CHANNEL=stable \ + K3S_URL=https://{{ master_ip }}:6443 \ + K3S_TOKEN="{{ k3s_join_token }}" \ + INSTALL_K3S_EXEC="--node-ip {{ worker_private_ip.stdout }} --flannel-iface=enp7s0 --kubelet-arg cloud-provider=external" \ + /tmp/k3s_install.sh + args: + creates: /usr/local/bin/k3s + when: not k3s_binary.stat.exists + register: k3s_install_output + async: 300 + poll: 10 + +- name: Verify K3s was installed successfully + stat: + path: /usr/local/bin/k3s + register: k3s_binary_check + +- name: Fail if K3s installation failed + fail: + msg: "K3s installation failed. Check /tmp/k3s-install.log on the worker node." + when: not k3s_binary_check.stat.exists + +- name: Display K3s installation output + debug: + msg: "{{ k3s_install_output.stdout_lines }}" + when: k3s_install_output is defined and k3s_install_output.stdout_lines is defined + +- name: Enable and start K3s agent service + systemd: + name: k3s-agent + enabled: yes + state: started + daemon_reload: yes + register: agent_service_start + ignore_errors: true + +- name: Wait for K3s agent to become active + shell: systemctl is-active k3s-agent + register: agent_active + until: agent_active.stdout == "active" + retries: 18 + delay: 10 + changed_when: false + ignore_errors: true + +- name: Collect k3s-agent logs if not active + shell: journalctl -xeu k3s-agent --no-pager -n 50 + register: agent_logs + changed_when: false + ignore_errors: true + when: agent_active.stdout != "active" + +- name: Show k3s-agent logs on failure + debug: + msg: "{{ agent_logs.stdout_lines }}" + when: agent_active.stdout != "active" + +- name: Fail if K3s agent is not active + fail: + msg: "K3s agent service failed to start. Check logs above." + when: agent_active.stdout != "active" + +- name: Display K3s worker installation info + debug: + msg: "K3s worker node joined the cluster successfully." diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/roles/tailscale/tasks/main.yml b/hetzner-setup/ProcessCube.Cloud/ansible/roles/tailscale/tasks/main.yml new file mode 100644 index 0000000..2b746c4 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/roles/tailscale/tasks/main.yml @@ -0,0 +1,54 @@ +--- +- name: Install required packages for Tailscale + apt: + name: + - curl + - gnupg + state: present + update_cache: yes + +- name: Add Tailscale GPG key + shell: | + curl -fsSL https://pkgs.tailscale.com/stable/ubuntu/noble.noarmor.gpg | tee /usr/share/keyrings/tailscale-archive-keyring.gpg >/dev/null + args: + creates: /usr/share/keyrings/tailscale-archive-keyring.gpg + +- name: Add Tailscale repository + shell: | + echo "deb [signed-by=/usr/share/keyrings/tailscale-archive-keyring.gpg] https://pkgs.tailscale.com/stable/ubuntu noble main" | tee /etc/apt/sources.list.d/tailscale.list + args: + creates: /etc/apt/sources.list.d/tailscale.list + +- name: Update apt cache + apt: + update_cache: yes + +- name: Install Tailscale + apt: + name: tailscale + state: present + +- name: Check if Tailscale is already authenticated + shell: tailscale status --json | grep -q '"BackendState":"Running"' + register: tailscale_status + changed_when: false + failed_when: false + +- name: Authenticate Tailscale + shell: | + {% if tailscale_tags is defined and tailscale_tags != "" %} + tailscale up --authkey={{ tailscale_auth_key }} --accept-routes --advertise-tags={{ tailscale_tags }} + {% else %} + tailscale up --authkey={{ tailscale_auth_key }} --accept-routes + {% endif %} + when: tailscale_status.rc != 0 + register: tailscale_auth + +- name: Display Tailscale status + shell: tailscale status + register: tailscale_info + changed_when: false + +- name: Show Tailscale connection info + debug: + msg: "{{ tailscale_info.stdout_lines }}" diff --git a/hetzner-setup/ProcessCube.Cloud/ansible/site.yml b/hetzner-setup/ProcessCube.Cloud/ansible/site.yml new file mode 100644 index 0000000..ddb3967 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/ansible/site.yml @@ -0,0 +1,160 @@ +--- +- name: Setup K3s Cluster + hosts: all + gather_facts: yes + become: yes + serial: 1 + tasks: + - name: Wait for system to be ready + wait_for_connection: + timeout: 300 + + - name: Prefer IPv4 over IPv6 system-wide + lineinfile: + path: /etc/gai.conf + line: "precedence ::ffff:0:0/96 100" + create: yes + + - name: Configure reliable DNS resolvers + copy: + content: | + [Resolve] + DNS=185.12.64.1 185.12.64.2 1.1.1.1 + FallbackDNS=8.8.8.8 8.8.4.4 + dest: /etc/systemd/resolved.conf + + - name: Restart systemd-resolved + systemd: + name: systemd-resolved + state: restarted + + - name: Force apt to use IPv4 + copy: + content: 'Acquire::ForceIPv4 "true";' + dest: /etc/apt/apt.conf.d/99force-ipv4 + + - name: Verify DNS resolution + shell: resolvectl status && dig +short google.com + register: dns_check + changed_when: false + + - name: Display DNS status + debug: + var: dns_check.stdout_lines + + - name: Update apt cache + apt: + update_cache: yes + cache_valid_time: 3600 + +- name: Install Tailscale on all nodes + hosts: k3s_cluster + gather_facts: yes + become: yes + roles: + - role: tailscale + when: tailscale_auth_key is defined and tailscale_auth_key != "" + +- name: Create K3s user and prepare system + hosts: k3s_cluster + gather_facts: yes + become: yes + roles: + - k3s_user + +- name: Configure K3s Master Node + hosts: k3s_master + gather_facts: yes + become: yes + roles: + - k3s_master + +- name: Install Hetzner Cloud Controller Manager + hosts: k3s_master + gather_facts: no + become: yes + roles: + - k3s_ccm + +- name: Retrieve K3s token from master + hosts: k3s_master + gather_facts: no + become: yes + tasks: + - name: Read K3s token + slurp: + src: /var/lib/rancher/k3s/server/node-token + register: k3s_master_token + + - name: Set token fact for workers + set_fact: + k3s_join_token: "{{ k3s_master_token.content | b64decode | trim }}" + delegate_to: "{{ item }}" + delegate_facts: true + with_items: "{{ groups['k3s_workers'] }}" + +- name: Configure K3s Worker Nodes + hosts: k3s_workers + gather_facts: yes + become: yes + roles: + - k3s_worker + +- name: Install K3s Addons (CSI, Ingress, cert-manager) + hosts: k3s_master + gather_facts: no + become: yes + roles: + - k3s_addons + +- name: Install External Secrets Operator with 1Password + hosts: k3s_master + gather_facts: no + become: yes + roles: + - role: external_secrets + when: onepassword_credentials_json is defined and onepassword_credentials_json != "" + +- name: Verify Cluster + hosts: k3s_master + gather_facts: no + become: yes + tasks: + - name: Wait for all nodes to be ready + shell: kubectl get nodes --no-headers | awk '$2 == "Ready" {count++} END {print count+0}' + register: ready_nodes + until: ready_nodes.stdout|int == groups['k3s_cluster']|length + retries: 30 + delay: 10 + changed_when: false + ignore_errors: true + + - name: Display cluster status + shell: kubectl get nodes + register: cluster_status + changed_when: false + + - name: Show cluster nodes + debug: + var: cluster_status.stdout_lines + + - name: Describe NotReady nodes for diagnostics + shell: | + for node in $(kubectl get nodes --no-headers | awk '$2 != "Ready" {print $1}'); do + echo "=== Node: $node ===" + kubectl describe node "$node" | tail -30 + done + register: notready_nodes_info + changed_when: false + ignore_errors: true + when: ready_nodes.stdout|int < groups['k3s_cluster']|length + + - name: Show NotReady node details + debug: + msg: "{{ notready_nodes_info.stdout_lines }}" + when: ready_nodes.stdout|int < groups['k3s_cluster']|length + + - name: Fail if not all nodes are ready + fail: + msg: "Only {{ ready_nodes.stdout }} of {{ groups['k3s_cluster']|length }} nodes are Ready. See node details above." + when: ready_nodes.stdout|int < groups['k3s_cluster']|length diff --git a/hetzner-setup/ProcessCube.Cloud/main.tf b/hetzner-setup/ProcessCube.Cloud/main.tf new file mode 100644 index 0000000..55c9893 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/main.tf @@ -0,0 +1,293 @@ +terraform { + required_version = ">= 1.0" + required_providers { + hcloud = { + source = "hetznercloud/hcloud" + version = "~> 1.45" + } + random = { + source = "hashicorp/random" + version = "~> 3.5" + } + local = { + source = "hashicorp/local" + version = "~> 2.4" + } + null = { + source = "hashicorp/null" + version = "~> 3.2" + } + } +} + +provider "hcloud" { + token = var.hcloud_token +} + +# SSH Key for accessing the servers +resource "hcloud_ssh_key" "k3s" { + name = "${var.cluster_name}-key" + public_key = trimspace(file(var.ssh_public_key_path)) +} + +# Network for the cluster +data "hcloud_location" "cluster" { + name = var.location +} + +resource "hcloud_network" "k3s" { + name = "${var.cluster_name}-network" + ip_range = "10.0.0.0/16" +} + +resource "hcloud_network_subnet" "k3s" { + network_id = hcloud_network.k3s.id + type = "cloud" + network_zone = data.hcloud_location.cluster.network_zone + ip_range = "10.0.1.0/24" + + lifecycle { + create_before_destroy = false + } +} + +# Firewall rules for K3s nodes +resource "hcloud_firewall" "k3s" { + name = "${var.cluster_name}-firewall" + + # SSH access (temporary for installation) + rule { + direction = "in" + protocol = "tcp" + port = "22" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + # Internal cluster communication + rule { + direction = "in" + protocol = "tcp" + port = "any" + source_ips = [ + "10.0.0.0/16" + ] + } + + rule { + direction = "in" + protocol = "udp" + port = "any" + source_ips = [ + "10.0.0.0/16" + ] + } + + # HTTP traffic for ingress + rule { + direction = "in" + protocol = "tcp" + port = "80" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + # HTTPS traffic for ingress + rule { + direction = "in" + protocol = "tcp" + port = "443" + source_ips = [ + "0.0.0.0/0", + "::/0" + ] + } + + # Kubernetes API Server (only from internal network and bastion) + rule { + direction = "in" + protocol = "tcp" + port = "6443" + source_ips = [ + "10.0.0.0/16" + ] + } +} + +# K3s Master Node (Server) +resource "hcloud_server" "k3s_master" { + name = "${var.cluster_name}-master" + server_type = var.server_type + image = var.server_image + location = var.location + ssh_keys = [hcloud_ssh_key.k3s.id] + firewall_ids = [hcloud_firewall.k3s.id] + + network { + network_id = hcloud_network.k3s.id + ip = "10.0.1.2" + } + + public_net { + ipv4_enabled = true + ipv6_enabled = false + } + + labels = { + role = "master" + cluster = var.cluster_name + } + + depends_on = [ + hcloud_network_subnet.k3s + ] +} + +# K3s Worker Nodes +resource "hcloud_server" "k3s_worker" { + count = var.worker_count + name = "${var.cluster_name}-worker-${count.index + 1}" + server_type = var.server_type + image = var.server_image + location = var.location + ssh_keys = [hcloud_ssh_key.k3s.id] + firewall_ids = [hcloud_firewall.k3s.id] + + network { + network_id = hcloud_network.k3s.id + ip = "10.0.1.${count.index + 3}" + } + + public_net { + ipv4_enabled = true + ipv6_enabled = false + } + + labels = { + role = "worker" + cluster = var.cluster_name + } + + depends_on = [ + hcloud_network_subnet.k3s, + hcloud_server.k3s_master + ] +} + +# Generate random token for K3s cluster +resource "random_password" "k3s_token" { + length = 32 + special = false +} + +# Generate Ansible inventory +resource "local_file" "ansible_inventory" { + content = templatefile("${path.module}/ansible/inventory/hosts.tpl", { + master_private_ip = one([for net in hcloud_server.k3s_master.network : net.ip]) + master_public_ip = hcloud_server.k3s_master.ipv4_address + worker_private_ips = [for worker in hcloud_server.k3s_worker : one([for net in worker.network : net.ip])] + worker_public_ips = [for worker in hcloud_server.k3s_worker : worker.ipv4_address] + k3s_token = random_password.k3s_token.result + cluster_name = var.cluster_name + ssh_private_key_path = var.ssh_private_key_path + hcloud_token = var.hcloud_token + hcloud_csi_version = var.hcloud_csi_version + hcloud_ccm_version = var.hcloud_ccm_version + network_id = hcloud_network.k3s.id + location = var.location + letsencrypt_email = var.letsencrypt_email + tailscale_auth_key = var.tailscale_auth_key + tailscale_tags = var.tailscale_tags + onepassword_credentials_json = var.onepassword_credentials_json + processcube_api_key = var.processcube_api_key + }) + filename = "${path.module}/ansible/inventory/hosts" + + depends_on = [ + hcloud_server.k3s_master, + hcloud_server.k3s_worker + ] +} + +# Wait for servers to be ready +resource "null_resource" "wait_for_servers" { + provisioner "local-exec" { + command = <<-EOT + for ip in ${hcloud_server.k3s_master.ipv4_address} ${join(" ", hcloud_server.k3s_worker[*].ipv4_address)}; do + echo "Waiting for SSH on $ip..." + timeout 300 bash -c "until ssh-keyscan -T 10 -p 22 $ip 2>/dev/null | grep -q ssh; do sleep 5; done" + echo "SSH ready on $ip" + done + EOT + } + + depends_on = [ + local_file.ansible_inventory + ] +} + +# Run Ansible playbook +resource "null_resource" "ansible_provisioning" { + provisioner "local-exec" { + command = "cd ${path.module}/ansible && ansible-playbook -i inventory/hosts site.yml" + } + + depends_on = [ + null_resource.wait_for_servers + ] + + triggers = { + master_id = hcloud_server.k3s_master.id + worker_ids = join(",", hcloud_server.k3s_worker[*].id) + inventory = local_file.ansible_inventory.content + } +} + +# Cleanup script for LoadBalancers before destroying network +# Uses Terraform's external data source to list and delete LoadBalancers via API +resource "null_resource" "cleanup_loadbalancers" { + triggers = { + network_id = hcloud_network.k3s.id + hcloud_token = var.hcloud_token + } + + provisioner "local-exec" { + when = destroy + command = <<-EOT + #!/bin/bash + set -e + + echo "Checking for LoadBalancers attached to network ${self.triggers.network_id}..." + + # Use curl to query Hetzner API + LBS=$(curl -s -H "Authorization: Bearer ${self.triggers.hcloud_token}" \ + "https://api.hetzner.cloud/v1/load_balancers" | \ + jq -r --arg net_id "${self.triggers.network_id}" \ + '.load_balancers[] | select(.private_net[]?.network == ($net_id | tonumber)) | .id') + + if [ -z "$LBS" ]; then + echo "No LoadBalancers found attached to network." + exit 0 + fi + + for lb_id in $LBS; do + echo "Deleting LoadBalancer $lb_id..." + curl -s -X DELETE -H "Authorization: Bearer ${self.triggers.hcloud_token}" \ + "https://api.hetzner.cloud/v1/load_balancers/$lb_id" || true + done + + echo "Waiting for LoadBalancers to be fully deleted..." + sleep 15 + EOT + interpreter = ["bash", "-c"] + } + + depends_on = [ + null_resource.ansible_provisioning, + hcloud_network.k3s + ] +} diff --git a/hetzner-setup/ProcessCube.Cloud/outputs.tf b/hetzner-setup/ProcessCube.Cloud/outputs.tf new file mode 100644 index 0000000..b7ae626 --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/outputs.tf @@ -0,0 +1,53 @@ +output "master_public_ip" { + description = "Public IP address of the master node" + value = hcloud_server.k3s_master.ipv4_address +} + +output "master_private_ip" { + description = "Private IP address of the master node" + value = one([for net in hcloud_server.k3s_master.network : net.ip]) +} + +output "worker_public_ips" { + description = "Public IP addresses of worker nodes" + value = [for worker in hcloud_server.k3s_worker : worker.ipv4_address] +} + +output "worker_private_ips" { + description = "Private IP addresses of worker nodes" + value = [for worker in hcloud_server.k3s_worker : one([for net in worker.network : net.ip])] +} + +output "k3s_token" { + description = "K3s cluster token (sensitive)" + value = random_password.k3s_token.result + sensitive = true +} + +output "kubeconfig_command" { + description = "Command to get kubeconfig from master node" + value = "ssh root@${hcloud_server.k3s_master.ipv4_address} 'cat /etc/rancher/k3s/k3s.yaml' > kubeconfig.yaml" +} + +output "network_id" { + description = "ID of the private network" + value = hcloud_network.k3s.id +} + +output "network_name" { + description = "Name of the private network" + value = hcloud_network.k3s.name +} + +output "load_balancer_info" { + description = "Information about LoadBalancer provisioning" + value = "LoadBalancer will be created automatically by Hetzner Cloud Controller Manager when Nginx Ingress Controller is deployed. Check with: kubectl get svc -n ingress-nginx ingress-nginx-controller" +} + +output "ssh_commands" { + description = "SSH commands to access nodes" + value = { + master = "ssh root@${hcloud_server.k3s_master.ipv4_address}" + workers = [for worker in hcloud_server.k3s_worker : "ssh root@${worker.ipv4_address}"] + } +} diff --git a/hetzner-setup/ProcessCube.Cloud/terraform.tfvars.example b/hetzner-setup/ProcessCube.Cloud/terraform.tfvars.example new file mode 100644 index 0000000..9e764be --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/terraform.tfvars.example @@ -0,0 +1,30 @@ +# Hetzner Cloud Configuration +hcloud_token = "YOUR_HETZNER_API_TOKEN_HERE" + +# Cluster Configuration +cluster_name = "processcube-k3s" +location = "fsn1" # Options: nbg1, fsn1, hel1 +server_type = "cx43" # Options: cx11, cx21, cx31, cx41, cx51 +worker_count = 2 + +# Hetzer csi-Driver +hcloud_csi_version = "v2.18.1" + +# SSH Key Paths +ssh_public_key_path = "~/.ssh/id_rsa.pub" +ssh_private_key_path = "~/.ssh/id_rsa" + +# Let's encrypt +letsencrypt_email = "info@processcube.io" + +# Tailscale Configuration +tailscale_auth_key = "YOUR_TAILSCALE_AUTH_KEY_HERE" +# tailscale_tags = "tag:k3s" # Optional: Uncomment to use tags + +# 1Password Connect Configuration for External Secrets Operator (Optional) +# onepassword_credentials_json = "/path/to/1password-credentials.json" +# Note: External Secrets Operator will only be installed if this is set +# Note: onepassword-connect-token must be created per application namespace + +# ProcessCube Marketplace Configuration +processcube_api_key = "YOUR_PROCESSCUBE_API_KEY_HERE" diff --git a/hetzner-setup/ProcessCube.Cloud/variables.tf b/hetzner-setup/ProcessCube.Cloud/variables.tf new file mode 100644 index 0000000..41ae35a --- /dev/null +++ b/hetzner-setup/ProcessCube.Cloud/variables.tf @@ -0,0 +1,93 @@ +variable "hcloud_token" { + description = "Hetzner Cloud API Token" + type = string + sensitive = true +} + +variable "cluster_name" { + description = "Name of the K3s cluster" + type = string + default = "processcube-k3s" +} + +variable "location" { + description = "Hetzner Cloud location" + type = string + default = "fsn1" + # Available: nbg1 (Nuremberg), fsn1 (Falkenstein), hel1 (Helsinki) +} + +variable "server_type" { + description = "Hetzner Cloud server type" + type = string + default = "cx11" # 2 vCPU, 4GB RAM + # Options: cx11, cx21, cx31, cx41, cx51 + # cpx11, cpx21, cpx31, cpx41, cpx51 (AMD) +} + +variable "server_image" { + description = "Server image to use" + type = string + default = "ubuntu-24.04" +} + +variable "worker_count" { + description = "Number of worker nodes" + type = number + default = 2 +} + +variable "hcloud_csi_version" { + description = "Hetzner Cloud CSI Driver version" + type = string + default = "v2.18.1" +} + +variable "hcloud_ccm_version" { + description = "Hetzner Cloud Controller Manager version" + type = string + default = "v1.31.0" +} + +variable "ssh_public_key_path" { + description = "Path to SSH public key file" + type = string + default = "~/.ssh/id_rsa.pub" +} + +variable "ssh_private_key_path" { + description = "Path to SSH private key file for Ansible" + type = string + default = "~/.ssh/id_rsa" +} + +variable "letsencrypt_email" { + description = "Email address for Let's Encrypt certificate notifications" + type = string +} + +variable "tailscale_auth_key" { + description = "Tailscale authentication key (optional - Tailscale will only be installed if this is set)" + type = string + sensitive = true + default = "" +} + +variable "tailscale_tags" { + description = "Tailscale tags to apply to nodes (optional)" + type = string + default = "" +} + +variable "onepassword_credentials_json" { + description = "Path to 1Password Connect credentials JSON file (optional - External Secrets Operator will only be installed if this is set)" + type = string + sensitive = true + default = "" +} + +variable "processcube_api_key" { + description = "ProcessCube API key for marketplace.processcube.io image registry" + type = string + sensitive = true +} diff --git a/package.json b/package.json index b068be0..e2c5124 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { "name": "processcube_deployment", - "version": "1.8.2", + "version": "1.9.1", "description": "Deployment Repository of ProcessCube" } diff --git a/sample/base/kustomization.yaml b/sample/base/kustomization.yaml index c638de5..8f58836 100644 --- a/sample/base/kustomization.yaml +++ b/sample/base/kustomization.yaml @@ -11,8 +11,8 @@ images: - name: ghcr.io/5minds/processcube_authority newTag: 3.2.0 - name: ghcr.io/5minds/processcube_engine - newTag: 19.0.0-extensions-2.3.0 + newTag: 19.2.1-extensions-2.3.0 - name: ghcr.io/5minds/processcube_lowcode - newTag: 6.1.1 + newTag: 7.0.2 - name: postgres newTag: "17" \ No newline at end of file diff --git a/base/artifactShipper/github-token-external-secret.yaml b/sample/overlays/dev/artifactShipper/github-token-external-secret.yaml similarity index 100% rename from base/artifactShipper/github-token-external-secret.yaml rename to sample/overlays/dev/artifactShipper/github-token-external-secret.yaml diff --git a/sample/overlays/dev/artifactShipper/kustomization.yaml b/sample/overlays/dev/artifactShipper/kustomization.yaml new file mode 100644 index 0000000..90c59ea --- /dev/null +++ b/sample/overlays/dev/artifactShipper/kustomization.yaml @@ -0,0 +1,5 @@ +apiVersion: kustomize.config.k8s.io/v1beta1 +kind: Kustomization + +resources: + - github-token-external-secret.yaml \ No newline at end of file diff --git a/sample/overlays/dev/authority/config.json b/sample/overlays/dev/authority/config.json index 16aaf5b..232c9f6 100644 --- a/sample/overlays/dev/authority/config.json +++ b/sample/overlays/dev/authority/config.json @@ -25,17 +25,24 @@ "redirect_uris": ["https://app.sample.dev.5minds.cloud"] }, { - "clientId": "nodered", - "clientSecret": "79C29A79-607D-452B-B4CA-AF79BF0D44E9", - "scope": "openid email profile", - "grant_types": ["authorization_code", "refresh_token"], - "response_types": ["code"], - "redirect_uris": [ - "https://nodered.sampleapp.dev.5minds.cloud/auth/strategy/callback", - "https://nodered.sampleapp.dev.5minds.cloud/auth/dashboard/callback" - ], - "post_logout_redirect_uris": ["https://nodered.sampleapp.dev.5minds.cloud"], - "corsOrigins": ["https://nodered.sampleapp.dev.5minds.cloud"] + "clientId": "LowCodeEditorClient", + "clientSecret": "79C29A79-607D-452B-B4CA-AF79BF0D44E9", + "scope": "openid email profile nodered lanes engine_read engine_write", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "redirect_uris": ["https://lowcode.sampleapp.dev.5minds.cloud/auth/strategy/callback"], + "post_logout_redirect_uris": ["https://lowcode.sampleapp.dev.5minds.cloud/"], + "corsOrigins": ["https://lowcode.sampleapp.dev.5minds.cloud"] + }, + { + "clientId": "LowCodeDashboardClient", + "clientSecret": "05844EF2-08B9-473D-8FD1-9A96289F5304", + "scope": "openid email profile lanes engine_read engine_write engine_admin", + "grant_types": ["authorization_code", "refresh_token"], + "response_types": ["code"], + "redirect_uris": ["https://lowcode.sampleapp.dev.5minds.cloud/auth/dashboard/callback"], + "post_logout_redirect_uris": ["https://lowcode.sampleapp.dev.5minds.cloud/dashboard"], + "corsOrigins": ["https://lowcode.sampleapp.dev.5minds.cloud"] } ], "database": { diff --git a/sample/overlays/dev/authority/httproute-patch.yml b/sample/overlays/dev/authority/httproute-patch.yml new file mode 100644 index 0000000..2ff554a --- /dev/null +++ b/sample/overlays/dev/authority/httproute-patch.yml @@ -0,0 +1,3 @@ +- op: replace + path: /spec/hostnames/0 + value: authority.sampleapp.dev.5minds.cloud diff --git a/sample/overlays/dev/authority/ingress-patch.yml b/sample/overlays/dev/authority/ingress-patch.yml deleted file mode 100644 index 64d4b6c..0000000 --- a/sample/overlays/dev/authority/ingress-patch.yml +++ /dev/null @@ -1,6 +0,0 @@ -- op: replace #action - path: "/spec/rules/0/host" #resource we want to change - value: authority.sampleapp.dev.5minds.cloud -- op: replace #action - path: "/spec/tls/0/hosts/0" #resource we want to change - value: authority.sampleapp.dev.5minds.cloud \ No newline at end of file diff --git a/sample/overlays/dev/engine/httproute-patch.yml b/sample/overlays/dev/engine/httproute-patch.yml new file mode 100644 index 0000000..aa0ae60 --- /dev/null +++ b/sample/overlays/dev/engine/httproute-patch.yml @@ -0,0 +1,3 @@ +- op: replace + path: /spec/hostnames/0 + value: engine.sampleapp.dev.5minds.cloud diff --git a/sample/overlays/dev/engine/ingress-patch.yml b/sample/overlays/dev/engine/ingress-patch.yml deleted file mode 100644 index 6014a12..0000000 --- a/sample/overlays/dev/engine/ingress-patch.yml +++ /dev/null @@ -1,6 +0,0 @@ -- op: replace #action - path: "/spec/rules/0/host" #resource we want to change - value: engine.sampleapp.dev.5minds.cloud -- op: replace #action - path: "/spec/tls/0/hosts/0" #resource we want to change - value: engine.sampleapp.dev.5minds.cloud \ No newline at end of file diff --git a/sample/overlays/dev/gateway-patch.yml b/sample/overlays/dev/gateway-patch.yml new file mode 100644 index 0000000..4abeb04 --- /dev/null +++ b/sample/overlays/dev/gateway-patch.yml @@ -0,0 +1,9 @@ +- op: replace + path: /spec/listeners/1/hostname + value: engine.sampleapp.dev.5minds.cloud +- op: replace + path: /spec/listeners/2/hostname + value: authority.sampleapp.dev.5minds.cloud +- op: replace + path: /spec/listeners/3/hostname + value: lowcode.sampleapp.dev.5minds.cloud diff --git a/sample/overlays/dev/kustomization.yaml b/sample/overlays/dev/kustomization.yaml index 063ed7e..1bcbda5 100644 --- a/sample/overlays/dev/kustomization.yaml +++ b/sample/overlays/dev/kustomization.yaml @@ -8,30 +8,35 @@ resources: patches: - target: - group: networking.k8s.io - kind: Ingress + group: gateway.networking.k8s.io + kind: Gateway + name: processcube-gateway + path: ./gateway-patch.yml + - target: + group: gateway.networking.k8s.io + kind: HTTPRoute name: engine - path: ./engine/ingress-patch.yml + path: ./engine/httproute-patch.yml - target: group: apps kind: Deployment name: engine path: ./engine/deployment-patch.yml - target: - group: networking.k8s.io - kind: Ingress + group: gateway.networking.k8s.io + kind: HTTPRoute name: authority - path: ./authority/ingress-patch.yml + path: ./authority/httproute-patch.yml - target: group: apps kind: Deployment name: nodered path: ./lowcode/deployment-patch.yml - target: - group: networking.k8s.io - kind: Ingress + group: gateway.networking.k8s.io + kind: HTTPRoute name: nodered - path: ./lowcode/ingress-patch.yml + path: ./lowcode/httproute-patch.yml configMapGenerator: diff --git a/sample/overlays/dev/lowcode/deployment-patch.yml b/sample/overlays/dev/lowcode/deployment-patch.yml index d8671ef..48a44c9 100644 --- a/sample/overlays/dev/lowcode/deployment-patch.yml +++ b/sample/overlays/dev/lowcode/deployment-patch.yml @@ -7,14 +7,24 @@ path: "/spec/template/spec/containers/0/env/-" #resource we want to change value: name: NODERED_BASE_URL - value: "https://nodered.sampleapp.dev.5minds.cloud" + value: "https://lowcode.sampleapp.dev.5minds.cloud" - op: add #action path: "/spec/template/spec/containers/0/env/-" #resource we want to change value: name: NODERED_CLIENT_ID - value: "nodered" + value: "LowCodeEditorClient" - op: add #action path: "/spec/template/spec/containers/0/env/-" #resource we want to change value: name: NODERED_CLIENT_SECRET - value: "79C29A79-607D-452B-B4CA-AF79BF0D44E9" \ No newline at end of file + value: "79C29A79-607D-452B-B4CA-AF79BF0D44E9" +- op: add #action + path: "/spec/template/spec/containers/0/env/-" #resource we want to change + value: + name: NODERED_DASHBOARD_CLIENT_ID + value: "LowCodeDashboardClient" +- op: add #action + path: "/spec/template/spec/containers/0/env/-" #resource we want to change + value: + name: NODERED_DASHBOARD_CLIENT_SECRET + value: "05844EF2-08B9-473D-8FD1-9A96289F5304" \ No newline at end of file diff --git a/sample/overlays/dev/lowcode/httproute-patch.yml b/sample/overlays/dev/lowcode/httproute-patch.yml new file mode 100644 index 0000000..7886ed8 --- /dev/null +++ b/sample/overlays/dev/lowcode/httproute-patch.yml @@ -0,0 +1,3 @@ +- op: replace + path: /spec/hostnames/0 + value: lowcode.sampleapp.dev.5minds.cloud diff --git a/sample/overlays/dev/lowcode/ingress-patch.yml b/sample/overlays/dev/lowcode/ingress-patch.yml deleted file mode 100644 index 2c79137..0000000 --- a/sample/overlays/dev/lowcode/ingress-patch.yml +++ /dev/null @@ -1,6 +0,0 @@ -- op: replace #action - path: "/spec/rules/0/host" #resource we want to change - value: nodered.sampleapp.dev.5minds.cloud -- op: replace #action - path: "/spec/tls/0/hosts/0" #resource we want to change - value: nodered.sampleapp.dev.5minds.cloud \ No newline at end of file